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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.CircuitMutation;
import com.cburch.logisim.circuit.CircuitTransaction;
import com.cburch.logisim.circuit.CircuitTransactionResult;
import com.cburch.logisim.circuit.ReplacementMap;
import com.cburch.logisim.circuit.SubcircuitFactory;
import com.cburch.logisim.circuit.Wire;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentFactory;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.file.LogisimFile;
import com.cburch.logisim.proj.Action;
import com.cburch.logisim.proj.JoinedAction;
import com.cburch.logisim.proj.Project;
import com.cburch.logisim.tools.AddTool;
import com.cburch.logisim.tools.Library;
import com.cburch.logisim.tools.Tool;

public class SelectionActions {
	private SelectionActions() { }
	
	public static Action drop(Selection sel, Collection<Component> comps) {
		HashSet<Component> floating = new HashSet<Component>(sel.getFloatingComponents());
		HashSet<Component> anchored = new HashSet<Component>(sel.getAnchoredComponents());
		ArrayList<Component> toDrop = new ArrayList<Component>();
		ArrayList<Component> toIgnore = new ArrayList<Component>();
		for (Component comp : comps) {
			if (floating.contains(comp)) {
				toDrop.add(comp);
			} else if (anchored.contains(comp)) {
				toDrop.add(comp);
				toIgnore.add(comp);
			}
		}
		int numDrop = toDrop.size() - toIgnore.size();
		if (numDrop == 0) {
			for (Component comp : toIgnore) {
				sel.remove(null, comp);
			}
			return null;
		} else {
			return new Drop(sel, toDrop, numDrop);
		}
	}
	
	public static Action dropAll(Selection sel) {
		return drop(sel, sel.getComponents());
	}

	public static Action clear(Selection sel) {
		return new Delete(sel);
	}
	
	public static Action duplicate(Selection sel) {
		return new Duplicate(sel);
	}

	public static Action cut(Selection sel) {
		return new Cut(sel);
	}

	public static Action copy(Selection sel) {
		return new Copy(sel);
	}

	public static Action pasteMaybe(Project proj, Selection sel) {
		HashMap<Component, Component> replacements = getReplacementMap(proj);
		return new Paste(sel, replacements);
	}
	
	public static Action translate(Selection sel, int dx, int dy, ReplacementMap repl) {
		return new Translate(sel, dx, dy, repl);
	}
	
	private static class Drop extends Action {
		private Selection sel;
		private Component[] drops;
		private int numDrops;
		private SelectionSave before;
		private CircuitTransaction xnReverse;
		
		Drop(Selection sel, Collection<Component> toDrop, int numDrops) {
			this.sel = sel;
			this.drops = new Component[toDrop.size()];
			toDrop.toArray(this.drops);
			this.numDrops = numDrops;
			this.before = SelectionSave.create(sel);
		}

		@Override
		public String getName() {
			return numDrops == 1 ? Strings.get("dropComponentAction")
					: Strings.get("dropComponentsAction");
		}

		@Override
		public void doIt(Project proj) {
			Circuit circuit = proj.getCurrentCircuit();
			CircuitMutation xn = new CircuitMutation(circuit);
			for (Component comp : drops) {
				sel.remove(xn, comp);
			}
			CircuitTransactionResult result = xn.execute();
			xnReverse = result.getReverseTransaction();
		}

		@Override
		public void undo(Project proj) {
			xnReverse.execute();
		}

		@Override
		public boolean shouldAppendTo(Action other) {
			Action last;
			if (other instanceof JoinedAction) last = ((JoinedAction) other).getLastAction();
			else last = other;
			
			SelectionSave otherAfter = null;
			if (last instanceof Paste) {
				otherAfter = ((Paste) last).after;
			} else if (last instanceof Duplicate) {
				otherAfter = ((Duplicate) last).after;
			}
			return otherAfter != null && otherAfter.equals(this.before);
		}
	}

	private static class Delete extends Action {
		private Selection sel;
		private CircuitTransaction xnReverse;

		Delete(Selection sel) {
			this.sel = sel;
		}

		@Override
		public String getName() {
			return Strings.get("deleteSelectionAction");
		}

		@Override
		public void doIt(Project proj) {
			Circuit circuit = proj.getCurrentCircuit();
			CircuitMutation xn = new CircuitMutation(circuit);
			sel.deleteAllHelper(xn);
			CircuitTransactionResult result = xn.execute();
			xnReverse = result.getReverseTransaction();
		}

		@Override
		public void undo(Project proj) {
			xnReverse.execute();
		}
	}

	private static class Duplicate extends Action {
		private Selection sel;
		private CircuitTransaction xnReverse;
		private SelectionSave after;

		Duplicate(Selection sel) {
			this.sel = sel;
		}

		@Override
		public String getName() {
			return Strings.get("duplicateSelectionAction");
		}

		@Override
		public void doIt(Project proj) {
			Circuit circuit = proj.getCurrentCircuit();
			CircuitMutation xn = new CircuitMutation(circuit);
			sel.duplicateHelper(xn);

			CircuitTransactionResult result = xn.execute();
			xnReverse = result.getReverseTransaction();
			after = SelectionSave.create(sel);
		}

		@Override
		public void undo(Project proj) {
			xnReverse.execute();
		}
	}

	private static class Cut extends Action {
		private Action first;
		private Action second;

		Cut(Selection sel) {
			first = new Copy(sel);
			second = new Delete(sel);
		}

		@Override
		public String getName() {
			return Strings.get("cutSelectionAction");
		}

		@Override
		public void doIt(Project proj) {
			first.doIt(proj);
			second.doIt(proj);
		}

		@Override
		public void undo(Project proj) {
			second.undo(proj);
			first.undo(proj);
		}
	}

	private static class Copy extends Action {
		private Selection sel;
		private Clipboard oldClip;

		Copy(Selection sel) {
			this.sel = sel;
		}

		@Override
		public boolean isModification() { return false; }

		@Override
		public String getName() {
			return Strings.get("copySelectionAction");
		}

		@Override
		public void doIt(Project proj) {
			oldClip = Clipboard.get();
			Clipboard.set(sel, sel.getAttributeSet());
		}

		@Override
		public void undo(Project proj) {
			Clipboard.set(oldClip);
		}
	}
	
	private static HashMap<Component, Component> getReplacementMap(Project proj) {
		HashMap<Component, Component> replMap;
		replMap = new HashMap<Component, Component>();
		
		LogisimFile file = proj.getLogisimFile();
		ArrayList<Library> libs = new ArrayList<Library>();
		libs.add(file);
		libs.addAll(file.getLibraries());
		
		ArrayList<String> dropped = null; 
		Clipboard clip = Clipboard.get();
		Collection<Component> comps = clip.getComponents();
		HashMap<ComponentFactory, ComponentFactory> factoryReplacements;
		factoryReplacements = new HashMap<ComponentFactory, ComponentFactory>();
		for (Component comp : comps) {
			if (comp instanceof Wire) continue;
			
			ComponentFactory compFactory = comp.getFactory();
			ComponentFactory copyFactory = findComponentFactory(compFactory, libs, false);
			if (factoryReplacements.containsKey(compFactory)) {
				copyFactory = factoryReplacements.get(compFactory);
			} else if (copyFactory == null) {
				ComponentFactory candidate = findComponentFactory(compFactory, libs, true);
				if (candidate == null) {
					if (dropped == null) {
						dropped = new ArrayList<String>();
					}
					dropped.add(compFactory.getDisplayName());
				} else {
					String msg = Strings.get("pasteCloneQuery",
							compFactory.getName());
					Object[] opts = { Strings.get("pasteCloneReplace"),
							Strings.get("pasteCloneIgnore"),
							Strings.get("pasteCloneCancel") };
					int select = JOptionPane.showOptionDialog(proj.getFrame(),
							msg, Strings.get("pasteCloneTitle"), 0,
							JOptionPane.QUESTION_MESSAGE, null, opts, opts[0]);
					if (select == 0) {
						copyFactory = candidate;
					} else if (select == 1) {
						copyFactory = null;
					} else {
						return null;
					}
					factoryReplacements.put(compFactory, copyFactory);
				}
			}
			
			if (copyFactory == null) {
				replMap.put(comp, null);
			} else if (copyFactory != compFactory) {
				Location copyLoc = comp.getLocation();
				AttributeSet copyAttrs = (AttributeSet) comp.getAttributeSet().clone();
				Component copy = copyFactory.createComponent(copyLoc, copyAttrs);
				replMap.put(comp, copy);
			}
		}
		
		if (dropped != null) {
			Collections.sort(dropped);
			StringBuilder droppedStr = new StringBuilder();
			droppedStr.append(Strings.get("pasteDropMessage"));
			String curName = dropped.get(0);
			int curCount = 1;
			int lines = 1;
			for (int i = 1; i <= dropped.size(); i++) {
				String nextName = i == dropped.size() ? "" : dropped.get(i);
				if (nextName.equals(curName)) {
					curCount++;
				} else {
					lines++;
					droppedStr.append("\n  ");
					droppedStr.append(curName);
					if (curCount > 1) {
						droppedStr.append(" \u00d7 " + curCount);
					}
					
					curName = nextName;
					curCount = 1;
				}
			}

			lines = Math.max(3, Math.min(7, lines));
			JTextArea area = new JTextArea(lines, 60);
			area.setEditable(false);
			area.setText(droppedStr.toString());
			area.setCaretPosition(0);
			JScrollPane areaPane = new JScrollPane(area);
			JOptionPane.showMessageDialog(proj.getFrame(), areaPane,
					Strings.get("pasteDropTitle"),
					JOptionPane.WARNING_MESSAGE);
		}
		
		return replMap;
	}
	
	private static ComponentFactory findComponentFactory(ComponentFactory factory,
			ArrayList<Library> libs, boolean acceptNameMatch) {
		String name = factory.getName();
		for (Library lib : libs) {
			for (Tool tool : lib.getTools()) {
				if (tool instanceof AddTool) {
					AddTool addTool = (AddTool) tool;
					if (name.equals(addTool.getName())) {
						ComponentFactory fact = addTool.getFactory(true);
						if (acceptNameMatch) {
							return fact;
						} else if (fact == factory) {
							return fact;
						} else if (fact.getClass() == factory.getClass()
								&& !(fact instanceof SubcircuitFactory)) {
							return fact;
						}
					}
				}
			}
		}
		return null;
	}

	private static class Paste extends Action {
		private Selection sel;
		private CircuitTransaction xnReverse;
		private SelectionSave after;
		private HashMap<Component,Component> componentReplacements; 

		Paste(Selection sel, HashMap<Component, Component> replacements) {
			this.sel = sel;
			this.componentReplacements = replacements;
		}

		@Override
		public String getName() {
			return Strings.get("pasteClipboardAction");
		}

		@Override
		public void doIt(Project proj) {
			Clipboard clip = Clipboard.get();
			Circuit circuit = proj.getCurrentCircuit();
			CircuitMutation xn = new CircuitMutation(circuit);
			Collection<Component> comps = clip.getComponents();
			Collection<Component> toAdd = computeAdditions(comps);
			if (toAdd.size() > 0) {
				sel.pasteHelper(xn, toAdd);
				CircuitTransactionResult result = xn.execute();
				xnReverse = result.getReverseTransaction();
				after = SelectionSave.create(sel);
			} else {
				xnReverse = null;
			}
		}
		
		private Collection<Component> computeAdditions(Collection<Component> comps) {
			HashMap<Component, Component> replMap = componentReplacements;
			ArrayList<Component> toAdd = new ArrayList<Component>(comps.size());
			for (Iterator<Component> it = comps.iterator(); it.hasNext(); ) {
				Component comp = it.next();
				if (replMap.containsKey(comp)) {
					Component repl = replMap.get(comp);
					if (repl != null) {
						toAdd.add(repl);
					}
				} else {
					toAdd.add(comp);
				}
			}
			return toAdd;
		}
		
		@Override
		public void undo(Project proj) {
			if (xnReverse != null) {
				xnReverse.execute();
			}
		}
	}

	private static class Translate extends Action {
		private Selection sel;
		private int dx;
		private int dy;
		private ReplacementMap replacements;
		private SelectionSave before;
		private CircuitTransaction xnReverse;
		
		Translate(Selection sel, int dx, int dy, ReplacementMap replacements) {
			this.sel = sel;
			this.dx = dx;
			this.dy = dy;
			this.replacements = replacements;
			this.before = SelectionSave.create(sel);
		}

		@Override
		public String getName() {
			return Strings.get("moveSelectionAction");
		}

		@Override
		public void doIt(Project proj) {
			Circuit circuit = proj.getCurrentCircuit();
			CircuitMutation xn = new CircuitMutation(circuit);

			sel.translateHelper(xn, dx, dy);
			if (replacements != null) {
				xn.replace(replacements);
			}

			CircuitTransactionResult result = xn.execute();
			xnReverse = result.getReverseTransaction();
		}

		@Override
		public void undo(Project proj) {
			xnReverse.execute();
		}

		@Override
		public boolean shouldAppendTo(Action other) {
			Action last;
			if (other instanceof JoinedAction) last = ((JoinedAction) other).getLastAction();
			else last = other;
			
			SelectionSave otherAfter = null;
			if (last instanceof Paste) {
				otherAfter = ((Paste) last).after;
			} else if (last instanceof Duplicate) {
				otherAfter = ((Duplicate) last).after;
			}
			return otherAfter != null && otherAfter.equals(this.before);
		}
	}
}
