/* 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.proj;

import java.util.HashMap;
import java.util.LinkedList;

import javax.swing.JFileChooser;

import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.CircuitListener;
import com.cburch.logisim.circuit.CircuitMutation;
import com.cburch.logisim.circuit.CircuitState;
import com.cburch.logisim.circuit.Simulator;
import com.cburch.logisim.circuit.SubcircuitFactory;
import com.cburch.logisim.file.Loader;
import com.cburch.logisim.file.LogisimFile;
import com.cburch.logisim.file.LibraryEvent;
import com.cburch.logisim.file.LibraryListener;
import com.cburch.logisim.file.Options;
import com.cburch.logisim.gui.log.LogFrame;
import com.cburch.logisim.gui.main.Canvas;
import com.cburch.logisim.gui.main.Frame;
import com.cburch.logisim.gui.main.Selection;
import com.cburch.logisim.gui.main.SelectionActions;
import com.cburch.logisim.gui.opts.OptionsFrame;
import com.cburch.logisim.tools.AddTool;
import com.cburch.logisim.tools.Library;
import com.cburch.logisim.tools.Tool;
import com.cburch.logisim.util.EventSourceWeakSupport;
import com.cburch.logisim.util.JFileChoosers;

public class Project {
	private static final int MAX_UNDO_SIZE = 64;

	private static class ActionData {
		CircuitState circuitState;
		Action action;

		public ActionData(CircuitState circuitState, Action action) {
			this.circuitState = circuitState;
			this.action = action;
		}
	}

	private class MyListener implements Selection.Listener, LibraryListener {
		public void selectionChanged(Selection.Event e) {
			fireEvent(ProjectEvent.ACTION_SELECTION, e.getSource());
		}
		
		public void libraryChanged(LibraryEvent event) {
			int action = event.getAction();
			if (action == LibraryEvent.REMOVE_LIBRARY) {
				Library unloaded = (Library) event.getData();
				if (tool != null && unloaded.containsFromSource(tool)) {
					setTool(null);
				}
			} else if (action == LibraryEvent.REMOVE_TOOL) {
				Object data = event.getData();
				if (data instanceof AddTool) {
					Object factory = ((AddTool) data).getFactory();
					if (factory instanceof SubcircuitFactory) {
						SubcircuitFactory fact = (SubcircuitFactory) factory;
						if (fact.getSubcircuit() == getCurrentCircuit()) {
							setCurrentCircuit(file.getMainCircuit());
						}
					}
				}
			}
		}
	}

	private Simulator simulator = new Simulator();
	private LogisimFile file;
	private CircuitState circuitState;
	private HashMap<Circuit,CircuitState> stateMap
		= new HashMap<Circuit,CircuitState>();
	private Frame frame = null;
	private OptionsFrame optionsFrame = null;
	private LogFrame logFrame = null;
	private Tool tool = null;
	private LinkedList<ActionData> undoLog = new LinkedList<ActionData>();
	private int undoMods = 0;
	private EventSourceWeakSupport<ProjectListener> projectListeners
		= new EventSourceWeakSupport<ProjectListener>();
	private EventSourceWeakSupport<LibraryListener> fileListeners
		= new EventSourceWeakSupport<LibraryListener>();
	private EventSourceWeakSupport<CircuitListener> circuitListeners
		= new EventSourceWeakSupport<CircuitListener>();
	private Dependencies depends;
	private MyListener myListener = new MyListener();
	private boolean startupScreen = false;

	public Project(LogisimFile file) {
		addLibraryListener(myListener);
		setLogisimFile(file);
	}

	public void setFrame(Frame value) {
		if (frame == value) return;
		Frame oldValue = frame;
		frame = value;
		Projects.windowCreated(this, oldValue, value);
		value.getCanvas().getSelection().addListener(myListener);
	}

	//
	// access methods
	//
	public LogisimFile getLogisimFile() {
		return file;
	}

	public Simulator getSimulator() {
		return simulator;
	}

	public Options getOptions() {
		return file.getOptions();
	}

	public Dependencies getDependencies() {
		return depends;
	}

	public Frame getFrame() {
		return frame;
	}
	
	public OptionsFrame getOptionsFrame(boolean create) {
		if (optionsFrame == null || optionsFrame.getLogisimFile() != file) {
			if (create) optionsFrame = new OptionsFrame(this);
			else optionsFrame = null;
		}
		return optionsFrame;
	}
	
	public LogFrame getLogFrame(boolean create) {
		if (logFrame == null) {
			if (create) logFrame = new LogFrame(this);
		}
		return logFrame;
	}

	public Circuit getCurrentCircuit() {
		return circuitState == null ? null : circuitState.getCircuit();
	}

	public CircuitState getCircuitState() {
		return circuitState;
	}
	
	public CircuitState getCircuitState(Circuit circuit) {
		if (circuitState != null && circuitState.getCircuit() == circuit) {
			return circuitState;
		} else {
			CircuitState ret = stateMap.get(circuit);
			if (ret == null) {
				ret = new CircuitState(this, circuit);
				stateMap.put(circuit, ret);
			}
			return ret;
		}
	}

	public Action getLastAction() {
		if (undoLog.size() == 0) {
			return null;
		} else {
			return undoLog.getLast().action;
		}
	}

	public Tool getTool() {
		return tool;
	}

	public Selection getSelection() {
		if (frame == null) return null;
		Canvas canvas = frame.getCanvas();
		if (canvas == null) return null;
		return canvas.getSelection();
	}

	public boolean isFileDirty() {
		return undoMods != 0;
	}

	public JFileChooser createChooser() {
		if (file == null) return JFileChoosers.create();
		Loader loader = file.getLoader();
		return loader == null ? JFileChoosers.create() : loader.createChooser();
	}

	//
	// Listener methods
	//
	public void addProjectListener(ProjectListener what) {
		projectListeners.add(what);
	}

	public void removeProjectListener(ProjectListener what) {
		projectListeners.remove(what);
	}
	
	public void addLibraryListener(LibraryListener value) {
		fileListeners.add(value);
		if (file != null) file.addLibraryListener(value);
	}
	
	public void removeLibraryListener(LibraryListener value) {
		fileListeners.remove(value);
		if (file != null) file.removeLibraryListener(value);
	}
	
	public void addCircuitListener(CircuitListener value) {
		circuitListeners.add(value);
		Circuit current = getCurrentCircuit();
		if (current != null) current.addCircuitListener(value);
	}
	
	public void removeCircuitListener(CircuitListener value) {
		circuitListeners.remove(value);
		Circuit current = getCurrentCircuit();
		if (current != null) current.removeCircuitListener(value);
	}

	private void fireEvent(int action, Object old, Object data) {
		fireEvent(new ProjectEvent(action, this, old, data));
	}

	private void fireEvent(int action, Object data) {
		fireEvent(new ProjectEvent(action, this, data));
	}

	private void fireEvent(ProjectEvent event) {
		for (ProjectListener l : projectListeners) {
			l.projectChanged(event);
		}
	}
	
	// We track whether this project is the empty project opened
	// at startup by default, because we want to close it
	// immediately as another project is opened, if there
	// haven't been any changes to it.
	public boolean isStartupScreen() {
		return startupScreen;
	}
	
	public boolean confirmClose(String title) {
		return frame.confirmClose(title);
	}

	//
	// actions
	//
	public void setStartupScreen(boolean value) {
		startupScreen = value;
	}

	public void setLogisimFile(LogisimFile value) {
		LogisimFile old = this.file;
		if (old != null) {
			for (LibraryListener l : fileListeners) {
				old.removeLibraryListener(l);
			}
		}
		file = value;
		stateMap.clear();
		depends = new Dependencies(file);
		undoLog.clear();
		undoMods = 0;
		fireEvent(ProjectEvent.ACTION_SET_FILE, old, file);
		setCurrentCircuit(file.getMainCircuit());
		if (file != null) {
			for (LibraryListener l : fileListeners) {
				file.addLibraryListener(l);
			}
		}
		file.setDirty(true); // toggle it so that everybody hears the file is fresh
		file.setDirty(false);
	}

	public void setCircuitState(CircuitState value) {
		if (value == null || circuitState == value) return;

		CircuitState old = circuitState;
		Circuit oldCircuit = old == null ? null : old.getCircuit();
		Circuit newCircuit = value.getCircuit();
		boolean circuitChanged = old == null || oldCircuit != newCircuit;
		if (circuitChanged) {
			Canvas canvas = frame == null ? null : frame.getCanvas();
			if (canvas != null) {
				if (tool != null) tool.deselect(canvas);
				Selection selection = canvas.getSelection();
				if (selection != null) {
					Action act = SelectionActions.dropAll(selection);
					if (act != null) {
						doAction(act);
					}
				}
				if (tool != null) tool.select(canvas);
			}
			if (oldCircuit != null) {
				for (CircuitListener l : circuitListeners) {
					oldCircuit.removeCircuitListener(l);
				}
			}
		}
		circuitState = value;
		stateMap.put(circuitState.getCircuit(), circuitState);
		simulator.setCircuitState(circuitState);
		if (circuitChanged) {
			fireEvent(ProjectEvent.ACTION_SET_CURRENT, oldCircuit, newCircuit);
			if (newCircuit != null) {
				for (CircuitListener l : circuitListeners) {
					newCircuit.addCircuitListener(l);
				}
			}
		}
		fireEvent(ProjectEvent.ACTION_SET_STATE, old, circuitState);
	}

	public void setCurrentCircuit(Circuit circuit) {
		CircuitState circState = stateMap.get(circuit);
		if (circState == null) circState = new CircuitState(this, circuit);
		setCircuitState(circState);
	}

	public void setTool(Tool value) {
		if (tool == value) return;
		Tool old = tool;
		Canvas canvas = frame.getCanvas();
		if (old != null) old.deselect(canvas);
		Selection selection = canvas.getSelection();
		if (selection != null && !selection.isEmpty()) {
			Circuit circuit = canvas.getCircuit();
			CircuitMutation xn = new CircuitMutation(circuit);
			if (value == null) {
				Action act = SelectionActions.dropAll(selection);
				if (act != null) {
					doAction(act);
				}
			} else if (!getOptions().getMouseMappings().containsSelectTool()) {
				Action act = SelectionActions.dropAll(selection);
				if (act != null) {
					doAction(act);
				}
			}
			if (!xn.isEmpty()) doAction(xn.toAction(null));
		}
		startupScreen = false;
		tool = value;
		if (tool != null) tool.select(frame.getCanvas());
		fireEvent(ProjectEvent.ACTION_SET_TOOL, old, tool);
	}

	public void doAction(Action act) {
		if (act == null) return;
		Action toAdd = act;
		startupScreen = false;
		if (!undoLog.isEmpty() && act.shouldAppendTo(getLastAction())) {
			ActionData firstData = undoLog.removeLast();
			Action first = firstData.action;
			if (first.isModification()) --undoMods;
			toAdd = first.append(act);
			if (toAdd != null) {
				undoLog.add(new ActionData(circuitState, toAdd));
				if (toAdd.isModification()) ++undoMods;
			}
			fireEvent(new ProjectEvent(ProjectEvent.ACTION_START, this, act));
			act.doIt(this);
			file.setDirty(isFileDirty());
			fireEvent(new ProjectEvent(ProjectEvent.ACTION_COMPLETE, this, act));
			fireEvent(new ProjectEvent(ProjectEvent.ACTION_MERGE, this, first, toAdd));
			return;
		}
		undoLog.add(new ActionData(circuitState, toAdd));
		fireEvent(new ProjectEvent(ProjectEvent.ACTION_START, this, act));
		act.doIt(this);
		while (undoLog.size() > MAX_UNDO_SIZE) {
			undoLog.removeFirst();
		}
		if (toAdd.isModification()) ++undoMods;
		file.setDirty(isFileDirty());
		fireEvent(new ProjectEvent(ProjectEvent.ACTION_COMPLETE, this, act));
	}

	public void undoAction() {
		if (undoLog != null && undoLog.size() > 0) {
			ActionData data = undoLog.removeLast();
			setCircuitState(data.circuitState);
			Action action = data.action;
			if (action.isModification()) --undoMods;
			fireEvent(new ProjectEvent(ProjectEvent.UNDO_START, this, action));
			action.undo(this);
			file.setDirty(isFileDirty());
			fireEvent(new ProjectEvent(ProjectEvent.UNDO_COMPLETE, this, action));
		}
	}

	public void setFileAsClean() {
		undoMods = 0;
		file.setDirty(isFileDirty());
	}

	public void repaintCanvas() {
		// for actions that ought not be logged (i.e., those that
		// change nothing, except perhaps the current values within
		// the circuit)
		fireEvent(new ProjectEvent(ProjectEvent.REPAINT_REQUEST, this, null));
	}
}
