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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.cburch.draw.model.CanvasObject;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.instance.Instance;
import com.cburch.logisim.instance.StdAttr;

class PortManager {
	private CircuitAppearance appearance;
	private boolean doingUpdate;
	
	PortManager(CircuitAppearance appearance) {
		this.appearance = appearance;
		this.doingUpdate = false;
	}
	
	void updatePorts() {
		appearance.recomputePorts();
	}
	
	void updatePorts(Set<Instance> adds, Set<Instance> removes,
			Map<Instance, Instance> replaces, Collection<Instance> allPins) {
		if (appearance.isDefaultAppearance()) {
			appearance.recomputePorts();
		} else if (!doingUpdate) {
			// "doingUpdate" ensures infinite recursion doesn't happen
			try {
				doingUpdate = true;
				performUpdate(adds, removes, replaces, allPins);
				appearance.recomputePorts();
			} finally {
				doingUpdate = false;
			}
		}
	}
	
	private void performUpdate(Set<Instance> adds, Set<Instance> removes,
			Map<Instance, Instance> replaces, Collection<Instance> allPins) {
		// Find the current objects corresponding to pins
		Map<Instance, AppearancePort> oldObjects;
		oldObjects = new HashMap<Instance, AppearancePort>();
		AppearanceAnchor anchor = null;
		for (CanvasObject o : appearance.getObjectsFromBottom()) {
			if (o instanceof AppearancePort) {
				AppearancePort port = (AppearancePort) o;
				oldObjects.put(port.getPin(), port);
			} else if (o instanceof AppearanceAnchor) {
				anchor = (AppearanceAnchor) o; 
			}
		}
		
		// ensure we have the anchor in the circuit
		if (anchor == null) {
			for (CanvasObject o : DefaultAppearance.build(allPins)) {
				if (o instanceof AppearanceAnchor) {
					anchor = (AppearanceAnchor) o;
				}
			}
			if (anchor == null) {
				anchor = new AppearanceAnchor(Location.create(100, 100));
			}
			int dest = appearance.getObjectsFromBottom().size();
			appearance.addObjects(dest, Collections.singleton(anchor));
		}

		// Compute how the ports should change
		ArrayList<AppearancePort> portRemoves;
		portRemoves = new ArrayList<AppearancePort>(removes.size());
		ArrayList<AppearancePort> portAdds;
		portAdds = new ArrayList<AppearancePort>(adds.size());
		
		// handle removals
		for (Instance pin : removes) {
			AppearancePort port = oldObjects.remove(pin);
			if (port != null) {
				portRemoves.add(port);
			}
		}
		// handle replacements
		ArrayList<Instance> addsCopy = new ArrayList<Instance>(adds);
		for (Map.Entry<Instance, Instance> entry : replaces.entrySet()) {
			AppearancePort port = oldObjects.remove(entry.getKey());
			if (port != null) {
				port.setPin(entry.getValue());
				oldObjects.put(entry.getValue(), port);
			} else { // this really shouldn't happen, but just to make sure...
				addsCopy.add(entry.getValue());
			}
		}
		// handle additions
		DefaultAppearance.sortPinList(addsCopy, Direction.EAST);
			// They're probably not really all facing east.
			// I'm just sorting them so it works predictably.
		for (Instance pin : addsCopy) {
			if (!oldObjects.containsKey(pin)) {
				Location loc = computeDefaultLocation(appearance, pin, oldObjects);
				AppearancePort o = new AppearancePort(loc, pin);
				portAdds.add(o);
				oldObjects.put(pin, o);
			}
		}

		// Now update the appearance
		appearance.replaceAutomatically(portRemoves, portAdds);
	}
	
	private static Location computeDefaultLocation(CircuitAppearance appear,
			Instance pin, Map<Instance, AppearancePort> others) {
		// Determine which locations are being used in canvas, and look for
		// which instances facing the same way in layout
		Set<Location> usedLocs = new HashSet<Location>();
		List<Instance> sameWay = new ArrayList<Instance>();
		Direction facing = pin.getAttributeValue(StdAttr.FACING);
		for (Map.Entry<Instance, AppearancePort> entry : others.entrySet()) {
			Instance pin2 = entry.getKey();
			Location loc = entry.getValue().getLocation();
			usedLocs.add(loc);
			if (pin2.getAttributeValue(StdAttr.FACING) == facing) {
				sameWay.add(pin2);
			}
		}
		
		// if at least one faces the same way, place pin relative to that
		if (sameWay.size() > 0) {
			sameWay.add(pin);
			DefaultAppearance.sortPinList(sameWay, facing);
			boolean isFirst = false; 
			Instance neighbor = null; // (preferably previous in map)
			for (Instance p : sameWay) {
				if (p == pin) {
					break;
				} else {
					neighbor = p;
				}
			}
			if (neighbor == null) { // pin must have been first in list
				neighbor = sameWay.get(1);
			}
			int dx;
			int dy;
			if (facing == Direction.EAST || facing == Direction.WEST) {
				dx = 0;
				dy = isFirst? -10 : 10;
			} else {
				dx = isFirst ? -10 : 10;
				dy = 0;
			}
			Location loc = others.get(neighbor).getLocation();
			do {
				loc = loc.translate(dx, dy);
			} while (usedLocs.contains(loc));
			if (loc.getX() >= 0 && loc.getY() >= 0) {
				return loc;
			}
			do {
				loc = loc.translate(-dx, -dy);
			} while (usedLocs.contains(loc));
			return loc;
		}
		
		// otherwise place it on the boundary of the bounding rectangle
		Bounds bds = appear.getAbsoluteBounds();
		int x;
		int y;
		int dx = 0;
		int dy = 0;
		if (facing == Direction.EAST) { // on west side by default
			x = bds.getX() - 7;
			y = bds.getY() + 5;
			dy = 10;
		} else if (facing == Direction.WEST) { // on east side by default
			x = bds.getX() + bds.getWidth() - 3;
			y = bds.getY() + 5;
			dy = 10;
		} else if (facing == Direction.SOUTH) { // on north side by default
			x = bds.getX() + 5;
			y = bds.getY() - 7;
			dx = 10;
		} else { // on south side by default
			x = bds.getX() + 5;
			y = bds.getY() + bds.getHeight() - 3;
			dx = 10;
		}
		x = (x + 9) / 10 * 10; // round coordinates up to ensure they're on grid
		y = (y + 9) / 10 * 10;
		Location loc = Location.create(x, y);
		while (usedLocs.contains(loc)) {
			loc = loc.translate(dx, dy);
		}
		return loc;
	}
}
