package com.ra4king.circuitsim.gui;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.ra4king.circuitsim.gui.Connection.PortConnection;
import com.ra4king.circuitsim.gui.Connection.WireConnection;
import com.ra4king.circuitsim.gui.EditHistory.EditAction;
import com.ra4king.circuitsim.gui.LinkWires.Wire;
import com.ra4king.circuitsim.gui.PathFinding.LocationPreference;
import com.ra4king.circuitsim.gui.PathFinding.Point;
import com.ra4king.circuitsim.simulator.Circuit;
import com.ra4king.circuitsim.simulator.CircuitState;
import com.ra4king.circuitsim.simulator.SimulationException;
import com.ra4king.circuitsim.simulator.Simulator;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.util.Pair;

/**
 * @author Roi Atalla
 */
public class CircuitBoard {
	private CircuitManager circuitManager;
	private Circuit circuit;
	private CircuitState currentState;
	
	private Set<ComponentPeer<?>> components;
	private Set<LinkWires> links;
	private Set<LinkWires> badLinks;
	
	private Set<GuiElement> moveElements;
	private Set<Connection> connectedPorts = new HashSet<>();
	
	private Thread computeThread;
	private MoveComputeResult moveResult;
	private boolean addMoveAction;
	private int moveDeltaX, moveDeltaY;
	
	private Map<Pair<Integer, Integer>, Set<Connection>> connectionsMap;
	
	private EditHistory editHistory;
	
	private static class MoveComputeResult {
		final Set<Wire> wiresToAdd;
		final Set<Wire> wiresToRemove;
		
		MoveComputeResult(Set<Wire> wiresToAdd, Set<Wire> wiresToRemove) {
			this.wiresToAdd = wiresToAdd;
			this.wiresToRemove = wiresToRemove;
		}
	}
	
	public CircuitBoard(String name, CircuitManager circuitManager, Simulator simulator, EditHistory editHistory) {
		this.circuitManager = circuitManager;
		
		circuit = new Circuit(name, simulator);
		currentState = circuit.getTopLevelState();
		
		this.editHistory = editHistory;
		
		components = new HashSet<>();
		links = new HashSet<>();
		
		connectionsMap = new HashMap<>();
	}
	
	public String getName() {
		return circuit.getName();
	}
	
	public void setName(String name) {
		circuit.setName(name);
	}
	
	@Override
	public String toString() {
		return "CircuitBoard of " + circuitManager;
	}
	
	public void destroy() {
		try {
			removeElements(components);
		} catch(Exception exc) {
			exc.printStackTrace();
		}
		
		try {
			removeElements(links.stream().flatMap(l -> l.getWires().stream()).collect(Collectors.toSet()));
		} catch(Exception exc) {
			exc.printStackTrace();
		}
		
		badLinks.clear();
		moveElements = null;
		circuit.getSimulator().removeCircuit(circuit);
	}
	
	public Circuit getCircuit() {
		return circuit;
	}
	
	public CircuitState getCurrentState() {
		return currentState;
	}
	
	public void setCurrentState(CircuitState state) {
		if(currentState == null) {
			throw new NullPointerException("CircuitState cannot be null");
		}
		
		if(state.getCircuit() != this.circuit) {
			throw new IllegalArgumentException("The state does not belong to this circuit.");
		}
		
		currentState = state;
	}
	
	public Set<ComponentPeer<?>> getComponents() {
		return components;
	}
	
	public Set<LinkWires> getLinks() {
		return links;
	}
	
	private Exception lastException;
	
	public Exception getLastException() {
		return lastException;
	}
	
	private void updateBadLinks() {
		if((badLinks = links.stream().filter(
			link -> !link.isLinkValid()).collect(Collectors.toSet())).size() > 0) {
			lastException = badLinks.iterator().next().getLastException();
		} else {
			lastException = null;
		}
	}
	
	public boolean isValidLocation(ComponentPeer<?> component) {
		return component.getX() >= 0
			       && component.getY() >= 0
			       && Stream.concat(components.stream(),
			                        moveElements != null
			                        ? moveElements.stream().filter(e -> e instanceof ComponentPeer<?>)
			                        : Stream.empty())
			                .noneMatch(c -> c != component && c.getX() == component.getX()
				                                && c.getY() == component.getY());
	}
	
	public void addComponent(ComponentPeer<?> component) {
		addComponent(component, true);
	}
	
	synchronized void addComponent(ComponentPeer<?> component, boolean splitWires) {
		if(!isValidLocation(component)) {
			throw new SimulationException("Cannot place component here.");
		}
		
		circuit.getSimulator().runSync(() -> {
			// Component must be added here before the circuit as listeners will be triggered to recreate Subcircuits
			components.add(component);
			
			try {
				circuit.addComponent(component.getComponent());
			} catch(Exception exc) {
				components.remove(component);
				throw exc;
			}
			
			try {
				editHistory.disable();
				
				if(splitWires) {
					Set<Wire> toReAdd = new HashSet<>();
					
					for(Connection connection : component.getConnections()) {
						Set<Connection> connections = getConnections(connection.getX(), connection.getY());
						if(connections != null) {
							for(Connection attached : connections) {
								LinkWires linkWires = attached.getLinkWires();
								linkWires.addPort((PortConnection)connection);
								links.add(linkWires);
								
								if(attached instanceof WireConnection) {
									Wire wire = (Wire)attached.getParent();
									if(attached != wire.getStartConnection() && attached != wire.getEndConnection()) {
										toReAdd.add(wire);
									}
								}
							}
						}
						
						addConnection(connection);
					}
					
					for(Wire wire : toReAdd) {
						removeWire(wire);
						addWire(wire.getX(), wire.getY(), wire.getLength(), wire.isHorizontal());
					}
					
					rejoinWires();
				}
				
				updateBadLinks();
			} finally {
				editHistory.enable();
			}
		});
		
		editHistory.addAction(EditAction.ADD_COMPONENT, circuitManager, component);
	}
	
	public void updateComponent(ComponentPeer<?> oldComponent, ComponentPeer<?> newComponent) {
		circuit.getSimulator().runSync(() -> {
			try {
				editHistory.disable();
				
				removeComponent(oldComponent, true);
				
				try {
					circuit.updateComponent(oldComponent.getComponent(), newComponent.getComponent(),
					                        () -> components.add(newComponent));
				} catch(Exception exc) {
					components.remove(newComponent);
					throw exc;
				}
				
				addComponent(newComponent);
			} finally {
				editHistory.enable();
				editHistory.addAction(EditAction.UPDATE_COMPONENT, circuitManager, oldComponent, newComponent);
			}
		});
	}
	
	public boolean isMoving() {
		return moveElements != null;
	}
	
	public void initMove(Set<GuiElement> elements) {
		initMove(elements, true);
	}
	
	void initMove(Set<GuiElement> elements, boolean remove) {
		if(moveElements != null) {
			try {
				finalizeMove();
			} catch(Exception exc) {
				circuitManager.getSimulatorWindow().getDebugUtil().logException(exc);
			}
		}
		
		connectedPorts.clear();
		moveElements = new LinkedHashSet<>(elements);
		addMoveAction = remove;
		
		if(remove) {
			for(GuiElement element : elements) {
				for(Connection connection : element.getConnections()) {
					if((connection instanceof PortConnection
						    || connection == ((Wire)connection.getParent()).getEndConnection()
						    || connection == ((Wire)connection.getParent()).getStartConnection())
						   && getConnections(connection.getX(), connection.getY()).size() > 1) {
						connectedPorts.add(connection);
					}
				}
			}
			
			editHistory.beginGroup();
			removeElements(elements, false);
		}
	}
	
	public void moveElements(int dx, int dy, boolean extendWires) {
		if(moveDeltaX == dx && moveDeltaY == dy) {
			return;
		}
		
		for(GuiElement element : moveElements) {
			element.setX(element.getX() + (-moveDeltaX + dx));
			element.setY(element.getY() + (-moveDeltaY + dy));
		}
		
		moveDeltaX = dx;
		moveDeltaY = dy;
		
		CountDownLatch latch = new CountDownLatch(1);
		
		synchronized(CircuitBoard.this) {
			moveResult = null;
			
			if(computeThread != null) {
				computeThread.interrupt();
			}
			
			if(!extendWires) {
				lastException = null;
				return;
			}
			
			List<Connection> connectedPorts = new ArrayList<>(this.connectedPorts);
			connectedPorts.sort((p1, p2) -> {
				if(p1.getX() == p2.getX()) {
					return dy > 0 ? p1.getY() - p2.getY() : p2.getY() - p1.getY();
				}
				
				return dx > 0 ? p1.getX() - p2.getX() : p2.getX() - p1.getX();
			});
			
			computeThread = new Thread(() -> {
				Set<Wire> paths = new HashSet<>();
				
				Set<Pair<Integer, Integer>> portsSeen = new HashSet<>();
				
				for(Connection connectedPort : connectedPorts) {
					if(Thread.currentThread().isInterrupted()) {
						return;
					}
					
					int x = connectedPort.getX();
					int y = connectedPort.getY();
					int sx = x - dx;
					int sy = y - dy;
					
					if(!portsSeen.add(new Pair<>(x, y))) {
						continue;
					}
					
					final LinkWires linkWires;
					
					synchronized(CircuitBoard.this) {
						Set<Connection> otherConnections = getConnections(sx, sy);
						if(otherConnections.isEmpty()) {
							continue;
						}
						
						LinkWires lw = null;
						for(Connection connection : otherConnections) {
							if(connection instanceof PortConnection) {
								if(lw != null && lw != connection.getLinkWires()) {
									throw new IllegalStateException("How is this remotely possible?!");
								}
								
								lw = connection.getLinkWires();
							} else if(connection instanceof WireConnection) {
								Wire wire = (Wire)connection.getParent();
								if(connection == wire.getStartConnection()
									   || connection == wire.getEndConnection()) {
									if(lw != null && lw != connection.getLinkWires()) {
										throw new IllegalStateException("How is this remotely possible?!");
									}
									
									lw = connection.getLinkWires();
								}
							}
						}
						
						linkWires = lw;
					}
					
					Set<ComponentPeer<?>> components = new HashSet<>(getComponents());
					components.addAll(moveElements.stream()
					                              .filter(e -> e instanceof ComponentPeer)
					                              .map(e -> (ComponentPeer<?>)e)
					                              .collect(Collectors.toSet()));
					
					Pair<Set<Wire>, Set<Point>> pair = PathFinding.bestPath(sx, sy, x, y, (px, py, horizontal) -> {
						if(px == x && py == y) {
							return LocationPreference.VALID;
						}
						
						if(px == sx && py == sy) {
							return LocationPreference.VALID;
						}
						
						Set<Connection> connections =
							Stream.concat(connectedPorts.stream(),
							              paths.stream().flatMap(w -> w.getConnections().stream()))
							      .filter(c -> c.getX() == px && c.getY() == py)
							      .collect(Collectors.toSet());
						synchronized(CircuitBoard.this) {
							connections.addAll(getConnections(px, py));
						}
						
						for(Connection connection : connections) {
							if(connection instanceof PortConnection) {
								return LocationPreference.INVALID;
							}
							
							if(connection.getLinkWires() == null
								   || linkWires == null
								   || connection.getLinkWires() == linkWires) {
								return LocationPreference.PREFER;
							}
							
							if(connection instanceof WireConnection) {
								Wire wire = (Wire)connection.getParent();
								if(wire.isHorizontal() == horizontal
									   || connection == wire.getStartConnection()
									   || connection == wire.getEndConnection()) {
									return LocationPreference.INVALID;
								}
							}
						}
						
						for(ComponentPeer<?> component : components) {
							if(component.contains(px, py)) {
								return LocationPreference.INVALID;
							}
						}
						
						return LocationPreference.VALID;
					});
					if(pair != null) {
						paths.addAll(pair.getKey());
					}
				}
				
				synchronized(CircuitBoard.this) {
					Set<Wire> toRemove = new HashSet<>();
					Set<Wire> toAdd = new HashSet<>();
					
					for(Wire newWire : paths) {
						if(wireAlreadyExists(newWire) != null) {
							toRemove.add(newWire);
						} else {
							boolean wasRemoved = false;
							
							for(LinkWires wires : links) {
								for(Wire existing : wires.getWires()) {
									if(existing.getLinkWires() == newWire.getLinkWires()
										   && existing.isHorizontal() == newWire.isHorizontal()) {
										if(existing.equals(newWire)) {
											wasRemoved = true;
											toRemove.add(existing);
											break;
										} else if(existing.isWithin(newWire)) {
											wasRemoved = true;
											toAdd.addAll(spliceWire(newWire, existing));
											toRemove.add(existing);
											break;
										} else if(newWire.isWithin(existing)) {
											wasRemoved = true;
											toAdd.addAll(spliceWire(existing, newWire));
											toRemove.add(newWire);
											break;
										} else if(existing.overlaps(newWire)) {
											Pair<Wire, Pair<Wire, Wire>> pairs =
												spliceOverlappingWire(newWire, existing);
											
											toAdd.add(pairs.getKey());
											toRemove.add(pairs.getValue().getKey());
											
											wasRemoved = true;
											break;
										}
									}
								}
							}
							
							if(!wasRemoved) {
								toAdd.add(newWire);
							}
						}
					}
					
					moveResult = new MoveComputeResult(toAdd, toRemove);
					if(computeThread == Thread.currentThread()) {
						computeThread = null;
					}
					
					lastException = null;
					
					circuitManager.setNeedsRepaint();
				}
				
				latch.countDown();
			});
			computeThread.start();
			
			lastException = new Exception("Computing...");
		}
		
		try {
			latch.await(20, TimeUnit.MILLISECONDS);
		} catch(InterruptedException e) {
			// ignore
		}
	}
	
	/**
	 * Returns the new set of selected elements by wires with actual ones they overlap.
	 * <p>
	 * Note only the ComponentPeers have their coordinates updated after this call. Wires will reset back to their
	 * original coordinates, thus the need for the returned set of updated wires.
	 */
	public Set<GuiElement> finalizeMove() {
		if(moveElements == null) {
			return null;
		}
		
		if(!addMoveAction) {
			editHistory.beginGroup();
		}
		
		MoveComputeResult result;
		
		synchronized(this) {
			if(computeThread != null) {
				computeThread.interrupt();
				computeThread = null;
			}
			
			result = moveResult;
			moveResult = null;
		}
		
		Set<Wire> wiresToAdd = result == null ? new HashSet<>() : result.wiresToAdd;
		Set<Wire> wiresRemoved = result == null ? new HashSet<>() : result.wiresToRemove;
		
		boolean cannotMoveHere = false;
		
		for(GuiElement element : moveElements) {
			if((element instanceof ComponentPeer<?> && !isValidLocation((ComponentPeer<?>)element)) ||
				   (element instanceof Wire && (element.getX() < 0 || element.getY() < 0))) {
				for(GuiElement element1 : moveElements) {
					element1.setX(element1.getX() - moveDeltaX);
					element1.setY(element1.getY() - moveDeltaY);
				}
				
				wiresToAdd.clear();
				wiresRemoved.clear();
				
				cannotMoveHere = true;
				break;
			}
		}
		
		removeElements(wiresRemoved);
		
		List<GuiElement> elements = new ArrayList<>(moveElements.size() + wiresToAdd.size());
		elements.addAll(moveElements);
		elements.addAll(wiresToAdd);
		
		Set<Wire> selectedWires = new HashSet<>();
		
		List<RuntimeException> toThrow = new ArrayList<>();
		
		boolean reset = cannotMoveHere;
		circuit.getSimulator().runSync(() -> {
			for(GuiElement element : elements) {
				if(element instanceof ComponentPeer<?>) {
					ComponentPeer<?> component = (ComponentPeer<?>)element;
					
					try {
						editHistory.beginGroup();
						editHistory.addAction(EditAction.MOVE_ELEMENT, circuitManager, component, moveDeltaX,
						                      moveDeltaY);
						addComponent(component, true);
					} catch(RuntimeException exc) {
						editHistory.clearGroup();
						toThrow.clear();
						toThrow.add(exc);
					} finally {
						editHistory.endGroup();
					}
				} else if(element instanceof Wire) {
					Wire wire = (Wire)element;
					try {
						editHistory.beginGroup();
						addWire(wire.getX(), wire.getY(), wire.getLength(), wire.isHorizontal());
						
						if(!reset) {
							// Make a copy of the wire for later use
							if(!wiresToAdd.contains(wire)) {
								selectedWires.add(new Wire(null, wire));
								
								// Things break in undo/redo if we don't reset wires back
								wire.setX(wire.getX() - moveDeltaX);
								wire.setY(wire.getY() - moveDeltaY);
							}
						}
					} catch(RuntimeException exc) {
						editHistory.clearGroup();
						toThrow.clear();
						toThrow.add(exc);
					} finally {
						editHistory.endGroup();
					}
				}
			}
		});
		
		Set<GuiElement> newSelectedElements;
		
		if(!cannotMoveHere) {
			for(GuiElement element : elements) {
				if(element instanceof ComponentPeer<?>) {
					ComponentPeer<?> component = (ComponentPeer<?>)element;
					// moving components doesn't actually modify the Circuit, so we must trigger the listener directly
					circuitManager.getSimulatorWindow().circuitModified(circuit, component.getComponent(), true);
				}
			}
			
			if(addMoveAction && moveDeltaX == 0 && moveDeltaY == 0) {
				editHistory.clearGroup();
			}
			
			newSelectedElements = new HashSet<>();
			moveElements.forEach(element -> {
				if(element instanceof ComponentPeer) {
					newSelectedElements.add(element);
				}
			});
			if(!selectedWires.isEmpty()) {
				links.forEach(
					linkWires ->
						linkWires.getWires().forEach(wire -> {
							if(selectedWires.contains(wire)) {
								newSelectedElements.add(wire);
							} else {
								selectedWires.forEach(selectedWire -> {
									if(selectedWire.overlaps(wire)) {
										newSelectedElements.add(wire);
									}
								});
							}
						}));
			}
		} else {
			newSelectedElements = null;
			
			if(addMoveAction) {
				editHistory.clearGroup();
			}
		}
		
		// Closes the beginGroup in initMove or in the beginning of this function
		editHistory.endGroup();
		
		moveElements = null;
		wiresToAdd.clear();
		connectedPorts.clear();
		moveDeltaX = 0;
		moveDeltaY = 0;
		
		updateBadLinks();
		
		if(cannotMoveHere) {
			throw new SimulationException("Cannot move components/wires here.");
		}
		
		if(!toThrow.isEmpty()) {
			throw toThrow.get(0);
		}
		
		return newSelectedElements;
	}
	
	public void removeElements(Set<? extends GuiElement> elements) {
		removeElements(elements, true);
	}
	
	synchronized void removeElements(Set<? extends GuiElement> elements, boolean removeFromCircuit) {
		circuit.getSimulator().runSync(() -> {
			try {
				editHistory.beginGroup();
				
				Map<LinkWires, Set<Wire>> wiresToRemove = new HashMap<>();
				
				Set<GuiElement> elementsToRemove = new HashSet<>(elements);
				
				while(!elementsToRemove.isEmpty()) {
					Iterator<GuiElement> iterator = elementsToRemove.iterator();
					GuiElement element = iterator.next();
					iterator.remove();
					
					if(element instanceof ComponentPeer<?>) {
						removeComponent((ComponentPeer<?>)element, removeFromCircuit);
						if(removeFromCircuit) {
							circuit.removeComponent(((ComponentPeer<?>)element).getComponent());
						}
					} else if(element instanceof Wire) {
						Wire wire = (Wire)element;
						
						Set<Wire> toRemove = new HashSet<>();
						for(int i = 0; i < wire.getLength(); i++) {
							int x = wire.isHorizontal() ? wire.getX() + i : wire.getX();
							int y = wire.isHorizontal() ? wire.getY() : wire.getY() + i;
							for(Connection conn : new HashSet<>(getConnections(x, y))) {
								if(conn instanceof WireConnection) {
									Wire w = (Wire)conn.getParent();
									if(w.isHorizontal() == wire.isHorizontal()) {
										if(w.equals(wire)) {
											toRemove.add(w);
											break;
										} else if(w.isWithin(wire)) {
											elementsToRemove.addAll(spliceWire(wire, w));
											
											toRemove.add(w);
											break;
										} else if(wire.isWithin(w)) {
											LinkWires linkWires = w.getLinkWires();
											removeWire(w);
											
											spliceWire(w, wire).forEach(w1 -> addWire(linkWires, w1));
											Wire clone = new Wire(wire);
											addWire(linkWires, clone);
											
											toRemove.add(clone);
											break;
										} else if(w.overlaps(wire)) {
											LinkWires linkWires = w.getLinkWires();
											removeWire(w);
											
											Pair<Wire, Pair<Wire, Wire>> pairs = spliceOverlappingWire(wire, w);
											elementsToRemove.add(pairs.getKey());
											addWire(linkWires, pairs.getValue().getKey());
											addWire(linkWires, pairs.getValue().getValue());
											
											toRemove.add(pairs.getValue().getKey());
											break;
										}
									}
								}
							}
						}
						
						toRemove.forEach(w -> {
							w.getConnections().forEach(this::removeConnection);
							
							LinkWires linkWires = w.getLinkWires();
							Set<Wire> set = wiresToRemove.containsKey(linkWires)
							                ? wiresToRemove.get(linkWires)
							                : new HashSet<>();
							set.add(w);
							wiresToRemove.put(linkWires, set);
						});
					}
				}
				
				wiresToRemove.forEach((linkWires, wires) -> {
					links.remove(linkWires);
					links.addAll(linkWires.splitWires(wires));
					
					wires.forEach(
						(wire) -> editHistory.addAction(EditAction.REMOVE_WIRE, circuitManager, new Wire(null, wire)));
				});
				
				rejoinWires();
				
				updateBadLinks();
			} finally {
				editHistory.endGroup();
			}
		});
	}
	
	private Set<Wire> spliceWire(Wire toSplice, Wire within) {
		if(!within.isWithin(toSplice)) throw new IllegalArgumentException("toSplice must contain within");
		
		Set<Wire> wires = new HashSet<>();
		
		if(toSplice.isHorizontal()) {
			if(toSplice.getX() < within.getX()) {
				wires.add(new Wire(toSplice.getLinkWires(),
				                   toSplice.getX(), toSplice.getY(), within.getX() - toSplice.getX(), true));
			}
			
			int withinEnd = within.getX() + within.getLength();
			int toSpliceEnd = toSplice.getX() + toSplice.getLength();
			if(withinEnd < toSpliceEnd) {
				wires.add(new Wire(toSplice.getLinkWires(),
				                   withinEnd, toSplice.getY(), toSpliceEnd - withinEnd, true));
			}
		} else {
			if(toSplice.getY() < within.getY()) {
				wires.add(new Wire(toSplice.getLinkWires(),
				                   toSplice.getX(), toSplice.getY(), within.getY() - toSplice.getY(), false));
			}
			
			int withinEnd = within.getY() + within.getLength();
			int toSpliceEnd = toSplice.getY() + toSplice.getLength();
			if(withinEnd < toSpliceEnd) {
				wires.add(new Wire(toSplice.getLinkWires(),
				                   toSplice.getX(), withinEnd, toSpliceEnd - withinEnd, false));
			}
		}
		
		return wires;
	}
	
	// returns (overlap leftover, (toSplice pieces))
	private Pair<Wire, Pair<Wire, Wire>> spliceOverlappingWire(Wire toSplice, Wire overlap) {
		if(!toSplice.overlaps(overlap)) throw new IllegalArgumentException("wires must overlap");
		
		if(toSplice.isHorizontal()) {
			Wire left = toSplice.getX() < overlap.getX() ? toSplice : overlap;
			Wire right = toSplice.getX() < overlap.getX() ? overlap : toSplice;
			
			Wire leftPiece =
				new Wire(
					left.getLinkWires(),
					left.getX(),
					left.getY(),
					right.getX() - left.getX(),
					true);
			Wire midPiece =
				new Wire(right.getLinkWires(),
				         right.getX(),
				         right.getY(),
				         left.getX() + left.getLength() - right.getX(),
				         true);
			Wire rightPiece =
				new Wire(right.getLinkWires(),
				         left.getX() + left.getLength(),
				         left.getY(),
				         right.getX() + right.getLength() - left.getX() - left.getLength(),
				         true);
			
			if(left == toSplice) {
				return new Pair<>(leftPiece, new Pair<>(midPiece, rightPiece));
			} else {
				return new Pair<>(rightPiece, new Pair<>(midPiece, leftPiece));
			}
		} else {
			Wire top = toSplice.getY() < overlap.getY() ? toSplice : overlap;
			Wire bottom = toSplice.getY() < overlap.getY() ? overlap : toSplice;
			
			Wire topPiece =
				new Wire(
					top.getLinkWires(),
					top.getX(), top.getY(),
					bottom.getY() - top.getY(),
					false);
			Wire midPiece =
				new Wire(
					bottom.getLinkWires(),
					bottom.getX(),
					bottom.getY(),
					
					top.getY() + top.getLength() - bottom.getY(),
					false);
			Wire bottomPiece =
				new Wire(
					bottom.getLinkWires(),
					top.getX(),
					top.getY() + top.getLength(),
					bottom.getY() + bottom.getLength() - top.getY() - top.getLength(),
					false);
			
			if(top == toSplice) {
				return new Pair<>(topPiece, new Pair<>(midPiece, bottomPiece));
			} else {
				return new Pair<>(bottomPiece, new Pair<>(midPiece, topPiece));
			}
		}
	}
	
	public synchronized void addWire(int x, int y, int length, boolean horizontal) {
		if(x < 0 || y < 0 || (horizontal && x + length < 0) || (!horizontal && y + length < 0)) {
			throw new SimulationException("Wire cannot go into negative space.");
		}
		
		if(length == 0) {
			throw new SimulationException("Length cannot be 0");
		}
		
		circuit.getSimulator().runSync(() -> {
			try {
				editHistory.beginGroup();
				
				LinkWires linkWires = new LinkWires();
				
				Set<Wire> wiresAdded = new LinkedHashSet<>();
				
				// these are wires that would be split in half
				Map<Wire, Connection> toSplit = new HashMap<>();
				
				{
					Set<Connection> connections = getConnections(x, y);
					if(connections != null) {
						for(Connection connection : connections) {
							handleConnection(connection, linkWires);
							
							GuiElement parent = connection.getParent();
							if(connection instanceof WireConnection) {
								Wire wire = (Wire)parent;
								if(connection != wire.getStartConnection() && connection != wire.getEndConnection()) {
									toSplit.put(wire, connection);
								}
							}
						}
					}
				}
				
				int lastX = x;
				int lastY = y;
				
				int sign = length / Math.abs(length);
				for(int i = sign; Math.abs(i) <= Math.abs(length); i += sign) {
					int xOff = horizontal ? i : 0;
					int yOff = horizontal ? 0 : i;
					Connection currConnection = findConnection(x + xOff, y + yOff);
					
					if(currConnection != null &&
						   (i == length || currConnection instanceof PortConnection ||
							    currConnection == ((Wire)currConnection.getParent()).getStartConnection() ||
							    currConnection == ((Wire)currConnection.getParent()).getEndConnection())) {
						int len = horizontal ? currConnection.getX() - lastX
						                     : currConnection.getY() - lastY;
						Wire wire = new Wire(linkWires, lastX, lastY, len, horizontal);
						Wire surrounding = wireAlreadyExists(wire);
						if(surrounding == null) {
							wiresAdded.add(wire);
						}
						
						Set<Connection> connections = i == length ? getConnections(x + xOff, y + yOff)
						                                          : Collections.singleton(currConnection);
						
						for(Connection connection : connections) {
							GuiElement parent = connection.getParent();
							if(connection instanceof WireConnection) {
								Wire connWire = (Wire)parent;
								if(connection != connWire.getStartConnection() &&
									   connection != connWire.getEndConnection()) {
									toSplit.put((Wire)parent, connection);
								}
							}
							
							handleConnection(connection, linkWires);
						}
						
						lastX = currConnection.getX();
						lastY = currConnection.getY();
					} else if(i == length) {
						int len = horizontal ? x + xOff - lastX
						                     : y + yOff - lastY;
						Wire wire = new Wire(linkWires, lastX, lastY, len, horizontal);
						Wire surrounding = wireAlreadyExists(wire);
						if(surrounding == null) {
							wiresAdded.add(wire);
						}
						
						lastX = x + xOff;
						lastY = y + yOff;
					}
				}
				
				for(Wire wire : wiresAdded) {
					addWire(linkWires, wire);
				}
				
				toSplit.forEach(this::splitWire);
				
				rejoinWires();
				
				updateBadLinks();
			} finally {
				editHistory.endGroup();
			}
		});
	}
	
	private Wire wireAlreadyExists(Wire wire) {
		Set<Connection> connections = connectionsMap.get(new Pair<>(wire.getX(), wire.getY()));
		if(connections == null || connections.isEmpty()) {
			return null;
		}
		
		for(Connection connection : connections) {
			if(connection instanceof WireConnection) {
				Wire existing = (Wire)connection.getParent();
				if(wire.isWithin(existing)) {
					return existing;
				}
			}
		}
		
		return null;
	}
	
	private void splitWire(Wire wire, Connection connection) {
		LinkWires links = wire.getLinkWires();
		removeWire(wire);
		
		int len = connection.getX() == wire.getX() ? connection.getY() - wire.getY()
		                                           : connection.getX() - wire.getX();
		Wire wire1 = new Wire(links, wire.getX(), wire.getY(), len, wire.isHorizontal());
		addWire(links, wire1);
		
		Wire wire2 = new Wire(links, connection.getX(), connection.getY(), wire.getLength() - len,
		                      wire.isHorizontal());
		addWire(links, wire2);
	}
	
	private void addWire(LinkWires linkWires, Wire wire) {
		linkWires.addWire(wire);
		links.add(linkWires);
		wire.getConnections().forEach(this::addConnection);
		
		editHistory.addAction(EditAction.ADD_WIRE, circuitManager, wire);
	}
	
	private void removeWire(Wire wire) {
		wire.getConnections().forEach(this::removeConnection);
		
		LinkWires linkWires = wire.getLinkWires();
		if(linkWires == null) {
			return;
		}
		
		linkWires.removeWire(wire);
		
		editHistory.addAction(EditAction.REMOVE_WIRE, circuitManager, new Wire(null, wire));
	}
	
	// For EditHistory usage, so rejoinWires will only need to be called once.
	private boolean rejoinWiresEnabled = true;
	
	void disableRejoinWires() {
		rejoinWiresEnabled = false;
	}
	
	void enableRejoinWires() {
		rejoinWiresEnabled = true;
		rejoinWires();
	}
	
	private synchronized void rejoinWires() {
		if(!rejoinWiresEnabled) {
			return;
		}
		
		editHistory.disable();
		
		try {
			for(LinkWires linkWires : links) {
				Set<Wire> removed = new HashSet<>();
				ArrayList<Wire> wires = new ArrayList<>(linkWires.getWires());
				for(int i = 0; i < wires.size(); i++) {
					Wire wire = wires.get(i);
					
					if(removed.contains(wire)) continue;
					
					Connection start = wire.getStartConnection();
					Connection end = wire.getEndConnection();
					
					int x = wire.getX();
					int y = wire.getY();
					int length = wire.getLength();
					
					Set<Connection> startConns = getConnections(start.getX(), start.getY());
					if(startConns != null && startConns.size() == 2) {
						List<Wire> startWires = startConns.stream()
						                                  .filter(
							                                  conn -> conn != start && conn instanceof WireConnection)
						                                  .map(conn -> (Wire)conn.getParent())
						                                  .filter(w -> w.isHorizontal() == wire.isHorizontal())
						                                  .collect(Collectors.toList());
						
						if(startWires.size() == 1) {
							Wire startWire = startWires.get(0);
							length += startWire.getLength();
							
							if(startWire.getX() < x) {
								x = startWire.getX();
							}
							if(startWire.getY() < y) {
								y = startWire.getY();
							}
							
							removeWire(startWire);
							removed.add(startWire);
						}
					}
					
					Set<Connection> endConns = getConnections(end.getX(), end.getY());
					if(endConns != null && endConns.size() == 2) {
						List<Wire> endWires = endConns.stream()
						                              .filter(conn -> conn != end && conn instanceof WireConnection)
						                              .map(conn -> (Wire)conn.getParent())
						                              .filter(w -> w.isHorizontal() == wire.isHorizontal())
						                              .collect(Collectors.toList());
						
						if(endWires.size() == 1) {
							Wire endWire = endWires.get(0);
							length += endWire.getLength();
							
							removeWire(endWire);
							removed.add(endWire);
						}
					}
					
					if(length != wire.getLength()) {
						removeWire(wire);
						removed.add(wire);
						Wire newWire = new Wire(linkWires, x, y, length, wire.isHorizontal());
						addWire(linkWires, newWire);
						wires.add(newWire);
					}
				}
			}
		} finally {
			editHistory.enable();
		}
	}
	
	private void removeComponent(ComponentPeer<?> component, boolean removeFromComponentsList) {
		if(!components.contains(component)) {
			return;
		}
		
		for(Connection connection : component.getConnections()) {
			removeConnection(connection);
			
			PortConnection portConnection = (PortConnection)connection;
			LinkWires linkWires = portConnection.getLinkWires();
			if(linkWires != null) {
				linkWires.removePort(portConnection);
				if(linkWires.isEmpty()) {
					linkWires.clear();
					links.remove(linkWires);
				}
			}
		}
		
		if(removeFromComponentsList) {
			components.remove(component);
		}
		
		editHistory.addAction(EditAction.REMOVE_COMPONENT, circuitManager, component);
	}
	
	private void handleConnection(Connection connection, LinkWires linkWires) {
		LinkWires linksToMerge = connection.getLinkWires();
		if(linksToMerge == null) {
			if(connection instanceof PortConnection) {
				linkWires.addPort((PortConnection)connection);
			} else if(connection instanceof WireConnection) {
				linkWires.addWire((Wire)connection.getParent());
			}
		} else if(linkWires != linksToMerge) {
			links.remove(linksToMerge);
			linkWires.merge(linksToMerge);
		}
		
		links.add(linkWires);
	}
	
	public Connection findConnection(int x, int y) {
		Pair<Integer, Integer> pair = new Pair<>(x, y);
		return connectionsMap.containsKey(pair) ? connectionsMap.get(pair).iterator().next() : null;
	}
	
	public Set<Connection> getConnections(int x, int y) {
		Pair<Integer, Integer> pair = new Pair<>(x, y);
		return connectionsMap.getOrDefault(pair, Collections.emptySet());
	}
	
	public void paint(GraphicsContext graphics, LinkWires highlightLinkWires) {
		CircuitState currentState = new CircuitState(this.currentState);
		
		components.forEach(component -> {
			if(moveElements == null || !moveElements.contains(component)) {
				paintComponent(graphics, currentState, component);
			}
		});
		
		for(LinkWires linkWires : links) {
			for(Wire wire : linkWires.getWires()) {
				paintWire(graphics, currentState, wire, linkWires == highlightLinkWires);
			}
		}
		
		if(badLinks != null) {
			for(LinkWires badLink : badLinks) {
				Stream.concat(badLink.getPorts().stream(),
				              badLink.getInvalidPorts().stream()).forEach(port -> {
					graphics.setFill(Color.BLACK);
					graphics.fillText(String.valueOf(port.getPort().getLink().getBitSize()),
					                  port.getScreenX() + 11,
					                  port.getScreenY() + 21);
					
					graphics.setStroke(Color.ORANGE);
					graphics.setFill(Color.ORANGE);
					graphics.strokeOval(port.getScreenX() - 2, port.getScreenY() - 2, 10, 10);
					graphics.fillText(String.valueOf(port.getPort().getLink().getBitSize()),
					                  port.getScreenX() + 10,
					                  port.getScreenY() + 20);
				});
			}
		}
		
		if(moveElements != null) {
			graphics.save();
			graphics.setGlobalAlpha(0.5);
			
			for(GuiElement element : moveElements) {
				if(element instanceof ComponentPeer<?>) {
					paintComponent(graphics, currentState, (ComponentPeer<?>)element);
				} else if(element instanceof Wire) {
					paintWire(graphics, currentState, (Wire)element, false);
				}
			}
			
			MoveComputeResult result = this.moveResult;
			if(result != null) {
				graphics.setFill(Color.RED);
				result.wiresToRemove.forEach(wire -> wire.paint(graphics));
				
				graphics.setFill(Color.BLACK);
				result.wiresToAdd.forEach(wire -> wire.paint(graphics));
			}
			
			graphics.restore();
		}
	}
	
	private void paintComponent(GraphicsContext graphics, CircuitState state, ComponentPeer<?> component) {
		graphics.save();
		component.paint(graphics, state);
		graphics.restore();
		
		for(PortConnection connection : component.getConnections()) {
			connection.paint(graphics, state);
		}
	}
	
	private void paintWire(GraphicsContext graphics, CircuitState state, Wire wire, boolean highlight) {
		graphics.save();
		wire.paint(graphics, state, highlight ? 4.0 : 2.0);
		graphics.restore();
		
		Connection startConn = wire.getStartConnection();
		if(getConnections(startConn.getX(), startConn.getY()).size() > 2) {
			startConn.paint(graphics, state);
		}
		Connection endConn = wire.getStartConnection();
		if(getConnections(endConn.getX(), endConn.getY()).size() > 2) {
			endConn.paint(graphics, state);
		}
	}
	
	private synchronized void addConnection(Connection connection) {
		Pair<Integer, Integer> pair = new Pair<>(connection.getX(), connection.getY());
		Set<Connection> set = connectionsMap.containsKey(pair) ? connectionsMap.get(pair) : new HashSet<>();
		set.add(connection);
		connectionsMap.put(pair, set);
	}
	
	private synchronized void removeConnection(Connection connection) {
		Pair<Integer, Integer> pair = new Pair<>(connection.getX(), connection.getY());
		if(!connectionsMap.containsKey(pair)) {
			return;
		}
		Set<Connection> set = connectionsMap.get(pair);
		set.remove(connection);
		if(set.isEmpty()) {
			connectionsMap.remove(pair);
		}
	}
}