package dis

import (
	"fmt"
	"okemu/z80"
	"strings"
)

type Disassembler struct {
	pc   uint16
	core z80.MemIoRW
}

type Disassembly interface {
	Disassm(pc uint16) string
}

func NewDisassembler(core z80.MemIoRW) *Disassembler {
	d := Disassembler{
		pc:   0,
		core: core,
	}
	return &d
}

// opcode & 0x07
var operands = []string{"B", "C", "D", "E", "H", "L", "(HL)", "A"}
var aluOp = []string{"ADD A" + sep, "ADC A" + sep, "SUB ", "SBC A" + sep, "AND ", "XOR ", "OR ", "CP "}

const sep = ", "

func (d *Disassembler) jp(op, cond string) string {
	addr := d.getW()
	if cond != "" {
		cond += sep
	}
	return fmt.Sprintf("%s %s%s", op, cond, addr)
}

func (d *Disassembler) jr(op, cond string) string {
	addr := d.pc
	offset := d.getByte()
	if offset&0x80 != 0 {
		addr += 0xFF00 | uint16(offset)
	} else {
		addr += d.pc + uint16(offset)
	}
	if cond != "" {
		cond += sep
	}
	return fmt.Sprintf("%s %s0x%04X", op, cond, addr)
}

func (d *Disassembler) getByte() byte {
	b := d.core.MemRead(d.pc)
	d.pc++
	return b
}

func (d *Disassembler) Disassm(pc uint16) string {
	d.pc = pc
	result := fmt.Sprintf("  %04X ", d.pc)
	op := d.getByte()

	switch {
	// == 00:0F
	case op == 0x00:
		result += "NOP"
	case op == 0x01:
		result += "LD BC" + sep + d.getW()
	case op == 0x02:
		result += "LD (BC)" + sep + "A"
	case op == 0x03:
		result += "INC BC"
	case op == 0x04:
		result += "INC B"
	case op == 0x05:
		result += "DEC B"
	case op == 0x06:
		result += "LD B" + sep + d.getB()
	case op == 0x07:
		result += "RLCA"
	case op == 0x08:
		result += "EX AF, AF'"
	case op == 0x09:
		result += "ADD HL" + sep + "BC"
	case op == 0x0A:
		result += "LD A" + sep + "(BC)"
	case op == 0x0B:
		result += "DEC BC"
	case op == 0x0C:
		result += "INC C"
	case op == 0x0D:
		result += "DEC C"
	case op == 0x0E:
		result += "LD C" + sep + d.getB()
	case op == 0x0F:
		result += "RRCA"
	// 10:1F
	case op == 0x10:
		// DJNZ rel
		result += d.jr("DJNZ", "")
	case op == 0x11:
		result += "LD DE" + sep + d.getW()
	case op == 0x12:
		result += "LD (DE)" + sep + "A"
	case op == 0x13:
		result += "INC DE"
	case op == 0x14:
		result += "INC D"
	case op == 0x15:
		result += "DEC D"
	case op == 0x16:
		result += "LD D" + sep + d.getB()
	case op == 0x17:
		result += "RLA"
	case op == 0x18:
		result += d.jr("JR", "")
	case op == 0x19:
		result += "ADD HL" + sep + "DE"
	case op == 0x1A:
		result += "LD A" + sep + "(DE)"
	case op == 0x1B:
		result += "DEC DE"
	case op == 0x1C:
		result += "INC E"
	case op == 0x1D:
		result += "DEC E"
	case op == 0x1E:
		result += "LD E" + sep + d.getB()
	case op == 0x1F:
		result += "RRA"
	// == 20:2F
	case op == 0x20:
		result += d.jr("JR", "NZ")
	case op == 0x21:
		result += "LD HL" + sep + d.getW()
	case op == 0x22:
		// LD (nn),HL
		result += "LD (" + d.getW() + ")" + sep + "HL"
	case op == 0x23:
		result += "INC HL"
	case op == 0x24:
		result += "INC H"
	case op == 0x25:
		result += "DEC H"
	case op == 0x26:
		result += "LD H" + sep + d.getB()
	case op == 0x27:
		result += "DAA"
	case op == 0x28:
		result += d.jr("JR", "Z")
	case op == 0x29:
		result += "ADD HL" + sep + "HL"
	case op == 0x2A:
		result += "LD HL" + sep + "(" + d.getW() + ")"
	case op == 0x2B:
		result += "DEC HL"
	case op == 0x2C:
		result += "INC L"
	case op == 0x2D:
		result += "DEC L"
	case op == 0x2E:
		result += "LD L" + sep + d.getB()
	case op == 0x2F:
		result += "CPL"
	// == 30:3F
	case op == 0x30:
		result += d.jr("JR", "NC")
	case op == 0x31:
		result += "LD SP" + sep + d.getW()
	case op == 0x32:
		result += "LD (" + d.getW() + ")" + sep + "A"
	case op == 0x33:
		result += "INC SP"
	case op == 0x34:
		result += "INC (HL)"
	case op == 0x35:
		result += "DEC (HL)"
	case op == 0x36:
		result += "LD (HL)" + sep + d.getB()
	case op == 0x37:
		result += "SCF"
	case op == 0x38:
		result += d.jr("JR", "C")
	case op == 0x39:
		result += "ADD HL" + sep + "SP"
	case op == 0x3A:
		result += "LD A" + sep + "(" + d.getW() + ")"
	case op == 0x3B:
		result += "DEC SP"
	case op == 0x3C:
		result += "INC A"
	case op == 0x3D:
		result += "DEC A"
	case op == 0x3E:
		result += "LD A" + sep + d.getB()
	case op == 0x3F:
		result += "CCF"
	case op == 0x76:
		result += "HALT"
	case op >= 0x40 && op <= 0x7F:
		// LD op8, op8
		result += "LD " + operands[(op>>3)&0x07] + sep + operands[op&0x07]
	case op >= 0x80 && op <= 0xBF:
		// ALU op8
		result += aluOp[(op>>3)&0x07] + operands[op&0x07]
	case op == 0xc0:
		result += "RET NZ"
	case op == 0xc1:
		result += "POP BC"
	case op == 0xc2:
		result += d.jp("JP", "NZ")
	case op == 0xc3:
		result += d.jp("JP", "")
	case op == 0xc4:
		result += d.jp("CALL", "NZ")
	case op == 0xc5:
		result += "PUSH BC"
	case op == 0xc6:
		result += "ADD A" + sep + d.getB()
	case op == 0xc7 || op == 0xd7 || op == 0xe7 || op == 0xf7 || op == 0xcf || op == 0xdf || op == 0xef || op == 0xff:
		// RST nnH
		result += fmt.Sprintf("RST %d%dH", (op>>4)&3, (op&1)*8)
	case op == 0xc8:
		result += "RET Z"
	case op == 0xc9:
		result += "RET"
	case op == 0xca:
		result += d.jp("JP", "Z")
	case op == 0xcb:
		result += d.opocodeCB()
	case op == 0xcc:
		result += d.jp("CALL", "Z")
	case op == 0xcd:
		result += d.jp("CALL", "")
	case op == 0xce:
		result += "ADC A" + sep + d.getB()
	case op == 0xd0:
		result += "RET NC"
	case op == 0xd1:
		result += "POP DE"
	case op == 0xd2:
		result += d.jp("JP", "NC")
	case op == 0xd3:
		result += "OUT (" + d.getB() + ")" + sep + "A"
	case op == 0xd4:
		result += d.jp("CALL", "NC")
	case op == 0xd5:
		result += "PUSH DE"
	case op == 0xd6:
		result += "SUB " + d.getB()
	case op == 0xd8:
		result += "RET C"
	case op == 0xd9:
		result += "EXX"
	case op == 0xda:
		result += d.jp("JP", "C")
	case op == 0xdb:
		result += "IN A" + sep + " (" + d.getB() + ")"
	case op == 0xdc:
		result += d.jp("CALL", "C")
	case op == 0xdd:
		result += d.opocodeDD(op)
	case op == 0xde:
		result += "SBC A" + sep + d.getB()
	case op == 0xe0:
		result += "RET PO"
	case op == 0xe1:
		result += "POP HL"
	case op == 0xe2:
		result += d.jp("JP", "PO")
	case op == 0xe3:
		result += "EX (SP)" + sep + "HL"
	case op == 0xe4:
		result += d.jp("CALL", "PO")
	case op == 0xe5:
		result += "PUSH HL"
	case op == 0xe6:
		result += "AND " + d.getB()
	case op == 0xe8:
		result += "RET PE"
	case op == 0xe9:
		result += "JP (HL)"
	case op == 0xea:
		result += d.jp("JP", "PE")
	case op == 0xeb:
		result += "EX DE" + sep + "HL"
	case op == 0xec:
		result += d.jp("CALL", "PE")
	case op == 0xed:
		result += d.opocodeED()
	case op == 0xee:
		result += "XOR " + d.getB()
	case op == 0xf0:
		result += "RET P"
	case op == 0xf1:
		result += "POP AF"
	case op == 0xf2:
		result += d.jp("JP", "P")
	case op == 0xf3:
		result += "DI"
	case op == 0xf4:
		result += d.jp("CALL", "P")
	case op == 0xf5:
		result += "PUSH AF"
	case op == 0xf6:
		result += "OR " + d.getB()
	case op == 0xf8:
		result += "RET M"
	case op == 0xf9:
		result += "LD SP" + sep + "HL"
	case op == 0xfa:
		result += d.jp("JP", "M")
	case op == 0xfb:
		result += "EI"
	case op == 0xfc:
		result += d.jp("CALL", "M")
	case op == 0xfd:
		result += d.opocodeDD(op)
	case op == 0xfe:
		result += "CP " + d.getB()
	default:
		// All unknown as DB
		result += fmt.Sprintf("DB 0x%02X", op)
	}
	return result
}

func (d *Disassembler) getW() string {
	lo := d.core.MemRead(d.pc)
	d.pc++
	hi := d.core.MemRead(d.pc)
	d.pc++
	return fmt.Sprintf("0x%02X%02X", hi, lo)
}

func (d *Disassembler) getB() string {
	lo := d.core.MemRead(d.pc)
	d.pc++
	return fmt.Sprintf("0x%02X", lo)
}

func (d *Disassembler) getRel() string {
	offset := d.core.MemRead(d.pc)
	var sign string
	if int8(offset) < 0 {
		sign = "-"
	} else {
		sign = "+"
	}
	return sign + fmt.Sprintf("0x%02X", offset&0x7F)
}

var shiftOps = []string{"RLC", "RRC", "RL", "RR", "SLA", "SRA", "SLL", "SRL"}
var bitOps = []string{"BIT", "RES", "SET"}

// opocodeCB disassemble Z80 Opcodes, with CB first byte
func (d *Disassembler) opocodeCB() string {
	op := ""
	opcode := d.getByte()
	if opcode <= 0x3F {
		op = shiftOps[opcode>>3&0x07] + operands[opcode&0x7]
	} else {
		op = shiftOps[(opcode>>6&0x03)-1] + operands[opcode&0x7]
	}
	return op
}

func (d *Disassembler) opocodeDD(op byte) string {
	opcode := d.getByte()
	result := ""
	switch opcode {
	case 0x09:
		result = "ADD ii" + sep + "BC"
	case 0x19:
		result = "ADD ii" + sep + "DE"
	case 0x21:
		result = "LD ii" + sep + d.getW()
	case 0x22:
		result = "LD (" + d.getW() + ")" + sep + "ii"
	case 0x23:
		result = "INC ii"
	case 0x24:
		result = "INC IXH"
	case 0x25:
		result = "DEC IXH"
	case 0x26:
		result = "LD IXH" + sep + "n"
	case 0x29:
		result = "ADD ii" + sep + "ii"
	case 0x2A:
		result = "LD ii" + sep + "(" + d.getW() + ")"
	case 0x2B:
		result = "DEC ii"
	case 0x34:
		result = "INC (ii" + d.getRel() + ")"
	case 0x35:
		result = "DEC (ii" + d.getRel() + ")"
	case 0x36:
		result = "LD (ii" + d.getRel() + ")" + sep + "n"
	case 0x39:
		result = "ADD ii" + sep + "SP"
	case 0x46:
		result = "LD B" + sep + "(ii" + d.getRel() + ")"
	case 0x4E:
		result = "LD C" + sep + "(ii" + d.getRel() + ")"
	case 0x56:
		result = "LD D" + sep + "(ii" + d.getRel() + ")"
	case 0x5E:
		result = "LD E" + sep + "(ii" + d.getRel() + ")"
	case 0x66:
		result = "LD H" + sep + "(ii" + d.getRel() + ")"
	case 0x6E:
		result = "LD L" + sep + "(ii" + d.getRel() + ")"
	case 0x70:
		result = "LD (ii" + d.getRel() + ")" + sep + "B"
	case 0x71:
		result = "LD (ii" + d.getRel() + ")" + sep + "C"
	case 0x72:
		result = "LD (ii" + d.getRel() + ")" + sep + "D"
	case 0x73:
		result = "LD (ii" + d.getRel() + ")" + sep + "E"
	case 0x74:
		result = "LD (ii" + d.getRel() + ")" + sep + "H"
	case 0x75:
		result = "LD (ii" + d.getRel() + ")" + sep + "L"
	case 0x77:
		result = "LD (ii" + d.getRel() + ")" + sep + "A"
	case 0x7E:
		result = "LD A" + sep + "(ii" + d.getRel() + ")"
	case 0x86:
		result = "ADD A" + sep + "(ii" + d.getRel() + ")"
	case 0x8E:
		result = "ADC A" + sep + "(ii" + d.getRel() + ")"
	case 0x96:
		result = "SUB (ii" + d.getRel() + ")"
	case 0x9E:
		result = "SBC A" + sep + "(ii" + d.getRel() + ")"
	case 0xA6:
		result = "AND (ii" + d.getRel() + ")"
	case 0xAE:
		result = "XOR (ii" + d.getRel() + ")"
	case 0xB6:
		result = "OR (ii" + d.getRel() + ")"
	case 0xBE:
		result = "CP (ii" + d.getRel() + ")"
	case 0xCB:
		result = d.opocodeDDCB(op, opcode)
	case 0xE1:
		result = "POP ii"
	case 0xE3:
		result = "EX (SP)" + sep + "ii"
	case 0xE5:
		result = "PUSH ii"
	case 0xE9:
		result = "JP (ii)"
	case 0xF9:
		result = "LD SP" + sep + "ii"
	default:
		return fmt.Sprintf("DB 0x%02X, 0x%02X", op, opcode)
	}

	reg := "IX"
	if op == 0xFD {
		reg = "IY"
	}

	return strings.ReplaceAll(result, "ii", reg)
}

func (d *Disassembler) opocodeDDCB(op1 byte, op2 byte) string {
	opcode := d.getByte()
	result := ""
	switch opcode {
	case 0x06:
		result = "RLC (ii" + d.getRel() + ")"
	case 0x0E:
		result = "RRC (ii" + d.getRel() + ")"
	case 0x16:
		result = "RL (ii" + d.getRel() + ")"
	case 0x1E:
		result = "RR (ii" + d.getRel() + ")"
	case 0x26:
		result = "SLA (ii" + d.getRel() + ")"
	case 0x2E:
		result = "SRA (ii" + d.getRel() + ")"
	case 0x3E:
		result = "SRL (ii" + d.getRel() + ")"
	case 0x46:
		result = "BIT 0" + sep + "(ii" + d.getRel() + ")"
	case 0x4E:
		result = "BIT 1" + sep + "(ii" + d.getRel() + ")"
	case 0x56:
		result = "BIT 2" + sep + "(ii" + d.getRel() + ")"
	case 0x5E:
		result = "BIT 3" + sep + "(ii" + d.getRel() + ")"
	case 0x66:
		result = "BIT 4" + sep + "(ii" + d.getRel() + ")"
	case 0x6E:
		result = "BIT 5" + sep + "(ii" + d.getRel() + ")"
	case 0x76:
		result = "BIT 6" + sep + "(ii" + d.getRel() + ")"
	case 0x7E:
		result = "BIT 7" + sep + "(ii" + d.getRel() + ")"
	case 0x86:
		result = "RES 0" + sep + "(ii" + d.getRel() + ")"
	case 0x8E:
		result = "RES 1" + sep + "(ii" + d.getRel() + ")"
	case 0x96:
		result = "RES 2" + sep + "(ii" + d.getRel() + ")"
	case 0x9E:
		result = "RES 3" + sep + "(ii" + d.getRel() + ")"
	case 0xA6:
		result = "RES 4" + sep + "(ii" + d.getRel() + ")"
	case 0xAE:
		result = "RES 5" + sep + "(ii" + d.getRel() + ")"
	case 0xB6:
		result = "RES 6" + sep + "(ii" + d.getRel() + ")"
	case 0xBE:
		result = "RES 7" + sep + "(ii" + d.getRel() + ")"
	case 0xC6:
		result = "SET 0" + sep + "(ii" + d.getRel() + ")"
	case 0xCE:
		result = "SET 1" + sep + "(ii" + d.getRel() + ")"
	case 0xD6:
		result = "SET 2" + sep + "(ii" + d.getRel() + ")"
	case 0xDE:
		result = "SET 3" + sep + "(ii" + d.getRel() + ")"
	case 0xE6:
		result = "SET 4" + sep + "(ii" + d.getRel() + ")"
	case 0xEE:
		result = "SET 5" + sep + "(ii" + d.getRel() + ")"
	case 0xF6:
		result = "SET 6" + sep + "(ii" + d.getRel() + ")"
	case 0xFE:
		result = "SET 7" + sep + "(ii" + d.getRel() + ")"
	default:
		result = fmt.Sprintf("DB 0x%02X, 0x%02X, 0x%02X", op1, op2, opcode)
	}
	return result
}

func (d *Disassembler) opocodeED() string {
	opcode := d.getByte()
	result := ""
	switch opcode {
	case 0x40:
		result = "IN B" + sep + "(C)"
	case 0x41:
		result = "OUT (C)" + sep + "B"
	case 0x42:
		result = "SBC HL" + sep + "BC"
	case 0x43:
		result = "LD (" + d.getW() + ")" + sep + "BC"
	case 0x44, 0x4C, 0x54, 0x5C, 0x64, 0x6C, 0x74, 0x7C:
		result = "NEG"
	case 0x45, 0x55, 0x5D, 0x65, 0x6D, 0x75, 0x7D:
		result = "RETN"
	case 0x46, 0x4E, 0x66, 0x6E:
		result = "IM 0"
	case 0x47:
		result = "LD I" + sep + "A"
	case 0x48:
		result = "IN C" + sep + "(C)"
	case 0x49:
		result = "OUT (C)" + sep + "C"
	case 0x4A:
		result = "ADC HL" + sep + "BC"
	case 0x4B:
		result = "LD BC" + sep + "(" + d.getW() + ")"
	case 0x4D:
		result = "REТI"
	case 0x4F:
		result = "LD R" + sep + "A"
	case 0x50:
		result = "IN D" + sep + "(C)"
	case 0x51:
		result = "OUT (C)" + sep + "D"
	case 0x52:
		result = "SBC HL" + sep + "DE"
	case 0x53:
		result = "LD (nn)" + sep + "DE"
	case 0x56, 0x76:
		result = "IM 1"
	case 0x57:
		result = "LD A" + sep + "I"
	case 0x58:
		result = "IN E" + sep + "(C)"
	case 0x59:
		result = "OUT (C)" + sep + "E"
	case 0x5A:
		result = "ADC HL" + sep + "DE"
	case 0x5B:
		result = "LD DE" + sep + "(" + d.getW() + ")"
	case 0x5E, 0x7E:
		result = "IM 2"
	case 0x5F:
		result = "LD A" + sep + "R"
	case 0x60:
		result = "IN H" + sep + "(C)"
	case 0x61:
		result = "OUT (C)" + sep + "H"
	case 0x62:
		result = "SBC HL" + sep + "HL"
	case 0x63:
		result = "LD (nn)" + sep + "HL"
	case 0x67:
		result = "RRD"
	case 0x68:
		result = "IN L" + sep + " (C)"
	case 0x69:
		result = "OUT (C)" + sep + "L"
	case 0x6A:
		result = "ADC HL" + sep + " HL"
	case 0x6B:
		result = "LD HL" + sep + " (nn)"
	case 0x6F:
		result = "RLD"
	case 0x70:
		result = "INF"
	case 0x71:
		result = "OUT (C)" + sep + " 0"
	case 0x72:
		result = "SBC HL" + sep + "SP"
	case 0x73:
		result = "LD (nn)" + sep + "SP"
	case 0x78:
		result = "IN A" + sep + "(C)"
	case 0x79:
		result = "OUT (C)" + sep + "A"
	case 0x7A:
		result = "ADC HL" + sep + "SP"
	case 0x7B:
		result = "LD SP" + sep + "(" + d.getW() + ")"
	case 0xA0:
		result = "LDI"
	case 0xA1:
		result = "CPI"
	case 0xA2:
		result = "INI"
	case 0xA3:
		result = "OUTI"
	case 0xA8:
		result = "LDD"
	case 0xA9:
		result = "CPD"
	case 0xAA:
		result = "IND"
	case 0xAB:
		result = "OUTD"
	case 0xB0:
		result = "LDIR"
	case 0xB1:
		result = "CPIR"
	case 0xB2:
		result = "INIR"
	case 0xB3:
		result = "OTIR"
	case 0xB8:
		result = "LDDR"
	case 0xB9:
		result = "CPDR"
	case 0xBA:
		result = "INDR"
	case 0xBB:
		result = "OTDR"
	default:
		result = fmt.Sprintf("DB 0xED, 0x%02X", opcode)
	}
	return result
}
