package miggy.cpupoet;

import static junit.framework.TestCase.assertEquals;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class CoreTest extends Core {
	private final ByteBuffer memory;
	private final boolean is24bits;

	private final Set<Integer> berrs = new HashSet<Integer>();

	private static final int MAGIC_NAME = 0x89ABCDEF;
	private static final int MAGIC_TRANSACTIONS = 0x456789AB;
	private static final int MAGIC_STATE = 0x01234567;
	private static final int MAGIC_TEST = 0xABC12367;
	private static final int MAGIC_FILE_HEADER = 0x1A3F5D71;

	public void executeBinTest(String name, int... skips) {
		ByteBuffer buffer = getResourceAsByteBuffer(name);

		setclrSSWI(SSWI_DNGR | SSWI_SNGL, 0);

		int magicNum = buffer.getInt();
		if (magicNum != MAGIC_FILE_HEADER) {
			throw new IllegalStateException(
					String.format("Unexpected magic number for file header: %X, expected %X in file %s", magicNum,
							MAGIC_FILE_HEADER, name));
		}

		int numTests = buffer.getInt();

		System.out.println(String.format("Number of tests for %s : %d", name, numTests));

		for (int i = 0; i < numTests; i++) {
			boolean skip = checkSkip(i, skips);

			if ((i == 383) && "Bcc".equals(name)) {
				toString();
			}
			executeBinTest(buffer, skip);
		}
	}
	
	public boolean checkSkip(int i, int... skips) {
		for (int j = 0; j < skips.length; j++) {
			if (i == skips[j]) {
				return true;
			}
		}
		
		return false;
	}

	public void executeBinTest(ByteBuffer buffer, boolean skip) {
		int pos = buffer.position();
		int numBytes = buffer.getInt();

		int magicNum = buffer.getInt();
		if (magicNum != MAGIC_TEST) {
			throw new IllegalStateException(
					String.format("Unexpected magic number for test case: %X, expected %X", magicNum, MAGIC_TEST));
		}

		String testName = readTestName(buffer);
		System.out.print(String.format("%s ", testName));

		CPUState start = new CPUState(buffer);
		CPUState end = new CPUState(buffer);

		if (!skip) {
			start.apply(this, end);
			execute(1000);
			end.check(this);

			System.out.println("success");
		} else {
			System.out.println("skipped");
		}

		skipTransactions(buffer);

		if ((buffer.position() - pos) != numBytes) {
			throw new IllegalStateException(
					String.format("test suite length mismatch: %X, expected %X", buffer.position() - pos, numBytes));
		}
	}

	private void skipTransactions(ByteBuffer buffer) {
		int pos = buffer.position();
		int numBytes = buffer.getInt();

		int magicNum = buffer.getInt();
		if (magicNum != MAGIC_TRANSACTIONS) {
			throw new IllegalStateException(
					String.format("Unexpected magic number for transactions: %X, expected %X", magicNum, MAGIC_TEST));
		}

		buffer.getInt(); // total cycles
		int numTransactions = buffer.getInt();

		for (int i = 0; i < numTransactions; i++) {
			int tw = buffer.get() & 0xFF; // Read as unsigned byte

			buffer.getInt(); // cycles for this transaction

			if (tw != 0) { // not nop cycle
				buffer.getInt(); // fc
				buffer.getInt(); // aob
				buffer.getInt(); // dib/dob
				buffer.getInt(); // uds
				buffer.getInt(); // lds
			}
		}

		if ((buffer.position() - pos) != numBytes) {
			throw new IllegalStateException(
					String.format("transactions length mismatch: %X, expected %X", buffer.position() - pos, numBytes));
		}
	}

	private static final class CPUState {
		private final List<MemoryValue> ram = new ArrayList<MemoryValue>();
		private final int[] dar = new int[15];
		private int usp;
		private int isp;
		private int sr;
		private int pc;

		private CPUState(ByteBuffer buffer) {
			int pos = buffer.position();
			int numBytes = buffer.getInt();

			int magicNum = buffer.getInt();
			if (magicNum != MAGIC_STATE) {
				throw new IllegalStateException(
						String.format("Unexpected magic number for state: %X, expected %X", magicNum, MAGIC_STATE));
			}

			for (int i = 0; i < 15; i++) {
				dar[i] = buffer.getInt();
			}

			this.usp = buffer.getInt();
			this.isp = buffer.getInt();
			this.sr = buffer.getInt();
			this.pc = buffer.getInt() - 4;

			/* skip prefetch check */
			buffer.getInt();
			buffer.getInt();

			int numRams = buffer.getInt();

			for (int i = 0; i < numRams; i++) {
				int addr = buffer.getInt();
				short dataShort = buffer.getShort();

				ram.add(new MemoryValue(addr, dataShort));
			}

			if ((buffer.position() - pos) != numBytes) {
				throw new IllegalStateException(
						String.format("state length mismatch: %X, expected %X", buffer.position() - pos, numBytes));
			}
		}

		private void apply(CoreTest core, CPUState finalState) {
			for (int i = 0; i < 15; i++) {
				core.setDARL(i, dar[i]);
			}

			core.setUSP(usp);
			core.setISP(isp);
			core.setSR(sr & ~(SR_T0 | SR_M));

			/* the pc points to the next instruction */
			core.pc = this.pc;

			for (MemoryValue value : finalState.ram) {
				core.write16(value.addr, 0);
			}

			for (MemoryValue value : ram) {
				core.write16(value.addr, value.data);
			}

			core.mpc = 11;
			core.sswi &= ~(SR_T1 | SR_T0);
		}

		private void check(CoreTest core) {
			boolean trap = false;
			boolean aerr = false;

			for (int i = 8; i <= 256; i += 4) {
				if (core.pc == core.read32(i)) {
					trap |= true;
					
					if ((i == 8) || (i == 12)) {
						aerr |= true;
					}
					break;
				}
			}
			
			if (!aerr) {
				for (int i = 0; i < 15; i++) {
					assertEquals(dar[i], core.getDARL(i));
				}

				assertEquals(usp, core.getUSP());
			}

			if (!trap) {
				assertEquals(isp, core.getISP());

				for (MemoryValue value : ram) {
					assertEquals(value.data, core.read16(value.addr));
				}
				assertEquals(sr, core.getSR());
			} else {
				assertEquals(sr & 0xff00, core.getSR() & 0xff00);
			}

			assertEquals(pc, core.pc + core.scan);
		}
	}

	private static final class MemoryValue {
		private final int addr;
		private final short data;

		private MemoryValue(int addr, short data) {
			this.addr = addr;
			this.data = data;
		}
	}

	private static String readTestName(ByteBuffer buffer) {
		int pos = buffer.position();
		int numBytes = buffer.getInt();

		int magicNum = buffer.getInt();
		if (magicNum != MAGIC_NAME) {
			throw new IllegalStateException(
					String.format("Unexpected magic number for name: %X, expected %X", magicNum, MAGIC_NAME));
		}

		int strlen = buffer.getInt();
		byte[] strBytes = new byte[strlen];

		buffer.get(strBytes);

		if ((buffer.position() - pos) != numBytes) {
			throw new IllegalStateException(
					String.format("name length mismatch: %X, expected %X", buffer.position() - pos, numBytes));
		}

		return new String(strBytes, StandardCharsets.UTF_8);
	}

	private static ByteBuffer getResourceAsByteBuffer(String path) {
		InputStream in = CoreTest.class.getResourceAsStream(String.format("%s.json.bin", path));
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		int read;
		byte[] data = new byte[1024];

		try {
			while ((read = in.read(data, 0, data.length)) != -1) {
				out.write(data, 0, read);
			}
		} catch (IOException e) {
			throw new IllegalStateException(e);
		} finally {
			try {
				in.close();
			} catch (IOException e) {
				throw new IllegalStateException(e);
			}
		}

		ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray());

		buffer.order(ByteOrder.LITTLE_ENDIAN);

		return buffer;
	}

	public CoreTest() {
		/* allocate 512 MiB */
		this(512 * 1024, false);
	}

	public CoreTest(int mem, boolean is24bits) {
		this.memory = ByteBuffer.allocate(mem);

		memory.order(ByteOrder.BIG_ENDIAN);
		this.is24bits = is24bits;
	}

	public void setInitialSSP(int ssp) {
		write32(0, ssp);
	}

	public void setInitialPC(int ssp) {
		write32(4, ssp);
	}

	public Set<Integer> getBErrs() {
		return berrs;
	}

	@Override
	public int fetch16(int aob) {
		return read16(aob);
	}

	@Override
	public int fetch32(int aob) {
		return read32(aob);
	}

	protected int check8(int aob) {
		if (is24bits) {
			aob &= 0xffffff;
		}

		if (((aob & 0x7fffffff) + 1 >= memory.capacity()) || berrs.contains(aob)) {
			setclrSSW(SSW_BR, 0);

			return -1;
		}

		return aob;
	}

	protected int check16(int aob) {
		if (is24bits) {
			aob &= 0xffffff;
		}

		if ((aob &= 0x7fffffff) + 2 >= memory.capacity() || berrs.contains(aob)) {
			setclrSSW(SSW_BR, 0);

			return -1;
		}

		return aob;
	}

	protected int check32(int aob) {
		if (is24bits) {
			aob &= 0xffffff;
		}

		if ((aob &= 0x7fffffff) + 4 >= memory.capacity() || berrs.contains(aob)) {
			setclrSSW(SSW_BR, 0);

			return -1;
		}

		return aob;
	}

	@Override
	public byte read8(int aob) {
		aob = check8(aob);

		return aob < 0 ? 0 : memory.get(aob);
	}

	@Override
	public short read16(int aob) {
		aob = check16(aob);

		return aob < 0 ? 0 : memory.getShort(aob);
	}

	@Override
	public int read32(int aob) {
		aob = check32(aob);

		return aob < 0 ? 0 : memory.getInt(aob);
	}

	@Override
	public void write8(int aob, int dob) {
		aob = check8(aob);

		if (aob >= 0) {
			memory.put(aob, (byte) dob);
		}
	}

	@Override
	public void write16(int aob, int dob) {
		aob = check16(aob);

		if (aob >= 0) {
			memory.putShort(aob, (short) dob);
		}
	}

	@Override
	public void write32(int aob, int dob) {
		aob = check32(aob);

		if (aob >= 0) {
			memory.putInt(aob, dob);
		}
	}

	@Override
	public int handle_interrupt(int level) {
		return IRQ_AVEC;
	}

	@Override
	public int handle_bkpt(int pc, int data) {
		return BKPT_RPIR | BKPT_EXIT | 0x4e71;
	}

	public int getPC() {
		return pc;
	}

	public void setPC(int pc) {
		this.pc = pc;
	}

	public int getScan() {
		return scan;
	}

	public void setScan(int scan) {
		this.scan = scan;
	}

	public int getAluB() {
		return alub;
	}

	public void setAluB(int alub) {
		this.alub = alub;
	}

	public int getIRB() {
		return irb;
	}

	public void setIRB(int irb) {
		this.irb = irb;
	}

	public int getIR() {
		return ir;
	}

	public void setIR(int ir) {
		this.ir = ir;
	}

	public int getMPC() {
		return mpc;
	}

	public void setMPC(int mpc) {
		this.mpc = mpc;
	}

	public int getCIP() {
		return cip;
	}

	public void setCIP(int cip) {
		this.cip = cip;
	}

	public int getAU() {
		return au;
	}

	public void setAU(int au) {
		this.au = au;
	}

	public int getAT() {
		return at;
	}

	public void setAT(int at) {
		this.at = at;
	}

	public int getDT() {
		return dt;
	}

	public void setDT(int dt) {
		this.dt = dt;
	}

	public int getSlice() {
		return slice;
	}

	public void setSlice(int slice) {
		this.slice = slice;
	}
}
