package gamax92.thistle;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.io.IOUtils;

import com.loomcom.symon.Cpu;
import com.loomcom.symon.CpuState;

import cpw.mods.fml.common.FMLCommonHandler;
import gamax92.thistle.devices.BankSelector;
import gamax92.thistle.exceptions.CallSynchronizedException;
import gamax92.thistle.exceptions.CallSynchronizedException.Cleanup;
import gamax92.thistle.util.ValueManager;
import li.cil.oc.Settings;
import li.cil.oc.api.Driver;
import li.cil.oc.api.driver.item.Processor;
import li.cil.oc.api.driver.item.Memory;
import li.cil.oc.api.machine.Architecture;
import li.cil.oc.api.machine.Callback;
import li.cil.oc.api.machine.ExecutionResult;
import li.cil.oc.api.machine.LimitReachedException;
import li.cil.oc.api.machine.Machine;
import li.cil.oc.api.machine.Signal;
import li.cil.oc.api.machine.Value;
import li.cil.oc.api.network.Component;
import li.cil.oc.api.network.Node;
import li.cil.oc.common.SaveHandler;
import li.cil.oc.server.PacketSender;
import li.cil.oc.server.machine.Callbacks;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import scala.Option;

@Architecture.Name("65C02 Thistle")
public class ThistleArchitecture implements Architecture {
	private final Machine machine;

	private ThistleVM vm;

	private boolean initialized = false;

	private boolean inSynchronizedCall = false;

	private CallSynchronizedException syncCall;

	/** The constructor must have exactly this signature. */
	public ThistleArchitecture(Machine machine) {
		this.machine = machine;
	}

	@Override
	public boolean isInitialized() {
		return initialized;
	}

	private static int calculateMemory(Iterable<ItemStack> components) {
		int memory = 0;
		for (ItemStack component : components) {
			if (Driver.driverFor(component) instanceof Memory) {
				Memory memdrv = (Memory) Driver.driverFor(component);
				memory += memdrv.amount(component) * 1024;
			}
		}
		return Math.min(Math.max(memory, 0), Settings.get().maxTotalRam());
	}

	@Override
	public boolean recomputeMemory(Iterable<ItemStack> components) {
		if (vm != null) {
			int memory = calculateMemory(components);
			vm.machine.resize(memory);
		}
		return true;
	}

	@Override
	public boolean initialize() {
		// Set up new VM here
		vm = new ThistleVM(machine);
		BankSelector banksel = this.vm.machine.getBankSelector();
		int memory = calculateMemory(this.machine.host().internalComponents());
		vm.machine.resize(memory);
		vm.machine.reset();
		for (ItemStack component : machine.host().internalComponents()) {
			if (Driver.driverFor(component) instanceof Processor) {
				vm.cyclesPerTick = ThistleConfig.debugCpuSlowDown ? 10 : (Driver.driverFor(component).tier(component) + 1) * ThistleConfig.clocksPerTick;
				break;
			}
		}
		try {
			PacketSender.sendSound(machine.host().world(), machine.host().xPosition(), machine.host().yPosition(), machine.host().zPosition(), ".");
		} catch (Throwable e) {
		}
		initialized = true;
		return true;
	}

	@Override
	public void close() {
		ValueManager.removeAll(this.machine);
		if (vm != null) {
			FMLCommonHandler.instance().bus().unregister(vm.machine);
			vm = null;
		}
	}

	@Override
	public ExecutionResult runThreaded(boolean isSynchronizedReturn) {
		try {
			if (!isSynchronizedReturn) {
				// Since our machine is a memory mapped one, parse signals here
				Signal signal = null;
				while (true) {
					signal = machine.popSignal();
					if (signal != null) {
						vm.machine.getBus().onSignal(signal);
					} else
						break;
				}
			}
			vm.run();

			return new ExecutionResult.Sleep(0);
		} catch (CallSynchronizedException e) {
			if (e.getCleanup() != null) {
				if (ThistleConfig.debugCpuTraceLog) // Exceptions thrown cause ThistleVM to skip trace logging.
					Thistle.log.info("[Cpu] " + vm.machine.getCpu().getCpuState().toTraceEvent());
				syncCall = e;
			}
			return new ExecutionResult.SynchronizedCall();
		} catch (LimitReachedException e) {
			return new ExecutionResult.SynchronizedCall();
		} catch (Throwable t) {
			t.printStackTrace();
			return new ExecutionResult.Error(t.toString());
		}
	}

	@Override
	public void runSynchronized() {
		if (syncCall != null) {
			// Nice clean method for us to avoid multiple bus writes
			Object thing = syncCall.getThing();
			Cleanup cleanup = syncCall.getCleanup();
			try {
				Object[] results = null;
				if (thing instanceof String)
					results = machine.invoke((String) thing, syncCall.getMethod(), syncCall.getArgs());
				else if (thing instanceof Value)
					results = machine.invoke((Value) thing, syncCall.getMethod(), syncCall.getArgs());
				cleanup.run(results, machine);
			} catch (Exception e) {
				cleanup.error(e);
			}
			cleanup.finish();
			syncCall = null;
		} else {
			// Attempt to invoke again by re-executing the last instruction
			inSynchronizedCall = true;
			Cpu cpu = vm.machine.getCpu();
			cpu.getCpuState().pc = cpu.getCpuState().lastPc;
			cpu.step();
			inSynchronizedCall = false;
		}
	}

	@Override
	public void onSignal() {
	}

	@Override
	public void onConnect() {
	}

	public Object[] invoke(String address, String method, Object[] args) throws Exception {
		if (!inSynchronizedCall) {
			Node node = machine.node().network().node(address);
			if (node instanceof Component) {
				Callback callback = ((Component) node).annotation(method);
				if (callback != null && !callback.direct())
					throw new CallSynchronizedException(address, method, args);
			}
		}
		try {
			return machine.invoke(address, method, args);
		} catch (LimitReachedException e) {
			throw new CallSynchronizedException(address, method, args);
		}
	}

	public Object[] invoke(Value value, String method, Object[] args) throws Exception {
		if (!inSynchronizedCall) {
			Option<Callbacks.Callback> option = Callbacks.apply(value).get(method);
			if (option != null) {
				Callbacks.Callback callback = option.get();
				if (callback != null && !callback.annotation().direct())
					throw new CallSynchronizedException(value, method, args);
			}
		}
		try {
			return machine.invoke(value, method, args);
		} catch (LimitReachedException e) {
			throw new CallSynchronizedException(value, method, args);
		}
	}

	@Override
	public void load(NBTTagCompound nbt) {
		// Restore Machine

		// Restore Memory
		byte[] gzmem = SaveHandler.load(nbt, this.machine.node().address() + "_memory");
		if (gzmem.length > 0) {
			try {
				ByteArrayInputStream bais = new ByteArrayInputStream(gzmem);
				GZIPInputStream gzis = new GZIPInputStream(bais);
				byte[] mem = IOUtils.toByteArray(gzis);
				IOUtils.closeQuietly(gzis);
				vm.machine.resize(mem.length);
				for (int i = 0; i < mem.length; i++)
					vm.machine.writeMem(i, mem[i]);
			} catch (IOException e) {
				Thistle.log.error("Failed to decompress memory from disk.");
				e.printStackTrace();
			}
		}

		// Restore CPU
		if (nbt.hasKey("cpu")) {
			Cpu mCPU = vm.machine.getCpu();
			CpuState cpuState = mCPU.getCpuState();
			NBTTagCompound cpuTag = nbt.getCompoundTag("cpu");
			cpuState.a = cpuTag.getInteger("rA");
			mCPU.setProcessorStatus(cpuTag.getInteger("rP"));
			cpuState.pc = cpuTag.getInteger("rPC");
			cpuState.sp = cpuTag.getInteger("rSP");
			cpuState.x = cpuTag.getInteger("rX");
			cpuState.y = cpuTag.getInteger("rY");
			cpuState.irqAsserted = cpuTag.getBoolean("iI");
			cpuState.nmiAsserted = cpuTag.getBoolean("iN");
			cpuState.dead = cpuTag.getBoolean("sD");
			cpuState.sleep = cpuTag.getBoolean("sS");
		}

		// Restore Values
		if (nbt.hasKey("values"))
			ValueManager.load(nbt.getCompoundTag("values"), this.machine);

		vm.machine.getBus().load(nbt);
	}

	@Override
	public void save(NBTTagCompound nbt) {
		// Persist Machine

		// Persist Memory
		byte mem[] = new byte[vm.machine.getMemsize()];
		for (int i = 0; i < mem.length; i++)
			mem[i] = vm.machine.readMem(i);
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			GZIPOutputStream gzos = new GZIPOutputStream(baos);
			gzos.write(mem);
			gzos.close();
			SaveHandler.scheduleSave(machine.host(), nbt, machine.node().address() + "_memory", baos.toByteArray());
		} catch (IOException e) {
			Thistle.log.error("Failed to compress memory to disk");
			e.printStackTrace();
		}

		// Persist CPU
		Cpu mCPU = vm.machine.getCpu();
		if (mCPU != null) {
			CpuState cpuState = mCPU.getCpuState();
			NBTTagCompound cpuTag = new NBTTagCompound();
			cpuTag.setInteger("rA", cpuState.a);
			cpuTag.setInteger("rP", mCPU.getProcessorStatus());
			cpuTag.setInteger("rPC", cpuState.pc);
			cpuTag.setInteger("rSP", cpuState.sp);
			cpuTag.setInteger("rX", cpuState.x);
			cpuTag.setInteger("rY", cpuState.y);
			cpuTag.setBoolean("iI", cpuState.irqAsserted);
			cpuTag.setBoolean("iN", cpuState.nmiAsserted);
			cpuTag.setBoolean("sD", cpuState.dead);
			cpuTag.setBoolean("sS", cpuState.sleep);
			nbt.setTag("cpu", cpuTag);
		}

		// Persist Values
		NBTTagCompound valueTag = new NBTTagCompound();
		ValueManager.save(valueTag, this.machine);
		nbt.setTag("values", valueTag);

		vm.machine.getBus().save(nbt);
	}
}
