//
//  65816ComparativeTests.m
//  Clock SignalTests
//
//  Created by Thomas Harte on 18/06/2022.
//  Copyright © 2022 Thomas Harte. All rights reserved.
//

#include "65816.hpp"

#import <XCTest/XCTest.h>
#include <array>
#include <optional>
#include <vector>
#include <unordered_map>

namespace {

struct StopException {};

struct BusHandler: public CPU::MOS6502Esque::BusHandler<uint32_t>  {
	// Use a map to store RAM contents, in order to preserve initialised state.
	std::unordered_map<uint32_t, uint8_t> ram;
	std::unordered_map<uint32_t, uint8_t> inventions;

	Cycles perform_bus_operation(CPU::MOS6502Esque::BusOperation operation, uint32_t address, uint8_t *value) {
		// Record the basics of the operation.
		auto &cycle = cycles.emplace_back();
		cycle.operation = operation;
		cycle.extended_bus = processor.get_extended_bus_output();

		// Perform the operation, and fill in the cycle's value.
		using BusOperation = CPU::MOS6502Esque::BusOperation;
		auto ram_value = ram.find(address);
		switch(operation) {
			case BusOperation::ReadOpcode:
				if(initial_pc && (*initial_pc != address || !allow_pc_repetition)) {
					cycles.pop_back();
					pc_overshoot = -1;
					throw StopException();
				}
				initial_pc = address;
				[[fallthrough]];

			case BusOperation::Read:
			case BusOperation::ReadProgram:
			case BusOperation::ReadVector:
				cycle.address = address;
				if(ram_value != ram.end()) {
					cycle.value = *value = ram_value->second;
				} else {
					cycle.value = *value = uint8_t(rand() >> 8);
					inventions[address] = ram[address] = *cycle.value;
				}
			break;

			case BusOperation::Write:
				cycle.address = address;
				cycle.value = ram[address] = *value;
			break;

			case BusOperation::Ready:
			case BusOperation::None:
				throw StopException();
			break;

			case BusOperation::InternalOperationWrite:
				cycle.value = *value = ram_value->second;
				[[fallthrough]];

			case BusOperation::InternalOperationRead:
				cycle.address = address;
			break;

			default: assert(false);
		}

		// Don't occupy any bonus time.
		return Cycles(1);
	}

	void setup(uint8_t opcode) {
		ram.clear();
		inventions.clear();
		cycles.clear();
		pc_overshoot = 0;
		initial_pc = std::nullopt;

		// For MVP or MVN, keep tracking fetches via the same location.
		// For other instructions, don't. That's to avoid endless loops
		// for flow control that happens to jump back to where it began.
		allow_pc_repetition = opcode == 0x54 || opcode == 0x44;

		using Register = CPU::MOS6502Esque::Register;
		const uint32_t pc =
			processor.value_of(Register::ProgramCounter) |
			(processor.value_of(Register::ProgramBank) << 16);
		inventions[pc] = ram[pc] = opcode;
	}

	int pc_overshoot = 0;
	std::optional<uint32_t> initial_pc;
	bool allow_pc_repetition = false;

	struct Cycle {
		CPU::MOS6502Esque::BusOperation operation;
		std::optional<uint32_t> address;
		std::optional<uint8_t> value;
		int extended_bus;
	};
	std::vector<Cycle> cycles;

	CPU::WDC65816::Processor<BusHandler, false> processor;

	BusHandler() : processor(*this) {
		// Never run the official reset procedure.
		processor.set_power_on(false);
	}
};

template <typename Processor> void print_registers(FILE *file, const Processor &processor, int pc_offset) {
	using Register = CPU::MOS6502Esque::Register;
	fprintf(file, "\"pc\": %d, ", (processor.value_of(Register::ProgramCounter) + pc_offset) & 65535);
	fprintf(file, "\"s\": %d, ", processor.value_of(Register::StackPointer));
	fprintf(file, "\"p\": %d, ", processor.value_of(Register::Flags));
	fprintf(file, "\"a\": %d, ", processor.value_of(Register::A));
	fprintf(file, "\"x\": %d, ", processor.value_of(Register::X));
	fprintf(file, "\"y\": %d, ", processor.value_of(Register::Y));
	fprintf(file, "\"dbr\": %d, ", processor.value_of(Register::DataBank));
	fprintf(file, "\"d\": %d, ", processor.value_of(Register::Direct));
	fprintf(file, "\"pbr\": %d, ", processor.value_of(Register::ProgramBank));
	fprintf(file, "\"e\": %d, ", processor.value_of(Register::EmulationFlag));
}

void print_ram(FILE *file, const std::unordered_map<uint32_t, uint8_t> &data) {
	fprintf(file, "\"ram\": [");
	bool is_first = true;
	for(const auto &pair: data) {
		if(!is_first) fprintf(file, ", ");
		is_first = false;
		fprintf(file, "[%d, %d]", pair.first, pair.second);
	}
	fprintf(file, "]");
}

}

// MARK: - New test generator.

@interface TestGenerator : NSObject
@end

@implementation TestGenerator

- (void)generate {
	BusHandler handler;

	// Make tests repeatable, at least for any given instance of
	// the runtime.
	srand(65816);

	NSString *const tempDir = NSTemporaryDirectory();
	NSLog(@"Outputting to %@", tempDir);

	for(int operation = 0; operation < 512; operation++) {
		const bool is_emulated = operation & 256;
		const uint8_t opcode = operation & 255;

		NSString *const targetName = [NSString stringWithFormat:@"%@%02x.%c.json", tempDir, opcode, is_emulated ? 'e' : 'n'];
		FILE *const target = fopen(targetName.UTF8String, "wt");

		bool is_first_test = true;
		fprintf(target, "[");
		for(int test = 0; test < 10'000; test++) {
			if(!is_first_test) fprintf(target, ",\n");
			is_first_test = false;

			// Ensure processor's next action is an opcode fetch.
			handler.processor.restart_operation_fetch();

			// Randomise most of the processor state...
			using Register = CPU::MOS6502Esque::Register;
			handler.processor.set_value_of(Register::A, rand() >> 8);
			handler.processor.set_value_of(Register::Flags, rand() >> 8);
			handler.processor.set_value_of(Register::X, rand() >> 8);
			handler.processor.set_value_of(Register::Y, rand() >> 8);
			handler.processor.set_value_of(Register::ProgramCounter, rand() >> 8);
			handler.processor.set_value_of(Register::StackPointer, rand() >> 8);
			handler.processor.set_value_of(Register::DataBank, rand() >> 8);
			handler.processor.set_value_of(Register::ProgramBank, rand() >> 8);
			handler.processor.set_value_of(Register::Direct, rand() >> 8);

			// ... except for emulation mode, which is a given.
			// And is set last to ensure proper internal state is applied.
			handler.processor.set_value_of(Register::EmulationFlag, is_emulated);

			// Establish the opcode.
			handler.setup(opcode);

			// Dump initial state.
			fprintf(target, "{ \"name\": \"%02x %c %d\", ", opcode, is_emulated ? 'e' : 'n', test + 1);
			fprintf(target, "\"initial\": {");
			print_registers(target, handler.processor, 0);

			// Run to the second opcode fetch.
			try {
				handler.processor.run_for(Cycles(100));
			} catch (const StopException &) {}

			// Dump all inventions as initial memory state.
			print_ram(target, handler.inventions);

			// Dump final state.
			fprintf(target, "}, \"final\": {");
			print_registers(target, handler.processor, handler.pc_overshoot);
			print_ram(target, handler.ram);
			fprintf(target, "}, ");

			// Append cycles.
			fprintf(target, "\"cycles\": [");

			bool is_first_cycle = true;
			for(const auto &cycle: handler.cycles) {
				if(!is_first_cycle) fprintf(target, ",");
				is_first_cycle = false;

				bool vda = false;
				bool vpa = false;
				bool vpb = false;
				bool read = false;
				bool wait = false;
				using BusOperation = CPU::MOS6502Esque::BusOperation;
				switch(cycle.operation) {
					case BusOperation::Read:					read = vda = true;			break;
					case BusOperation::ReadOpcode:				read = vda = vpa = true;	break;
					case BusOperation::ReadProgram:				read = vpa = true;			break;
					case BusOperation::ReadVector:				read = vpb = vda = true;	break;
					case BusOperation::InternalOperationRead:	read = true;				break;

					case BusOperation::Write:					vda = true;					break;
					case BusOperation::InternalOperationWrite:								break;

					case BusOperation::None:
					case BusOperation::Ready:					wait = true;				break;

					default:
						assert(false);
				}

				using ExtendedBusOutput = CPU::WDC65816::ExtendedBusOutput;
				const bool emulation = cycle.extended_bus & ExtendedBusOutput::Emulation;
				const bool memory_size = cycle.extended_bus & ExtendedBusOutput::MemorySize;
				const bool index_size = cycle.extended_bus & ExtendedBusOutput::IndexSize;
				const bool memory_lock = cycle.extended_bus & ExtendedBusOutput::MemoryLock;

				fprintf(target, "[");
				if(cycle.address) {
					fprintf(target, "%d, ", *cycle.address);
				} else {
					fprintf(target, "null, ");
				}
				if(cycle.value) {
					fprintf(target, "%d, ", *cycle.value);
				} else {
					fprintf(target, "null, ");
				}
				fprintf(target, "\"%c%c%c%c%c%c%c%c\"]",
					vda ? 'd' : '-',
					vpa ? 'p' : '-',
					vpb ? 'v' : '-',
					wait ? '-' : (read ? 'r' : 'w'),
					wait ? '-' : (emulation ? 'e' : '-'),
					wait ? '-' : (memory_size ? 'm' : '-'),
					wait ? '-' : (index_size ? 'x' : '-'),
					wait ? '-' : (memory_lock ? 'l' : '-')
				);
			}

			// Terminate object.
			fprintf(target, "]}");
		}

		fprintf(target, "]");
		fclose(target);
	}
}

@end

// MARK: - Existing test evaluator.

@interface WDC65816ComparativeTests : XCTestCase
@end

@implementation WDC65816ComparativeTests

// A generator for tests; not intended to be a permanent fixture.
//- (void)testGenerate {
//	[[[TestGenerator alloc] init] generate];
//}

@end
