// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600.  If not, see <https://www.gnu.org/licenses/>.

package cartridge

import (
	"fmt"
	"io"

	"github.com/jetsetilly/gopher2600/cartridgeloader"
	"github.com/jetsetilly/gopher2600/environment"
	"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
	"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
	"github.com/jetsetilly/gopher2600/logger"
)

// from http://www.taswegian.com/WoodgrainWizard/tiki-index.php?page=CV
//
// $F000-$F3FF read from RAM
// $F400-$F7FF write to RAM
// $F800-$FFFF ROM
//
// this seems to be the same principle as other simple RAM cartridges, like the
// Atari Superchip or CBS cartridges
//
// from bankswitch_sizes.txt:
//
// cartridges:
//   - MagiCard
//   - Video Lfe
//
// and the reason why I implemented this format for Gopher2600:
//
// https://forums.atariage.com/topic/342021-new-512-bytes-demo-released-gar-nix/
type commavid struct {
	env *environment.Environment

	mappingID string

	bankSize int
	bankData []uint8

	// rewindable state
	state *commavidState
}

func newCommaVid(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) {
	data, err := io.ReadAll(loader)
	if err != nil {
		return nil, fmt.Errorf("CV: %w", err)
	}

	cart := &commavid{
		env:       env,
		bankSize:  4096,
		mappingID: "CV",
		state:     newCommaVidState(),
	}

	cart.bankData = make([]uint8, cart.bankSize)
	if len(data) < 2048 {
		// place undersized binaries at the end of memory
		copy(cart.bankData[4096-len(data):], data)
		logger.Logf("CV", "placing undersized commavid data at end of cartridge memory")
	} else if len(data) == 2048 {
		copy(cart.bankData[2048:], data[:2048])
		logger.Logf("CV", "placing 2k commavid data at end of cartridge memory")
	} else {
		return nil, fmt.Errorf("CV: unhandled size for commavid cartridges (%d)", len(data))
	}

	return cart, nil
}

// MappedBanks implements the mapper.CartMapper interface.
func (cart *commavid) MappedBanks() string {
	return fmt.Sprintf("Bank: 0")
}

// ID implements the mapper.CartMapper interface.
func (cart *commavid) ID() string {
	return cart.mappingID
}

// Snapshot implements the mapper.CartMapper interface.
func (cart *commavid) Snapshot() mapper.CartMapper {
	n := *cart
	n.state = cart.state.Snapshot()
	return &n
}

// Plumb implements the mapper.CartMapper interface.
func (cart *commavid) Plumb(env *environment.Environment) {
	cart.env = env
}

// Reset implements the mapper.CartMapper interface.
func (cart *commavid) Reset() {
	// always starting with random state. this is because Video Life, one of
	// the few original CommaVid cartridges, expects it for the opening effect
	for i := range cart.state.ram {
		cart.state.ram[i] = uint8(cart.env.Random.NoRewind(0xff))
	}
}

// Access implements the mapper.CartMapper interface.
func (cart *commavid) Access(addr uint16, _ bool) (uint8, uint8, error) {
	if addr >= 0x0000 && addr <= 0x03ff {
		return cart.state.ram[addr], mapper.CartDrivenPins, nil
	}
	if addr >= 0x0400 && addr <= 0x07ff {
		return 0, 0, nil
	}

	return cart.bankData[addr], mapper.CartDrivenPins, nil
}

// AccessVolatile implements the mapper.CartMapper interface.
func (cart *commavid) AccessVolatile(addr uint16, data uint8, poke bool) error {
	if addr >= 0x0400 && addr <= 0x07ff {
		cart.state.ram[addr&0x03ff] = data
		return nil
	}

	if poke {
		cart.bankData[addr] = data
		return nil
	}

	return nil
}

// NumBanks implements the mapper.CartMapper interface.
func (cart *commavid) NumBanks() int {
	return 1
}

// GetBank implements the mapper.CartMapper interface.
func (cart *commavid) GetBank(addr uint16) mapper.BankInfo {
	// commavid cartridges are like atari cartridges in that the entire address
	// space points to the selected bank
	return mapper.BankInfo{Number: 0, IsRAM: addr <= 0x07ff}
}

// Patch implements the mapper.CartPatchable interface
func (cart *commavid) Patch(offset int, data uint8) error {
	if offset >= cart.bankSize {
		return fmt.Errorf("CV: patch offset too high (%d)", offset)
	}
	cart.bankData[offset] = data
	return nil
}

// AccessPassive implements the mapper.CartMapper interface.
func (cart *commavid) AccessPassive(addr uint16, data uint8) error {
	return nil
}

// Step implements the mapper.CartMapper interface.
func (cart *commavid) Step(_ float32) {
}

// GetRAM implements the mapper.CartRAMBus interface.
func (cart *commavid) GetRAM() []mapper.CartRAM {
	r := make([]mapper.CartRAM, 1)
	r[0] = mapper.CartRAM{
		Label:  "CommaVid",
		Origin: 0x0,
		Data:   make([]uint8, len(cart.state.ram)),
		Mapped: true,
	}
	copy(r[0].Data, cart.state.ram)
	return r
}

// PutRAM implements the mapper.CartRAMBus interface.
func (cart *commavid) PutRAM(_ int, idx int, data uint8) {
	cart.state.ram[idx] = data
}

// IterateBank implements the mapper.CartMapper interface.
func (cart *commavid) CopyBanks() []mapper.BankContent {
	c := make([]mapper.BankContent, 1)
	c[0] = mapper.BankContent{Number: 0,
		Data:    cart.bankData,
		Origins: []uint16{memorymap.OriginCart},
	}
	return c
}

// rewindable state for commavid cartridges
type commavidState struct {
	// commavid cartridges are distinguished for having 1k of onboard ram
	ram []uint8
}

func newCommaVidState() *commavidState {
	const commavidRAMsize = 1024

	return &commavidState{
		ram: make([]uint8, commavidRAMsize),
	}
}

// Snapshot implements the mapper.CartMapper interface.
func (s *commavidState) Snapshot() *commavidState {
	n := *s
	n.ram = make([]uint8, len(s.ram))
	copy(n.ram, s.ram)
	return &n
}
