package core6502

import (
	"encoding/binary"
	"fmt"
	"io"
)

// https://www.masswerk.at/6502/6502_instruction_set.html
// http://www.emulator101.com/reference/6502-reference.html
// https://www.csh.rit.edu/~moffitt/docs/6502.html#FLAGS
// https://ia800509.us.archive.org/18/items/Programming_the_6502/Programming_the_6502.pdf

const (
	maxInstructionSize = 3
)

// State represents the state of the simulated device
type State struct {
	reg       registers
	mem       Memory
	cycles    uint64
	opcodes   *[256]opcode
	trace     bool
	lineCache []uint8
	// We cache the allocation of a line to avoid a malloc per instruction. To be used only
	// by ExecuteInstruction(). 2x speedup on the emulation!!
}

const (
	vectorNMI   uint16 = 0xfffa
	vectorReset uint16 = 0xfffc
	vectorBreak uint16 = 0xfffe
)

type opcode struct {
	name        string
	bytes       uint16
	cycles      int
	addressMode int
	action      opFunc
}

type opFunc func(s *State, line []uint8, opcode opcode)

func (s *State) executeLine(line []uint8) {
	opcode := s.opcodes[line[0]]
	if opcode.cycles == 0 {
		panic(fmt.Sprintf("Unknown opcode 0x%02x\n", line[0]))
	}
	opcode.action(s, line, opcode)
}

// ExecuteInstruction transforms the state given after a single instruction is executed.
func (s *State) ExecuteInstruction() {
	pc := s.reg.getPC()
	opcodeID := s.mem.PeekCode(pc)
	opcode := s.opcodes[opcodeID]

	if opcode.cycles == 0 {
		panic(fmt.Sprintf("Unknown opcode 0x%02x\n", opcodeID))
	}

	if s.lineCache == nil {
		s.lineCache = make([]uint8, maxInstructionSize)
	}
	for i := uint16(0); i < opcode.bytes; i++ {
		s.lineCache[i] = s.mem.PeekCode(pc)
		pc++
	}
	s.reg.setPC(pc)

	if s.trace {
		//fmt.Printf("%#04x %#02x\n", pc-opcode.bytes, opcodeID)
		fmt.Printf("%#04x %-13s: ", pc-opcode.bytes, lineString(s.lineCache, opcode))
	}
	opcode.action(s, s.lineCache, opcode)
	s.cycles += uint64(opcode.cycles)
	if s.trace {
		fmt.Printf("%v, [%02x]\n", s.reg, s.lineCache[0:opcode.bytes])
	}
}

// Reset resets the processor. Moves the program counter to the vector in 0cfffc.
func (s *State) Reset() {
	startAddress := getWord(s.mem, vectorReset)
	s.cycles += 6
	s.reg.setPC(startAddress)
}

// GetCycles returns the count of CPU cycles since last reset.
func (s *State) GetCycles() uint64 {
	return s.cycles
}

// SetTrace activates tracing of the cpu execution
func (s *State) SetTrace(trace bool) {
	s.trace = trace
}

// GetTrace gets trhe tracing state of the cpu execution
func (s *State) GetTrace() bool {
	return s.trace
}

// SetMemory changes the memory provider
func (s *State) SetMemory(mem Memory) {
	s.mem = mem
}

// GetPCAndSP returns the current program counter and stack pointer. Used to trace MLI calls
func (s *State) GetPCAndSP() (uint16, uint8) {
	return s.reg.getPC(), s.reg.getSP()
}

// GetCarryAndAcc returns the value of te carry flag and the accumulator. Used to trace MLI calls
func (s *State) GetCarryAndAcc() (bool, uint8) {
	return s.reg.getFlag(flagC), s.reg.getA()
}

// Save saves the CPU state (registers and cycle counter)
func (s *State) Save(w io.Writer) error {
	err := binary.Write(w, binary.BigEndian, s.cycles)
	if err != nil {
		return err
	}
	binary.Write(w, binary.BigEndian, s.reg.data)
	if err != nil {
		return err
	}
	return nil
}

// Load loads the CPU state (registers and cycle counter)
func (s *State) Load(r io.Reader) error {
	err := binary.Read(r, binary.BigEndian, &s.cycles)
	if err != nil {
		return err
	}
	err = binary.Read(r, binary.BigEndian, &s.reg.data)
	if err != nil {
		return err
	}
	return nil
}