// 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 debugger

import (
	"fmt"
	"strings"

	"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
	"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
)

// targetValue represents the underlying value of the target. for example in
// the case of the CPU program counter target, the underlying type is a uint16.
//
// !!TODO: candidate for generic / comparable type
type targetValue interface{}

type target struct {
	// a label for the label
	label string

	// the current value of the target. note this may not be the same value as the
	// underlying target. for example, target PC will return zero if there is a
	// coprocessor running.
	value func() targetValue

	// the value returned by the value field/function can be formatted for
	// presentation purposes with formatValue()
	format string

	// some targets should only be checked on an instruction boundary
	instructionBoundary bool

	// this target will always break in playmode almost immediately. we use
	// this flag to decide whether to allow the debugger to switch to playmode
	notInPlaymode bool
}

// returns value() formated by the format string. accepts a target value as an
// argument so the format string can be used on any valid targetValue.
func (trg target) stringValue(v targetValue) string {
	if trg.format == "" {
		return fmt.Sprintf("%v", v)
	}
	return fmt.Sprintf(trg.format, v)
}

// parseTarget interprets the next token and returns a target if it is
// recognised. returns error if it is not.
func parseTarget(dbg *Debugger, tokens *commandline.Tokens) (*target, error) {
	var trg *target

	keyword, present := tokens.Get()
	if present {
		keyword = strings.ToUpper(keyword)
		switch keyword {
		// cpu registers
		case "PC":
			trg = &target{
				label: "PC",
				value: func() targetValue {
					// check that there is no coprocessor running - the ARM
					// class of coprocessors cause the 6507 to run NOP
					// instructions, which causes the PC to advance. this means
					// that a PC break can be triggered when the user probably
					// doesn't want it to be.
					bank := dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address())
					if bank.ExecutingCoprocessor {
						// we return zero. it's highly unlikely a genuine BREAK PC 0
						// has been requested but even so, this isn't great design.
						// better to return two values perhaps, the second value saying
						// that the target is out of action.
						return 0
					}

					// for breakpoints it is important that the breakpoint
					// value be normalised through MapAddress() too
					ai := dbg.dbgmem.GetAddressInfo(dbg.vcs.CPU.PC.Address(), true)
					return int(ai.MappedAddress)
				},
				format:              "%#04x",
				instructionBoundary: true,
			}

		case "A":
			trg = &target{
				label: "A",
				value: func() targetValue {
					return int(dbg.vcs.CPU.A.Value())
				},
				format:              "%#02x",
				instructionBoundary: true,
			}

		case "X":
			trg = &target{
				label: "X",
				value: func() targetValue {
					return int(dbg.vcs.CPU.X.Value())
				},
				format:              "%#02x",
				instructionBoundary: true,
			}

		case "Y":
			trg = &target{
				label: "Y",
				value: func() targetValue {
					return int(dbg.vcs.CPU.Y.Value())
				},
				format:              "%#02x",
				instructionBoundary: true,
			}

		case "SP":
			trg = &target{
				label: "SP",
				value: func() targetValue {
					return int(dbg.vcs.CPU.SP.Value())
				},
				format:              "%#02x",
				instructionBoundary: true,
			}

		case "RDY":
			trg = &target{
				label: "RDY",
				value: func() targetValue {
					return bool(dbg.vcs.CPU.RdyFlg)
				},
				format:              "%v",
				instructionBoundary: true,
			}

		case "NONEXE":
			trg = &target{
				label: "NONEXE",
				value: func() targetValue {
					pc := dbg.vcs.CPU.PC.Address()
					_, area := memorymap.MapAddress(pc, true)
					return area == memorymap.RIOT || area == memorymap.TIA
				},
				format:              "%v",
				instructionBoundary: true,
			}

		// tv state
		case "FRAMENUM", "FRAME", "FR":
			trg = &target{
				label: "Frame",
				value: func() targetValue {
					return dbg.vcs.TV.GetCoords().Frame
				},
			}

		case "SCANLINE", "SL":
			trg = &target{
				label: "Scanline",
				value: func() targetValue {
					return dbg.vcs.TV.GetCoords().Scanline
				},

				// specifying scanline to be notInPlaymode was considered but
				// this will occasionaly not be true. for example, a scanline
				// value that is beyond the extent of the generated TV frame
			}

		case "CLOCK", "CL":
			trg = &target{
				label: "Clock",
				value: func() targetValue {
					return dbg.vcs.TV.GetCoords().Clock
				},

				// it is impossible to measure the clock value accurately in
				// playmode because the state of the machine is only checked
				// after every CPU instruction
				//
				// another reason not to allow playmode when this halt target
				// is being used, is that the emulation will almost immediately
				// halt - the clock value *will* be reached within 228 ticks of
				// the emulation
				notInPlaymode: true,
			}

		case "JITTER":
			trg = &target{
				label: "JITTER",
				value: func() targetValue {
					return dbg.vcs.TV.GetFrameInfo().Jitter
				},
			}

		case "BANK":
			trg = bankTarget(dbg)

		// cpu instruction targeting was originally added as an experiment, to
		// help investigate a bug in the emulation. I don't think it's much use
		// but it was an instructive exercise and may come in useful one day.
		case "RESULT", "RES":
			subkey, present := tokens.Get()
			if present {
				subkey = strings.ToUpper(subkey)
				switch subkey {
				case "OPERATOR", "OP":
					trg = &target{
						label: "Operator",
						value: func() targetValue {
							if !dbg.vcs.CPU.LastResult.Final || dbg.vcs.CPU.LastResult.Defn == nil {
								return ""
							}

							// for simplicity we treat strings as uppercase in
							// the breakpoint/trap list. we should therefore
							// convert the return string to uppercase
							return strings.ToUpper(dbg.vcs.CPU.LastResult.Defn.Operator.String())
						},
						instructionBoundary: true,
					}

				case "ADDRESSMODE", "AM":
					trg = &target{
						label: "AddressMode",
						value: func() targetValue {
							if !dbg.vcs.CPU.LastResult.Final || dbg.vcs.CPU.LastResult.Defn == nil {
								return ""
							}
							return int(dbg.vcs.CPU.LastResult.Defn.AddressingMode)
						},
						instructionBoundary: true,
					}

				case "EFFECT", "EFF":
					trg = &target{
						label: "Instruction Effect",
						value: func() targetValue {
							if !dbg.vcs.CPU.LastResult.Final || dbg.vcs.CPU.LastResult.Defn == nil {
								return -1
							}
							return int(dbg.vcs.CPU.LastResult.Defn.Effect)
						},
						instructionBoundary: true,
					}

				case "PAGEFAULT", "PAGE":
					trg = &target{
						label: "PageFault",
						value: func() targetValue {
							return dbg.vcs.CPU.LastResult.PageFault
						},
						instructionBoundary: true,
					}

				case "BUG":
					trg = &target{
						label: "CPU Bug",
						value: func() targetValue {
							s := dbg.vcs.CPU.LastResult.CPUBug
							if s == "" {
								return "ok"
							}
							return s
						},
						instructionBoundary: true,
					}

				default:
					return nil, fmt.Errorf("invalid target: %s %s", keyword, subkey)
				}
			} else {
				return nil, fmt.Errorf("invalid target: %s", keyword)
			}

		default:
			return nil, fmt.Errorf("invalid target: %s", keyword)
		}
	}

	return trg, nil
}

// a bank target is generated automatically by the breakpoints system and also
// explicitly by parseTarget()
func bankTarget(dbg *Debugger) *target {
	return &target{
		label: "Bank",
		value: func() targetValue {
			return dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address()).Number
		},
		instructionBoundary: true,
	}
}
