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

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;

import javax.swing.Icon;
import javax.swing.JOptionPane;

import com.cburch.logisim.LogisimVersion;
import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.CircuitException;
import com.cburch.logisim.circuit.CircuitMutation;
import com.cburch.logisim.circuit.SubcircuitFactory;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentFactory;
import com.cburch.logisim.comp.ComponentDrawContext;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeEvent;
import com.cburch.logisim.data.AttributeListener;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.gui.main.Canvas;
import com.cburch.logisim.gui.main.SelectionActions;
import com.cburch.logisim.gui.main.ToolAttributeAction;
import com.cburch.logisim.prefs.AppPreferences;
import com.cburch.logisim.proj.Action;
import com.cburch.logisim.proj.Dependencies;
import com.cburch.logisim.proj.Project;
import com.cburch.logisim.tools.key.KeyConfigurationEvent;
import com.cburch.logisim.tools.key.KeyConfigurator;
import com.cburch.logisim.tools.key.KeyConfigurationResult;
import com.cburch.logisim.util.StringUtil;

public class AddTool extends Tool {
	private static int INVALID_COORD = Integer.MIN_VALUE;
	
	private static int SHOW_NONE    = 0;
	private static int SHOW_GHOST   = 1;
	private static int SHOW_ADD     = 2;
	private static int SHOW_ADD_NO  = 3;

	private static Cursor cursor
		= Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);

	private class MyAttributeListener implements AttributeListener {
		public void attributeListChanged(AttributeEvent e) {
			bounds = null;
		}
		public void attributeValueChanged(AttributeEvent e) {
			bounds = null;
		}
	}

	private Class<? extends Library> descriptionBase;
	private FactoryDescription description;
	private boolean sourceLoadAttempted;
	private ComponentFactory factory;
	private AttributeSet attrs;
	private Bounds bounds;
	private boolean shouldSnap;
	private int lastX = INVALID_COORD;
	private int lastY = INVALID_COORD;
	private int state = SHOW_GHOST;
	private Action lastAddition;
	private boolean keyHandlerTried;
	private KeyConfigurator keyHandler;
	
	public AddTool(Class<? extends Library> base, FactoryDescription description) {
		this.descriptionBase = base;
		this.description = description;
		this.sourceLoadAttempted = false;
		this.shouldSnap = true;
		this.attrs = new FactoryAttributes(base, description);
		attrs.addAttributeListener(new MyAttributeListener());
		this.keyHandlerTried = false;
	}

	public AddTool(ComponentFactory source) {
		this.description = null;
		this.sourceLoadAttempted = true;
		this.factory = source;
		this.bounds = null;
		this.attrs = new FactoryAttributes(source);
		attrs.addAttributeListener(new MyAttributeListener());
		Boolean value = (Boolean) source.getFeature(ComponentFactory.SHOULD_SNAP, attrs);
		this.shouldSnap = value == null ? true : value.booleanValue();
	}

	private AddTool(AddTool base) {
		this.descriptionBase = base.descriptionBase;
		this.description = base.description;
		this.sourceLoadAttempted = base.sourceLoadAttempted;
		this.factory = base.factory;
		this.bounds = base.bounds;
		this.shouldSnap = base.shouldSnap;
		this.attrs = (AttributeSet) base.attrs.clone();
		attrs.addAttributeListener(new MyAttributeListener());
	}
	
	@Override
	public boolean equals(Object other) {
		if (!(other instanceof AddTool)) return false;
		AddTool o = (AddTool) other;
		if (this.description != null) {
			return this.descriptionBase == o.descriptionBase
				&& this.description.equals(o.description);
		} else {
			return this.factory.equals(o.factory);
		}
	}
	
	@Override
	public int hashCode() {
		FactoryDescription desc = description;
		return desc != null ? desc.hashCode() : factory.hashCode();
	}
	
	@Override
	public boolean sharesSource(Tool other) {
		if (!(other instanceof AddTool)) return false;
		AddTool o = (AddTool) other;
		if (this.sourceLoadAttempted && o.sourceLoadAttempted) {
			return this.factory.equals(o.factory);
		} else if (this.description == null) {
			return o.description == null;
		} else {
			return this.description.equals(o.description);
		}
	}
	
	public ComponentFactory getFactory(boolean forceLoad) {
		return forceLoad ? getFactory() : factory;
	}

	public ComponentFactory getFactory() {
		ComponentFactory ret = factory;
		if (ret != null || sourceLoadAttempted) {
			return ret;
		} else {
			ret = description.getFactory(descriptionBase);
			if (ret != null) {
				AttributeSet base = getBaseAttributes();
				Boolean value = (Boolean) ret.getFeature(ComponentFactory.SHOULD_SNAP, base);
				shouldSnap = value == null ? true : value.booleanValue();
			}
			factory = ret;
			sourceLoadAttempted = true;
			return ret;
		}
	}

	@Override
	public String getName() {
		FactoryDescription desc = description;
		return desc == null ? factory.getName() : desc.getName();
	}

	@Override
	public String getDisplayName() {
		FactoryDescription desc = description;
		return desc == null ? factory.getDisplayName() : desc.getDisplayName();
	}

	@Override
	public String getDescription() {
		String ret;
		FactoryDescription desc = description;
		if (desc != null) {
			ret = desc.getToolTip();
		} else {
			ComponentFactory source = getFactory();
			if (source != null) {
				ret = (String) source.getFeature(ComponentFactory.TOOL_TIP,
						getAttributeSet());
			} else {
				ret = null;
			}
		}
		if (ret == null) {
			ret = StringUtil.format(Strings.get("addToolText"), getDisplayName());
		}
		return ret;
	}

	@Override
	public Tool cloneTool() {
		return new AddTool(this);
	}

	@Override
	public AttributeSet getAttributeSet() {
		return attrs;
	}
	
	@Override
	public boolean isAllDefaultValues(AttributeSet attrs, LogisimVersion ver) {
		return this.attrs == attrs && attrs instanceof FactoryAttributes
			&& !((FactoryAttributes) attrs).isFactoryInstantiated();
	}
	
	@Override
	public Object getDefaultAttributeValue(Attribute<?> attr, LogisimVersion ver) {
		return getFactory().getDefaultAttributeValue(attr, ver);
	}

	@Override
	public void draw(Canvas canvas, ComponentDrawContext context) {
		// next "if" suggested roughly by Kevin Walsh of Cornell to take care of
		// repaint problems on OpenJDK under Ubuntu
		int x = lastX;
		int y = lastY;
		if (x == INVALID_COORD || y == INVALID_COORD) return;
		ComponentFactory source = getFactory();
		if (source == null) return;
		if (state == SHOW_GHOST) {
			source.drawGhost(context, Color.GRAY, x, y, getBaseAttributes());
		} else if (state == SHOW_ADD) {
			source.drawGhost(context, Color.BLACK, x, y, getBaseAttributes());
		}
	}
	
	private AttributeSet getBaseAttributes() {
		AttributeSet ret = attrs;
		if (ret instanceof FactoryAttributes) {
			ret = ((FactoryAttributes) ret).getBase();
		}
		return ret;
	}

	public void cancelOp() { }

	@Override
	public void select(Canvas canvas) {
		setState(canvas, SHOW_GHOST);
		bounds = null;
	}

	@Override
	public void deselect(Canvas canvas) {
		setState(canvas, SHOW_GHOST);
		moveTo(canvas, canvas.getGraphics(), INVALID_COORD, INVALID_COORD);
		bounds = null;
		lastAddition = null;
	}

	private synchronized void moveTo(Canvas canvas, Graphics g,
			int x, int y) {
		if (state != SHOW_NONE) expose(canvas, lastX, lastY);
		lastX = x;
		lastY = y;
		if (state != SHOW_NONE) expose(canvas, lastX, lastY);
	}

	@Override
	public void mouseEntered(Canvas canvas, Graphics g,
			MouseEvent e) {
		if (state == SHOW_GHOST || state == SHOW_NONE) {
			setState(canvas, SHOW_GHOST);
			canvas.requestFocusInWindow();
		} else if (state == SHOW_ADD_NO) {
			setState(canvas, SHOW_ADD);
			canvas.requestFocusInWindow();
		}
	}

	@Override
	public void mouseExited(Canvas canvas, Graphics g,
			MouseEvent e) {
		if (state == SHOW_GHOST) {
			moveTo(canvas, canvas.getGraphics(), INVALID_COORD, INVALID_COORD);
			setState(canvas, SHOW_NONE);
		} else if (state == SHOW_ADD) {
			moveTo(canvas, canvas.getGraphics(), INVALID_COORD, INVALID_COORD);
			setState(canvas, SHOW_ADD_NO);
		}
	}

	@Override
	public void mouseMoved(Canvas canvas, Graphics g, MouseEvent e) {
		if (state != SHOW_NONE) {
			if (shouldSnap) Canvas.snapToGrid(e);
			moveTo(canvas, g, e.getX(), e.getY());
		}
	}

	@Override
	public void mousePressed(Canvas canvas, Graphics g, MouseEvent e) {
		// verify the addition would be valid
		Circuit circ = canvas.getCircuit();
		if (!canvas.getProject().getLogisimFile().contains(circ)) {
			canvas.setErrorMessage(Strings.getter("cannotModifyError"));
			return;
		}
		if (factory instanceof SubcircuitFactory) {
			SubcircuitFactory circFact = (SubcircuitFactory) factory;
			Dependencies depends = canvas.getProject().getDependencies();
			if (!depends.canAdd(circ, circFact.getSubcircuit())) {
				canvas.setErrorMessage(Strings.getter("circularError"));
				return;
			}
		}

		if (shouldSnap) Canvas.snapToGrid(e);
		moveTo(canvas, g, e.getX(), e.getY());
		setState(canvas, SHOW_ADD);
	}

	@Override
	public void mouseDragged(Canvas canvas, Graphics g, MouseEvent e) {
		if (state != SHOW_NONE) {
			if (shouldSnap) Canvas.snapToGrid(e);
			moveTo(canvas, g, e.getX(), e.getY());
		}
	}

	@Override
	public void mouseReleased(Canvas canvas, Graphics g,
			MouseEvent e) {
		Component added = null;
		if (state == SHOW_ADD) {
			Circuit circ = canvas.getCircuit();
			if (!canvas.getProject().getLogisimFile().contains(circ)) return;
			if (shouldSnap) Canvas.snapToGrid(e);
			moveTo(canvas, g, e.getX(), e.getY());

			Location loc = Location.create(e.getX(), e.getY());
			AttributeSet attrsCopy = (AttributeSet) attrs.clone();
			ComponentFactory source = getFactory();
			if (source == null) return;
			Component c = source.createComponent(loc, attrsCopy);
			
			if (circ.hasConflict(c)) {
				canvas.setErrorMessage(Strings.getter("exclusiveError"));
				return;
			}
			
			Bounds bds = c.getBounds(g);
			if (bds.getX() < 0 || bds.getY() < 0) {
				canvas.setErrorMessage(Strings.getter("negativeCoordError"));
				return;
			}

			try {
				CircuitMutation mutation = new CircuitMutation(circ);
				mutation.add(c);
				Action action = mutation.toAction(Strings.getter("addComponentAction", factory.getDisplayGetter()));
				canvas.getProject().doAction(action);
				lastAddition = action;
				added = c;
			} catch (CircuitException ex) {
				JOptionPane.showMessageDialog(canvas.getProject().getFrame(),
					ex.getMessage());
			}
			setState(canvas, SHOW_GHOST);
		} else if (state == SHOW_ADD_NO) {
			setState(canvas, SHOW_NONE);
		}
		
		Project proj = canvas.getProject();
		Tool next = determineNext(proj);
		if (next != null) {
			proj.setTool(next);
			Action act = SelectionActions.dropAll(canvas.getSelection());
			if (act != null) {
				proj.doAction(act);
			}
			if (added != null) canvas.getSelection().add(added);
		}
	}
	
	private Tool determineNext(Project proj) {
		String afterAdd = AppPreferences.ADD_AFTER.get();
		if (afterAdd.equals(AppPreferences.ADD_AFTER_UNCHANGED)) {
			return null;
		} else { // switch to Edit Tool
			Library base = proj.getLogisimFile().getLibrary("Base");
			if (base == null) {
				return null;
			} else {
				return base.getTool("Edit Tool");
			}
		}
	}
	
	@Override
	public void keyPressed(Canvas canvas, KeyEvent event) {
		processKeyEvent(canvas, event, KeyConfigurationEvent.KEY_PRESSED);

		if (!event.isConsumed() && event.getModifiersEx() == 0) {
			switch (event.getKeyCode()) {
			case KeyEvent.VK_UP:    setFacing(canvas, Direction.NORTH); break;
			case KeyEvent.VK_DOWN:  setFacing(canvas, Direction.SOUTH); break;
			case KeyEvent.VK_LEFT:  setFacing(canvas, Direction.WEST); break;
			case KeyEvent.VK_RIGHT: setFacing(canvas, Direction.EAST); break;
			case KeyEvent.VK_BACK_SPACE:
				if (lastAddition != null && canvas.getProject().getLastAction() == lastAddition) {
					canvas.getProject().undoAction();
					lastAddition = null;
				}
			}
		}
	}
	
	@Override
	public void keyReleased(Canvas canvas, KeyEvent event) {
		processKeyEvent(canvas, event, KeyConfigurationEvent.KEY_RELEASED);
	}
	
	@Override
	public void keyTyped(Canvas canvas, KeyEvent event) {
		processKeyEvent(canvas, event, KeyConfigurationEvent.KEY_TYPED);
	}
	
	private void processKeyEvent(Canvas canvas, KeyEvent event, int type) {
		KeyConfigurator handler = keyHandler;
		if (!keyHandlerTried) {
			ComponentFactory source = getFactory();
			AttributeSet baseAttrs = getBaseAttributes();
			handler = (KeyConfigurator) source.getFeature(KeyConfigurator.class, baseAttrs);
			keyHandler = handler;
			keyHandlerTried = true;
		}

		if (handler != null) {
			AttributeSet baseAttrs = getBaseAttributes();
			KeyConfigurationEvent e = new KeyConfigurationEvent(type, baseAttrs, event, this);
			KeyConfigurationResult r = handler.keyEventReceived(e);
			if (r != null) {
				Action act = ToolAttributeAction.create(r);
				canvas.getProject().doAction(act);
			}
		}
	}
	
	private void setFacing(Canvas canvas, Direction facing) {
		ComponentFactory source = getFactory();
		if (source == null) return;
		AttributeSet base = getBaseAttributes();
		Object feature = source.getFeature(ComponentFactory.FACING_ATTRIBUTE_KEY, base);
		@SuppressWarnings("unchecked")
		Attribute<Direction> attr = (Attribute<Direction>) feature;
		if (attr != null) {
			Action act = ToolAttributeAction.create(this, attr, facing);
			canvas.getProject().doAction(act);
		}
	}

	@Override
	public void paintIcon(ComponentDrawContext c, int x, int y) {
		FactoryDescription desc = description;
		if (desc != null && !desc.isFactoryLoaded()) {
			Icon icon = desc.getIcon();
			if (icon != null) {
				icon.paintIcon(c.getDestination(), c.getGraphics(), x + 2, y + 2);
				return;
			}
		}
		
		ComponentFactory source = getFactory();
		if (source != null) {
			AttributeSet base = getBaseAttributes();
			source.paintIcon(c, x, y, base);
		}
	}

	private void expose(java.awt.Component c, int x, int y) {
		Bounds bds = getBounds();
		c.repaint(x + bds.getX(), y + bds.getY(),
			bds.getWidth(), bds.getHeight());
	}

	@Override
	public Cursor getCursor() { return cursor; }

	private void setState(Canvas canvas, int value) {
		if (value == SHOW_GHOST) {
			if (canvas.getProject().getLogisimFile().contains(canvas.getCircuit())
					&& AppPreferences.ADD_SHOW_GHOSTS.getBoolean()) {
				state = SHOW_GHOST;
			} else {
				state = SHOW_NONE;
			}
		} else{
			state = value;
		}
	}

	private Bounds getBounds() {
		Bounds ret = bounds;
		if (ret == null) {
			ComponentFactory source = getFactory();
			if (source == null) {
				ret = Bounds.EMPTY_BOUNDS;
			} else {
				AttributeSet base = getBaseAttributes();
				ret = source.getOffsetBounds(base).expand(5);
			}
			bounds = ret;
		}
		return ret;
	}
}
