//
//  Z80MemptrTests.swift
//  Clock Signal
//
//  Created by Thomas Harte on 21/07/2017.
//  Copyright 2017 Thomas Harte. All rights reserved.
//

import XCTest

class Z80MemptrTester: XCTestCase {

	let machine = CSTestMachineZ80()

	private func test(program : [UInt8], initialValue : UInt16) -> UInt16 {
		// Create a machine and install the supplied program at address 0, setting the PC to run from there
		machine.setValue(0x0000, for: .programCounter)
		machine.setData(Data(_: program), atAddress: 0x0000)

		// Set the initial value of memptr, run for the requested number of cycles,
		// return the new value
		machine.setValue(initialValue, for: .memPtr)
		machine.runForInstruction()
		return machine.value(for: .memPtr)
	}

	private func testPage(_ prefix: UInt8, exclusions: [Int]) {
		for opcode in 0 ..< 256 {
			if exclusions.contains(opcode) {
				continue
			}

			var program: [UInt8] = [
				(prefix != 0) ? prefix : UInt8(opcode), UInt8(opcode), 0, 0
			]

			let argumentPosition = (prefix != 0) ? 2 : 1
			for _ in 0 ..< 10 {
				let random = arc4random_uniform(65536)
				program[argumentPosition + 0] = UInt8(random & 0x00ff)
				program[argumentPosition + 1] = UInt8(random >> 8)

				let expectedResult = UInt16(arc4random_uniform(65536))

				let result = test(program: program, initialValue: expectedResult)
				XCTAssertEqual(result, expectedResult, "Failed for opcode \(String(opcode, radix:16))")

				// One failure per opcode will do.
				if result != expectedResult {
					break
				}
			}
		}
	}

	private func insert16(program: inout [UInt8], address: Int, offset: size_t) {
		program[offset] = UInt8(address & 0x00ff)
		program[offset + 1] = UInt8(address >> 8)
	}

	/// Tests that everything not listed in the documentation has no effect upon MEMPTR.
	func testStandardPageOthers() {
		testPage(0, exclusions: [
			0x02,		// LD (BC), A
			0x09,		// ADD HL, BC
			0x0a,		// LD A, (BC)
			0x10,		// DJNZ
			0x12,		// LD (DE), A
			0x18,		// JR
			0x19,		// ADD HL, DE
			0x1a,		// LD A, (DE)
			0x20,		// JR NZ
			0x22,		// LD (nn), HL
			0x28,		// JR Z
			0x29,		// ADD HL, HL
			0x2a,		// LD HL, (nn)
			0x30,		// JR NC
			0x32,		// LD (nn), A
			0x38,		// JR C
			0x39,		// ADD HL, SP
			0x3a,		// LD A, (nn)

			0xcb,		// CB page
			0xdd,		// DD page
			0xed,		// ED page
			0xfd,		// FD page
		])
	}

	func testEDPageOthers() {
		testPage(0xed, exclusions: [
			0x40,		// IN B, (C)
			0x41,		// OUT (C), B
			0x42,		// SBC HL, BC
			0x43,		// LD (nn), HL
			0x45,		// RETN				(??)
			0x48,		// IN C, (C)
			0x49,		// OUT (C), C
			0x4a,		// ADC HL, BC
			0x4b,		// LD BC, (nn)
			0x4d,		// RETI
			0x50,		// IN D, (C)
			0x51,		// OUT (C), D
			0x52,		// SBC HL, DE
			0x53,		// LD (nn), DE
			0x55,		// RETN				(??)
			0x58,		// IN E, (C)
			0x59,		// OUT (C), E
			0x5a,		// ADC HL, DE
			0x5b,		// LD DE, (nn)
			0x5d,		// RETN				(??)
			0x60,		// IN H, (C)
			0x61,		// OUT (C), H
			0x62,		// SBC HL, HL
			0x63,		// LD (nn), HL
			0x65,		// RETN				(??)
			0x67,		// RRD
			0x68,		// IN L, (C)
			0x69,		// OUT (C), L
			0x6a,		// ADC HL, HL
			0x6b,		// LD HL, (nn)
			0x6d,		// RETN				(??)
			0x6f,		// RLD
			0x70,		// IN (C)
			0x71,		// OUT (C), 0
			0x72,		// SBC HL, SP
			0x73,		// LD (nn), SP
			0x75,		// RETN				(??)
			0x78,		// IN A, (C)
			0x79,		// OUT (C), A
			0x7a,		// ADC HL, SP
			0x7b,		// LD SP, (nn)
			0x7d,		// RETN				(??)
			0xa1,		// CPI
			0xa2,		// INI
			0xa3,		// OUTI
			0xa9,		// CPD
			0xaa,		// IND
			0xab,		// OUTD
			0xb0,		// LDIR
			0xb1,		// CPIR
			0xb2,		// INIR
			0xb3,		// OUIR
			0xb8,		// LDDR
			0xb9,		// CPDR
			0xba,		// INDR
			0xbb,		// OTDR
		])
//		testPage(0xcb, exclusions: [])
//		testPage(0xdd, exclusions: [])
//		testPage(0xfd, exclusions: [])
	}

	/*
		Re: comments below:
		All the CPU chips tested give the same results except KP1858BM1 and T34BM1 slices noted as "BM1".
	*/

	// LD A, (addr)
	func testLDAnn() {
		// MEMPTR = addr+1
		var program: [UInt8] = [
			0x3a, 0x00, 0x00
		]
		for addr in 0 ..< 65536 {
			program[1] = UInt8(addr & 0x00ff)
			program[2] = UInt8(addr >> 8)
			let expectedResult = UInt16((addr + 1) & 0xffff)

			let result = test(program: program, initialValue: 0xffff)
			XCTAssertEqual(result, expectedResult)
		}
	}

	// LD (bc/de),A, and LD (nn),A
	func testLDrpA() {
		// MEMPTR_low = (addr + 1) & #FF,  MEMPTR_hi = A
		// Note for *BM1: MEMPTR_low = (addr + 1) & #FF,  MEMPTR_hi = 0
		let bcProgram: [UInt8] = [
			0x02
		]
		let deProgram: [UInt8] = [
			0x12
		]
		var nnProgram: [UInt8] = [
			0x32, 0x00, 0x00
		]

		for addr in 0 ..< 256 {
			machine.setValue(UInt16(addr), for: .BC)
			machine.setValue(UInt16(addr), for: .DE)
			insert16(program: &nnProgram, address: addr, offset: 1)

			for a in 0 ..< 256 {
				machine.setValue(UInt16(a), for: .A)

				let expectedResult = UInt16(((addr + 1) & 0xff) + (a << 8))

				let bcResult = test(program: bcProgram, initialValue: 0xffff)
				let deResult = test(program: deProgram, initialValue: 0xffff)
				let nnResult = test(program: nnProgram, initialValue: 0xffff)

				XCTAssertEqual(bcResult, expectedResult)
				XCTAssertEqual(deResult, expectedResult)
				XCTAssertEqual(nnResult, expectedResult)
			}
		}
	}

	// LD A, (rp)
	func testLDArp() {
		// MEMPTR = rp+1
		let bcProgram: [UInt8] = [
			0x0a
		]
		let deProgram: [UInt8] = [
			0x1a
		]
		for addr in 0 ..< 65536 {
			machine.setValue(UInt16(addr), for: .BC)
			machine.setValue(UInt16(addr), for: .DE)

			let expectedResult = UInt16((addr + 1) & 0xffff)

			let bcResult = test(program: bcProgram, initialValue: 0xffff)
			let deResult = test(program: deProgram, initialValue: 0xffff)

			XCTAssertEqual(bcResult, expectedResult)
			XCTAssertEqual(deResult, expectedResult)
		}
	}

	// LD (addr), rp
	func testLDnnrp() {
		// MEMPTR = addr + 1
		var ldnnhlBaseProgram: [UInt8] = [
			0x22, 0x00, 0x00
		]
		var ldnnbcEDProgram: [UInt8] = [
			0xed, 0x43, 0x00, 0x00
		]
		var ldnndeEDProgram: [UInt8] = [
			0xed, 0x53, 0x00, 0x00
		]
		var ldnnhlEDProgram: [UInt8] = [
			0xed, 0x63, 0x00, 0x00
		]
		var ldnnspEDProgram: [UInt8] = [
			0xed, 0x73, 0x00, 0x00
		]

		for addr in 0 ..< 65536 {
			insert16(program: &ldnnhlBaseProgram, address: addr, offset: 1)
			insert16(program: &ldnnbcEDProgram, address: addr, offset: 2)
			insert16(program: &ldnndeEDProgram, address: addr, offset: 2)
			insert16(program: &ldnnhlEDProgram, address: addr, offset: 2)
			insert16(program: &ldnnspEDProgram, address: addr, offset: 2)

			let expectedResult = UInt16((addr + 1) & 0xffff)

			XCTAssertEqual(test(program: ldnnhlBaseProgram, initialValue: expectedResult ^ 1), expectedResult)
			XCTAssertEqual(test(program: ldnnbcEDProgram, initialValue: expectedResult ^ 1), expectedResult)
			XCTAssertEqual(test(program: ldnndeEDProgram, initialValue: expectedResult ^ 1), expectedResult)
			XCTAssertEqual(test(program: ldnnhlEDProgram, initialValue: expectedResult ^ 1), expectedResult)
			XCTAssertEqual(test(program: ldnnspEDProgram, initialValue: expectedResult ^ 1), expectedResult)
		}
	}

	// LD rp, (addr)
	func testLDrpnn() {
		// MEMPTR = addr+1
		var hlBaseProgram: [UInt8] = [
			0x22, 0x00, 0x00
		]

		var bcEDProgram: [UInt8] = [
			0xed, 0x43, 0x00, 0x00
		]
		var deEDProgram: [UInt8] = [
			0xed, 0x53, 0x00, 0x00
		]
		var hlEDProgram: [UInt8] = [
			0xed, 0x63, 0x00, 0x00
		]
		var spEDProgram: [UInt8] = [
			0xed, 0x73, 0x00, 0x00
		]

		var ixProgram: [UInt8] = [
			0xdd, 0x22, 0x00, 0x00
		]
		var iyProgram: [UInt8] = [
			0xfd, 0x22, 0x00, 0x00
		]

		for addr in 0 ..< 65536 {
			hlBaseProgram[1] = UInt8(addr & 0x00ff)
			hlBaseProgram[2] = UInt8(addr >> 8)

			bcEDProgram[2] = UInt8(addr & 0x00ff)
			bcEDProgram[3] = UInt8(addr >> 8)
			deEDProgram[2] = UInt8(addr & 0x00ff)
			deEDProgram[3] = UInt8(addr >> 8)
			hlEDProgram[2] = UInt8(addr & 0x00ff)
			hlEDProgram[3] = UInt8(addr >> 8)
			spEDProgram[2] = UInt8(addr & 0x00ff)
			spEDProgram[3] = UInt8(addr >> 8)

			ixProgram[2] = UInt8(addr & 0x00ff)
			ixProgram[3] = UInt8(addr >> 8)
			iyProgram[2] = UInt8(addr & 0x00ff)
			iyProgram[3] = UInt8(addr >> 8)

			let expectedResult = UInt16((addr + 1) & 0xffff)

			XCTAssertEqual(test(program: hlBaseProgram, initialValue: 0xffff), expectedResult)

			XCTAssertEqual(test(program: bcEDProgram, initialValue: 0xffff), expectedResult)
			XCTAssertEqual(test(program: deEDProgram, initialValue: 0xffff), expectedResult)
			XCTAssertEqual(test(program: hlEDProgram, initialValue: 0xffff), expectedResult)
			XCTAssertEqual(test(program: spEDProgram, initialValue: 0xffff), expectedResult)

			XCTAssertEqual(test(program: ixProgram, initialValue: 0xffff), expectedResult)
			XCTAssertEqual(test(program: iyProgram, initialValue: 0xffff), expectedResult)
		}
	}

	// EX (SP), rp
	func testEXSPrp() {
		// MEMPTR = rp at end
		var hlProgram: [UInt8] = [
			0xe3, 0x00, 0x00, 0x00
		]
		var ixProgram: [UInt8] = [
			0xdd, 0xe3, 0x00, 0x00
		]
		var iyProgram: [UInt8] = [
			0xfd, 0xe3, 0x00, 0x00
		]

		machine.setValue(2, for: .stackPointer)

		for addr in 0 ..< 65536 {
			hlProgram[2] = UInt8(addr & 0x00ff)
			hlProgram[3] = UInt8(addr >> 8)
			ixProgram[2] = UInt8(addr & 0x00ff)
			ixProgram[3] = UInt8(addr >> 8)
			iyProgram[2] = UInt8(addr & 0x00ff)
			iyProgram[3] = UInt8(addr >> 8)

			XCTAssertEqual(test(program: hlProgram, initialValue: 0xffff), UInt16(addr))
			XCTAssertEqual(test(program: ixProgram, initialValue: 0xffff), UInt16(addr))
			XCTAssertEqual(test(program: iyProgram, initialValue: 0xffff), UInt16(addr))
		}
	}

	// ADD/ADC/SBC dest, src
	func testADDADCSBCrr() {
		// MEMPTR = dest prior to modification + 1
		let addProgram: [UInt8] = [
			0x09
		]
		let adcProgram: [UInt8] = [
			0xed, 0x4a
		]
		let sbcProgram: [UInt8] = [
			0xed, 0x42
		]

		for addr in 0 ..< 65536 {
			let expectedResult = UInt16((addr + 1) & 0xffff)
			machine.setValue(UInt16(addr), for: .HL)

			XCTAssertEqual(test(program: addProgram, initialValue: 0xffff), expectedResult)
			XCTAssertEqual(test(program: adcProgram, initialValue: 0xffff), expectedResult)
			XCTAssertEqual(test(program: sbcProgram, initialValue: 0xffff), expectedResult)
		}
	}

	// RLD/RRD
	func testRLDRRD() {
		// MEMPTR = HL + 1
		let rldProgram: [UInt8] = [
			0xed, 0x6f
		]
		let rrdProgram: [UInt8] = [
			0xed, 0x67
		]

		for addr in 0 ..< 65536 {
			let expectedResult = UInt16((addr + 1) & 0xffff)
			machine.setValue(UInt16(addr), for: .HL)

			XCTAssertEqual(test(program: rldProgram, initialValue: 0xffff), expectedResult)
			XCTAssertEqual(test(program: rrdProgram, initialValue: 0xffff), expectedResult)
		}
	}

	/* TODO:
		JR/DJNZ/RET/RETI/RST (jumping to addr)
			MEMPTR = addr

		(implemented in principle)
	*/
	func testJR() {
		var jrProgram: [UInt8] = [
			0x18, 0x00
		]

		for offset in 0 ..< 256 {
			jrProgram[1] = UInt8(offset)
			let result = test(program: jrProgram, initialValue: 0xffff)
			XCTAssertEqual(result, machine.value(for: .programCounter))
		}
	}

	func testJRcc() {
		func testJR(instruction: UInt8, shouldPass: Bool) {
			var program: [UInt8] = [
				instruction, 0x00
			]

			for offset in 0 ..< 256 {
				program[1] = UInt8(offset)
				let result = test(program: program, initialValue: 0xffff)
				XCTAssertEqual(result, shouldPass ? machine.value(for: .programCounter) : 0xffff)
			}
		}

		// JR NZ.
		machine.setValue(0x00, for: .AF)
		testJR(instruction: 0x20, shouldPass: true)

		machine.setValue(0xff, for: .AF)
		testJR(instruction: 0x20, shouldPass: false)

		// JR NC
		machine.setValue(0x00, for: .AF)
		testJR(instruction: 0x30, shouldPass: true)

		machine.setValue(0xff, for: .AF)
		testJR(instruction: 0x30, shouldPass: false)

		// JR Z
		machine.setValue(0x00, for: .AF)
		testJR(instruction: 0x28, shouldPass: false)

		machine.setValue(0xff, for: .AF)
		testJR(instruction: 0x28, shouldPass: true)

		// JR C
		machine.setValue(0x00, for: .AF)
		testJR(instruction: 0x38, shouldPass: false)

		machine.setValue(0xff, for: .AF)
		testJR(instruction: 0x38, shouldPass: true)
	}

	func testRST() {
		var rstProgram: [UInt8] = [
			0x00
		]

		for offset in 0 ..< 8 {
			rstProgram[0] = UInt8(offset << 3) + 0xc7
			let result = test(program: rstProgram, initialValue: 0xffff)
			XCTAssertEqual(result, machine.value(for: .programCounter))
		}
	}

	func testRET() {
		let retProgram: [UInt8] = [
			0xc9
		]

		for addr in 0 ..< 65536 {
			let stackContents: [UInt8] = [
				UInt8(addr & 0xff), UInt8(addr >> 8)
			]

			machine.setData(Data(stackContents), atAddress: 0xf001)
			machine.setValue(0xf000, for: .stackPointer)
			let result = test(program: retProgram, initialValue: 0xffff)
			XCTAssertEqual(result, machine.value(for: .programCounter))
		}
	}
	func testRETI() {
		let retiProgram: [UInt8] = [
			0xed, 0x4d
		]

		for addr in 0 ..< 65536 {
			let stackContents: [UInt8] = [
				UInt8(addr & 0xff), UInt8(addr >> 8)
			]

			machine.setData(Data(stackContents), atAddress: 0xf001)
			machine.setValue(0xf000, for: .stackPointer)
			let result = test(program: retiProgram, initialValue: 0xffff)
			XCTAssertEqual(result, machine.value(for: .programCounter))
		}
	}

	/* TODO:
		JP(except JP rp)/CALL addr (even in case of conditional call/jp, independently on condition satisfied or not)
			MEMPTR = addr
	*/
	func testCALL() {
		var callProgram: [UInt8] = [
			0xcd, 0x00, 0x00
		]

		for offset in 0 ..< 65536 {
			callProgram[1] = UInt8(offset & 0xff)
			callProgram[2] = UInt8(offset >> 8)
			let result = test(program: callProgram, initialValue: 0xffff)
			XCTAssertEqual(result, machine.value(for: .programCounter))
		}
	}

	/* TODO:
		IN A,(port)
			MEMPTR = (A_before_operation << 8) + port + 1

		(implemented, not tested)
	*/

	/* TODO:
		IN A,(C)
			MEMPTR = BC + 1

		(implemented, not tested)
	*/

	/* TODO:
		OUT (port),A
			MEMPTR_low = (port + 1) & #FF,  MEMPTR_hi = A
			Note for *BM1: MEMPTR_low = (port + 1) & #FF,  MEMPTR_hi = 0
	*/

	/* TODO:
		OUT (C),A
			MEMPTR = BC + 1
	*/

	/* TODO:
		LDIR/LDDR
			when BC == 1: MEMPTR is not changed
			when BC <> 1: MEMPTR = PC + 1, where PC = instruction address
	*/

	// CPI
	func testCPI() {
		// MEMPTR = MEMPTR + 1
		let program: [UInt8] = [
			0xed, 0xa1
		]
		machine.setData(Data(_: program), atAddress: 0x0000)
		machine.setValue(0, for: .memPtr)

		for c in 1 ..< 65536 {
			machine.setValue(0x0000, for: .programCounter)
			machine.runForNumber(ofCycles: 16)
			XCTAssertEqual(UInt16(c), machine.value(for: .memPtr))
		}
	}

	// CPD
	func testCPD() {
		// MEMPTR = MEMPTR - 1
		let program: [UInt8] = [
			0xed, 0xa9
		]
		machine.setData(Data(_: program), atAddress: 0x0000)
		machine.setValue(0, for: .memPtr)

		for c in 1 ..< 65536 {
			machine.setValue(0x0000, for: .programCounter)
			machine.runForNumber(ofCycles: 16)
			XCTAssertEqual(UInt16(65536 - c), machine.value(for: .memPtr))
		}
	}

	/* TODO:
		CPIR
			when BC=1 or A=(HL): exactly as CPI
			In other cases MEMPTR = PC + 1 on each step, where PC = instruction address.
			Note* since at the last execution BC=1 or A=(HL), resulting MEMPTR = PC + 1 + 1
			  (if there were not interrupts during the execution)
	*/

	/* TODO:
		CPDR
			when BC=1 or A=(HL): exactly as CPD
			In other cases MEMPTR = PC + 1 on each step, where PC = instruction address.
			Note* since at the last execution BC=1 or A=(HL), resulting MEMPTR = PC + 1 - 1
			  (if there were not interrupts during the execution)
	*/

	/* TODO:
		INI
			MEMPTR = BC_before_decrementing_B + 1
	*/

	/* TODO:
		IND
			MEMPTR = BC_before_decrementing_B - 1
	*/

	/* TODO:
		INIR
			exactly as INI on each execution.
			I.e. resulting MEMPTR = ((1 << 8) + C) + 1
	*/

	/* TODO:
		INDR
			exactly as IND on each execution.
			I.e. resulting MEMPTR = ((1 << 8) + C) - 1
	*/

	/* TODO:
		OUTI
			MEMPTR = BC_after_decrementing_B + 1
	*/

	/* TODO:
		OUTD
			MEMPTR = BC_after_decrementing_B - 1
	*/

	/* TODO:
		OTIR
			exactly as OUTI on each execution. I.e. resulting MEMPTR = C + 1
	*/

	/* TODO:
		OTDR
			exactly as OUTD on each execution. I.e. resulting MEMPTR = C - 1
	*/

	/* TODO:
		Any instruction with (INDEX+d):
			MEMPTR = INDEX+d
	*/

	/* TODO:
		Interrupt call to addr:
			As usual CALL. I.e. MEMPTR = addr
	*/
}
