// 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 usdful,
// 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"
)

type df struct {
	env *environment.Environment

	mappingID string

	// df cartridges have 3 banks of 4096 bytes
	bankSize int
	banks    [][]uint8

	// rewindable state
	state *dfState
}

// example ROM: penult RPG.
func newDF(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) {
	data, err := io.ReadAll(loader)
	if err != nil {
		return nil, fmt.Errorf("DF: %w", err)
	}

	cart := &df{
		env:       env,
		mappingID: "DF",
		bankSize:  4096,
		state:     newDfState(),
	}

	if len(data) != cart.bankSize*cart.NumBanks() {
		return nil, fmt.Errorf("DF: wrong number of bytes in the cartridge data")
	}

	cart.banks = make([][]uint8, cart.NumBanks())

	for k := 0; k < cart.NumBanks(); k++ {
		cart.banks[k] = make([]uint8, cart.bankSize)
		offset := k * cart.bankSize
		copy(cart.banks[k], data[offset:offset+cart.bankSize])
	}

	return cart, nil
}

// MappedBanks implements the mapper.CartMapper interface.
func (cart *df) MappedBanks() string {
	return fmt.Sprintf("Bank: %d", cart.state.bank)
}

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

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

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

// Reset implements the mapper.CartMapper interface.
func (cart *df) Reset() {
	for i := range cart.state.ram {
		if cart.env.Prefs.RandomState.Get().(bool) {
			cart.state.ram[i] = uint8(cart.env.Random.NoRewind(0xff))
		} else {
			cart.state.ram[i] = 0
		}
	}

	cart.state.bank = 15
}

// Access implements the mapper.CartMapper interface.
func (cart *df) Access(addr uint16, peek bool) (uint8, uint8, error) {
	if addr <= 0x007f {
		return 0, 0, nil
	}
	if addr >= 0x0080 && addr <= 0x00ff {
		return cart.state.ram[addr-0x80], mapper.CartDrivenPins, nil
	}

	if !peek {
		cart.bankswitch(addr)
	}

	return cart.banks[cart.state.bank][addr], mapper.CartDrivenPins, nil
}

// AccessVolatile implements the mapper.CartMapper interface.
func (cart *df) AccessVolatile(addr uint16, data uint8, poke bool) error {
	if !poke {
		if cart.bankswitch(addr) {
			return nil
		}
	}

	if addr <= 0x007f {
		cart.state.ram[addr] = data
		return nil
	}

	if poke {
		cart.banks[cart.state.bank][addr] = data
		return nil
	}

	return nil
}

// bankswitch on hotspot access.
func (cart *df) bankswitch(addr uint16) bool {
	if addr >= 0x0fc0 && addr <= 0xfdf {
		// looking at this switch, I'm now thinking hotspots could be done
		// programmatically. for now though, we'll keep it like this.
		if addr == 0x0fc0 {
			cart.state.bank = 0
		} else if addr == 0x0fc1 {
			cart.state.bank = 1
		} else if addr == 0x0fc2 {
			cart.state.bank = 2
		} else if addr == 0x0fc3 {
			cart.state.bank = 3
		} else if addr == 0x0fc4 {
			cart.state.bank = 4
		} else if addr == 0x0fc5 {
			cart.state.bank = 5
		} else if addr == 0x0fc6 {
			cart.state.bank = 6
		} else if addr == 0x0fc7 {
			cart.state.bank = 7
		} else if addr == 0x0fc8 {
			cart.state.bank = 8
		} else if addr == 0x0fc9 {
			cart.state.bank = 9
		} else if addr == 0x0fca {
			cart.state.bank = 10
		} else if addr == 0x0fcb {
			cart.state.bank = 11
		} else if addr == 0x0fcc {
			cart.state.bank = 12
		} else if addr == 0x0fcd {
			cart.state.bank = 13
		} else if addr == 0x0fce {
			cart.state.bank = 14
		} else if addr == 0x0fcf {
			cart.state.bank = 15
		} else if addr == 0x0fd0 {
			cart.state.bank = 16
		} else if addr == 0x0fd1 {
			cart.state.bank = 17
		} else if addr == 0x0fd2 {
			cart.state.bank = 18
		} else if addr == 0x0fd3 {
			cart.state.bank = 19
		} else if addr == 0x0fd4 {
			cart.state.bank = 20
		} else if addr == 0x0fd5 {
			cart.state.bank = 21
		} else if addr == 0x0fd6 {
			cart.state.bank = 22
		} else if addr == 0x0fd7 {
			cart.state.bank = 23
		} else if addr == 0x0fd8 {
			cart.state.bank = 24
		} else if addr == 0x0fd9 {
			cart.state.bank = 25
		} else if addr == 0x0fda {
			cart.state.bank = 26
		} else if addr == 0x0fdb {
			cart.state.bank = 27
		} else if addr == 0x0fdc {
			cart.state.bank = 28
		} else if addr == 0x0fdd {
			cart.state.bank = 29
		} else if addr == 0x0fde {
			cart.state.bank = 30
		} else if addr == 0x0fdf {
			cart.state.bank = 31
		}
		return true
	}
	return false
}

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

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

// Patch implements the mapper.CartPatchable interface
func (cart *df) Patch(offset int, data uint8) error {
	if offset >= cart.bankSize*len(cart.banks) {
		return fmt.Errorf("DF: patch offset too high (%d)", offset)
	}

	bank := offset / cart.bankSize
	offset %= cart.bankSize
	cart.banks[bank][offset] = data
	return nil
}

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

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

// GetRAM implements the mapper.CartRAMBus interface.
func (cart *df) GetRAM() []mapper.CartRAM {
	r := make([]mapper.CartRAM, 1)
	r[0] = mapper.CartRAM{
		Label:  "df+RAM",
		Origin: 0x1080,
		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 *df) PutRAM(_ int, idx int, data uint8) {
	cart.state.ram[idx] = data
}

// IterateBank implements the mapper.CartMapper interface.
func (cart *df) CopyBanks() []mapper.BankContent {
	c := make([]mapper.BankContent, len(cart.banks))
	for b := 0; b < len(cart.banks); b++ {
		c[b] = mapper.BankContent{Number: b,
			Data:    cart.banks[b],
			Origins: []uint16{memorymap.OriginCart},
		}
	}
	return c
}

// ReadHotspots implements the mapper.CartHotspotsBus interface.
func (cart *df) ReadHotspots() map[uint16]mapper.CartHotspotInfo {
	return map[uint16]mapper.CartHotspotInfo{
		0x1fc0: {Symbol: "BANK0", Action: mapper.HotspotBankSwitch},
		0x1fc1: {Symbol: "BANK1", Action: mapper.HotspotBankSwitch},
		0x1fc2: {Symbol: "BANK2", Action: mapper.HotspotBankSwitch},
		0x1fc3: {Symbol: "BANK3", Action: mapper.HotspotBankSwitch},
		0x1fc4: {Symbol: "BANK4", Action: mapper.HotspotBankSwitch},
		0x1fc5: {Symbol: "BANK5", Action: mapper.HotspotBankSwitch},
		0x1fc6: {Symbol: "BANK6", Action: mapper.HotspotBankSwitch},
		0x1fc7: {Symbol: "BANK7", Action: mapper.HotspotBankSwitch},
		0x1fc8: {Symbol: "BANK8", Action: mapper.HotspotBankSwitch},
		0x1fc9: {Symbol: "BANK9", Action: mapper.HotspotBankSwitch},
		0x1fca: {Symbol: "BANK10", Action: mapper.HotspotBankSwitch},
		0x1fcb: {Symbol: "BANK11", Action: mapper.HotspotBankSwitch},
		0x1fcc: {Symbol: "BANK12", Action: mapper.HotspotBankSwitch},
		0x1fcd: {Symbol: "BANK13", Action: mapper.HotspotBankSwitch},
		0x1fce: {Symbol: "BANK14", Action: mapper.HotspotBankSwitch},
		0x1fcf: {Symbol: "BANK15", Action: mapper.HotspotBankSwitch},
		0x1fd0: {Symbol: "BANK16", Action: mapper.HotspotBankSwitch},
		0x1fd1: {Symbol: "BANK17", Action: mapper.HotspotBankSwitch},
		0x1fd2: {Symbol: "BANK18", Action: mapper.HotspotBankSwitch},
		0x1fd3: {Symbol: "BANK19", Action: mapper.HotspotBankSwitch},
		0x1fd4: {Symbol: "BANK20", Action: mapper.HotspotBankSwitch},
		0x1fd5: {Symbol: "BANK21", Action: mapper.HotspotBankSwitch},
		0x1fd6: {Symbol: "BANK22", Action: mapper.HotspotBankSwitch},
		0x1fd7: {Symbol: "BANK23", Action: mapper.HotspotBankSwitch},
		0x1fd8: {Symbol: "BANK24", Action: mapper.HotspotBankSwitch},
		0x1fd9: {Symbol: "BANK25", Action: mapper.HotspotBankSwitch},
		0x1fda: {Symbol: "BANK26", Action: mapper.HotspotBankSwitch},
		0x1fdb: {Symbol: "BANK27", Action: mapper.HotspotBankSwitch},
		0x1fdc: {Symbol: "BANK28", Action: mapper.HotspotBankSwitch},
		0x1fdd: {Symbol: "BANK29", Action: mapper.HotspotBankSwitch},
		0x1fde: {Symbol: "BANK30", Action: mapper.HotspotBankSwitch},
		0x1fdf: {Symbol: "BANK31", Action: mapper.HotspotBankSwitch},
	}
}

// WriteHotspots implements the mapper.CartHotspotsBus interface.
func (cart *df) WriteHotspots() map[uint16]mapper.CartHotspotInfo {
	return cart.ReadHotspots()
}

// rewindable state for the cartridge type.
type dfState struct {
	// identifies the currently selected bank
	bank int

	// df cartridges always have a RAM area
	ram []uint8
}

func newDfState() *dfState {
	const dfRAMsize = 256

	return &dfState{
		ram: make([]uint8, dfRAMsize),
	}
}

func (s *dfState) Snapshot() *dfState {
	n := *s
	n.ram = make([]uint8, len(s.ram))
	copy(n.ram, s.ram)
	return &n
}
