/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.gui.start;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;

import com.cburch.logisim.circuit.Analyze;
import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.CircuitState;
import com.cburch.logisim.circuit.Propagator;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.data.Value;
import com.cburch.logisim.file.LoadFailedException;
import com.cburch.logisim.file.Loader;
import com.cburch.logisim.file.LogisimFile;
import com.cburch.logisim.file.FileStatistics;
import com.cburch.logisim.instance.Instance;
import com.cburch.logisim.instance.InstanceState;
import com.cburch.logisim.proj.Project;
import com.cburch.logisim.std.io.Keyboard;
import com.cburch.logisim.std.io.Tty;
import com.cburch.logisim.std.memory.Ram;
import com.cburch.logisim.std.wiring.Pin;
import com.cburch.logisim.tools.Library;
import com.cburch.logisim.util.StringUtil;

public class TtyInterface {
	public static final int FORMAT_TABLE = 1;
	public static final int FORMAT_SPEED = 2;
	public static final int FORMAT_TTY = 4;
	public static final int FORMAT_HALT = 8;
	public static final int FORMAT_STATISTICS = 16;

	private static boolean lastIsNewline = true;
	
	public static void sendFromTty(char c) {
		lastIsNewline = c == '\n';
		System.out.print(c); //OK
	}
	
	private static void ensureLineTerminated() {
		if (!lastIsNewline) {
			lastIsNewline = true;
			System.out.print('\n'); //OK
		}
	}
	
	public static void run(Startup args) {
		File fileToOpen = args.getFilesToOpen().get(0);
		Loader loader = new Loader(null);
		LogisimFile file;
		try {
			file = loader.openLogisimFile(fileToOpen, args.getSubstitutions());
		} catch (LoadFailedException e) {
			System.err.println(Strings.get("ttyLoadError", fileToOpen.getName())); //OK
			System.exit(-1);
			return;
		}
		
		int format = args.getTtyFormat();
		if ((format & FORMAT_STATISTICS) != 0) {
			format &= ~FORMAT_STATISTICS;
			displayStatistics(file);
		}
		if (format == 0) { // no simulation remaining to perform, so just exit
			System.exit(0);
		}
		
		Project proj = new Project(file);
		Circuit circuit = file.getMainCircuit();
		Map<Instance, String> pinNames = Analyze.getPinLabels(circuit);
		ArrayList<Instance> outputPins = new ArrayList<Instance>();
		Instance haltPin = null;
		for (Map.Entry<Instance, String> entry : pinNames.entrySet()) {
			Instance pin = entry.getKey();
			String pinName = entry.getValue();
			if (!Pin.FACTORY.isInputPin(pin)) {
				outputPins.add(pin);
				if (pinName.equals("halt")) {
					haltPin = pin;
				}
			}
		}
		
		CircuitState circState = new CircuitState(proj, circuit);
		// we have to do our initial propagation before the simulation starts -
		// it's necessary to populate the circuit with substates.
		circState.getPropagator().propagate();
		if (args.getLoadFile() != null) {
			try {
				boolean loaded = loadRam(circState, args.getLoadFile());
				if (!loaded) {
					System.err.println(Strings.get("loadNoRamError")); //OK
					System.exit(-1);
				}
			} catch (IOException e) {
				System.err.println(Strings.get("loadIoError") + ": " + e.toString()); //OK
				System.exit(-1);
			}
		}
		int ttyFormat = args.getTtyFormat();
		int simCode = runSimulation(circState, outputPins, haltPin, ttyFormat);
		System.exit(simCode);
	}
	
	private static void displayStatistics(LogisimFile file) {
		FileStatistics stats = FileStatistics.compute(file, file.getMainCircuit());
		FileStatistics.Count total = stats.getTotalWithSubcircuits();
		int maxName = 0;
		for (FileStatistics.Count count : stats.getCounts()) {
			int nameLength = count.getFactory().getDisplayName().length();
			if (nameLength > maxName) maxName = nameLength;
		}
		String fmt = "%" + countDigits(total.getUniqueCount()) + "d\t"
			+ "%" + countDigits(total.getRecursiveCount()) + "d\t";
		String fmtNormal = fmt + "%-" + maxName + "s\t%s\n";
		for (FileStatistics.Count count : stats.getCounts()) {
			Library lib = count.getLibrary();
			String libName = lib == null ? "-" : lib.getDisplayName();
			System.out.printf(fmtNormal, //OK
					Integer.valueOf(count.getUniqueCount()),
					Integer.valueOf(count.getRecursiveCount()),
					count.getFactory().getDisplayName(), libName);
		}
		FileStatistics.Count totalWithout = stats.getTotalWithoutSubcircuits();
		System.out.printf(fmt + "%s\n", //OK
				Integer.valueOf(totalWithout.getUniqueCount()),
				Integer.valueOf(totalWithout.getRecursiveCount()),
				Strings.get("statsTotalWithout"));
		System.out.printf(fmt + "%s\n", //OK
				Integer.valueOf(total.getUniqueCount()),
				Integer.valueOf(total.getRecursiveCount()),
				Strings.get("statsTotalWith"));
	}
	
	private static int countDigits(int num) {
		int digits = 1;
		int lessThan = 10;
		while (num >= lessThan) {
			digits++;
			lessThan *= 10;
		}
		return digits;
	}
	
	private static boolean loadRam(CircuitState circState, File loadFile)
			throws IOException {
		if (loadFile == null) return false;
		
		boolean found = false;
		for (Component comp : circState.getCircuit().getNonWires()) {
			if (comp.getFactory() instanceof Ram) {
				Ram ramFactory = (Ram) comp.getFactory();
				InstanceState ramState = circState.getInstanceState(comp);
				ramFactory.loadImage(ramState, loadFile);
				found = true;
			}
		}
		
		for (CircuitState sub : circState.getSubstates()) {
			found |= loadRam(sub, loadFile);
		}
		return found;
	}
	
	private static boolean prepareForTty(CircuitState circState,
			ArrayList<InstanceState> keybStates) {
		boolean found = false;
		for (Component comp : circState.getCircuit().getNonWires()) {
			Object factory = comp.getFactory();
			if (factory instanceof Tty) {
				Tty ttyFactory = (Tty) factory;
				InstanceState ttyState = circState.getInstanceState(comp);
				ttyFactory.sendToStdout(ttyState);
				found = true;
			} else if (factory instanceof Keyboard) {
				keybStates.add(circState.getInstanceState(comp));
				found = true;
			}
		}
		
		for (CircuitState sub : circState.getSubstates()) {
			found |= prepareForTty(sub, keybStates);
		}
		return found;
	}
	
	private static int runSimulation(CircuitState circState,
			ArrayList<Instance> outputPins, Instance haltPin, int format) {
		boolean showTable = (format & FORMAT_TABLE) != 0;
		boolean showSpeed = (format & FORMAT_SPEED) != 0;
		boolean showTty = (format & FORMAT_TTY) != 0;
		boolean showHalt = (format & FORMAT_HALT) != 0;
		
		ArrayList<InstanceState> keyboardStates = null;
		StdinThread stdinThread = null;
		if (showTty) {
			keyboardStates = new ArrayList<InstanceState>();
			boolean ttyFound = prepareForTty(circState, keyboardStates);
			if (!ttyFound) {
				System.err.println(Strings.get("ttyNoTtyError")); //OK
				System.exit(-1);
			}
			if (keyboardStates.isEmpty()) {
				keyboardStates = null;
			} else {
				stdinThread = new StdinThread();
				stdinThread.start();
			}
		}

		int retCode;
		long tickCount = 0;
		long start = System.currentTimeMillis();
		boolean halted = false;
		ArrayList<Value> prevOutputs = null;
		Propagator prop = circState.getPropagator();
		while (true) {
			ArrayList<Value> curOutputs = new ArrayList<Value>();
			for (Instance pin : outputPins) {
				InstanceState pinState = circState.getInstanceState(pin);
				Value val = Pin.FACTORY.getValue(pinState);
				if (pin == haltPin) {
					halted |= val.equals(Value.TRUE);
				} else if (showTable) {
					curOutputs.add(val);
				}
			}
			if (showTable) {
				displayTableRow(prevOutputs, curOutputs);
			}
			
			if (halted) {
				retCode = 0; // normal exit
				break;
			}
			if (prop.isOscillating()) {
				retCode = 1; // abnormal exit
				break;
			}
			if (keyboardStates != null) {
				char[] buffer = stdinThread.getBuffer();
				if (buffer != null) {
					for (InstanceState keyState : keyboardStates) {
						Keyboard.addToBuffer(keyState, buffer);
					}
				}
			}
			prevOutputs = curOutputs;
			tickCount++;
			prop.tick();
			prop.propagate();
		}
		long elapse = System.currentTimeMillis() - start;
		if (showTty) ensureLineTerminated();
		if (showHalt || retCode != 0) {
			if (retCode == 0) {
				System.out.println(Strings.get("ttyHaltReasonPin")); //OK
			} else if (retCode == 1) {
				System.out.println(Strings.get("ttyHaltReasonOscillation")); //OK
			}
		}
		if (showSpeed) {
			displaySpeed(tickCount, elapse);
		}
		return retCode;
	}
	
	private static void displayTableRow(ArrayList<Value> prevOutputs,
			ArrayList<Value> curOutputs) {
		boolean shouldPrint = false;
		if (prevOutputs == null) {
			shouldPrint = true;
		} else {
			for (int i = 0; i < curOutputs.size(); i++) {
				Value a = prevOutputs.get(i);
				Value b = curOutputs.get(i);
				if (!a.equals(b)) {
					shouldPrint = true;
					break;
				}
			}
		}
		if (shouldPrint) {
			for (int i = 0; i < curOutputs.size(); i++) {
				if (i != 0) System.out.print("\t"); //OK
				System.out.print(curOutputs.get(i)); //OK
			}
			System.out.println(); //OK
		}
	}
	
	private static void displaySpeed(long tickCount, long elapse) {
		double hertz = (double) tickCount / elapse * 1000.0;
		double precision;
		if (hertz >= 100) precision = 1.0;
		else if (hertz >= 10) precision = 0.1;
		else if (hertz >= 1) precision = 0.01;
		else if (hertz >= 0.01) precision = 0.0001;
		else precision = 0.0000001;
		hertz = (int) (hertz / precision) * precision;
		String hertzStr = hertz == (int) hertz ? "" + (int) hertz : "" + hertz;
		System.out.println(StringUtil.format(Strings.get("ttySpeedMsg"), //OK
				hertzStr, "" + tickCount, "" + elapse));
	}

	// It's possible to avoid using the separate thread using System.in.available(),
	// but this doesn't quite work because on some systems, the keyboard input
	// is not interactively echoed until System.in.read() is invoked.
	private static class StdinThread extends Thread {
		private LinkedList<char[]> queue; // of char[]
		
		public StdinThread() {
			queue = new LinkedList<char[]>();
		}
		
		public char[] getBuffer() {
			synchronized (queue) {
				if (queue.isEmpty()) {
					return null;
				} else {
					return queue.removeFirst();
				}
			}
		}
		
		@Override
		public void run() {
			InputStreamReader stdin = new InputStreamReader(System.in);
			char[] buffer = new char[32];
			while (true) {
				try {
					int nbytes = stdin.read(buffer);
					if (nbytes > 0) {
						char[] add = new char[nbytes];
						System.arraycopy(buffer, 0, add, 0, nbytes);
						synchronized (queue) {
							queue.addLast(add);
						}
					}
				} catch (IOException e) { }
			}
		}
	}
}
