/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: AStarMaster.java
 * Written by: Christian Julg, Jonas Thedering (Team 1)
 *
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 *
 * Electric(tm) is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.tool.routing.experimentalAStar1;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.tool.routing.RoutingFrame.RoutePoint;
import com.sun.electric.tool.routing.RoutingFrame.RouteWire;
import com.sun.electric.tool.routing.RoutingFrame.RoutingContact;
import com.sun.electric.tool.routing.RoutingFrame.RoutingGeometry;
import com.sun.electric.tool.routing.RoutingFrame.RoutingLayer;
import com.sun.electric.tool.routing.RoutingFrame.RoutingSegment;

/** 
 * Creates the jobs for the worker threads and processes the results
 * 
 * @author Jonas Thedering
 * @author Christian Jülg
 */
public class AStarMaster {

	private boolean DEBUG = false;

	// Displacement to map the given coordinates to the field array
	private final int dispX;
	private final int dispY;

	private ExecutorCompletionService<Net> netCompletionService;

	private List<Net> netList;
	private List<Net> unroutedNetList;
	private List<Net> activeNetList = new LinkedList<Net>();

	private List<ObjectPool<Node>> nodePools;
	private List<ObjectPool<Storage>> storagePools;

	private RoutingLayer[] metalLayers;
	private RoutingContact[] metalPins;

	private List<RoutingContact> allContacts;

	private final Map map;

	private int poolSize;

	private int totalNumPaths;
	
	private int threadCount;

	private long shutdownTime;

	private int unroutableNetCount = 0;
	private int unroutablePathCount = 0;

	public AStarMaster(ExecutorService service, Map map, List<ObjectPool<Node>> nodePools, List<ObjectPool<Storage>> storagePools, RoutingLayer[] metalLayers,
			RoutingContact[] metalPins, int threadCount, long shutDownTime) {

		netCompletionService = new ExecutorCompletionService<Net>(service);

		this.nodePools = nodePools;
		this.storagePools = storagePools;
		this.map = map;

		this.metalLayers = metalLayers;
		this.metalPins = metalPins;

		this.threadCount = threadCount;

		this.dispX = map.getDispX();
		this.dispY = map.getDispY();

		this.poolSize = nodePools.size();

		this.shutdownTime = shutDownTime;

		DEBUG &= AStarRoutingFrame.getInstance().isOutputEnabled();
	}

	private void run() {
		long startTime = 0;
		long masterTimeActiveStart = 0;
		long masterTimeWaitingStart = 0;
		long masterTimeActiveSum = 0;
		long masterTimeWaitingSum = 0;
		if (DEBUG) {
			long currentTime = System.currentTimeMillis();
			masterTimeActiveStart = currentTime;
			startTime = currentTime;
		}

		int invalidLength = 0;
		int invalidPathSum = 0;

		int completedNetCount = 0;
		int completedPathCount = 0;
		int invalidNetSum = 0;
		
		EndPointMarker marker = new EndPointMarker(map);
		marker.markStartAndFinish(netList);
		
		processUnroutables();

		// Schedule one net per thread at the start
		for (int i = 0; i < threadCount; ++i) {
			scheduleUnroutedNet();
		}
		
		boolean shutdown = false;

		// The "main loop", which waits for finishing workers and creates new jobs
		while (completedNetCount < netList.size() && !(activeNetList.isEmpty() && shutdown)) {
			try {
				if (DEBUG) {
					long currentTime = System.currentTimeMillis();
					masterTimeActiveSum += currentTime - masterTimeActiveStart;
					masterTimeWaitingStart = currentTime;
				}
				Future<Net> future = netCompletionService.take();

				Net net = future.get();

				if (DEBUG) {
					long currentTime = System.currentTimeMillis();
					masterTimeWaitingSum += currentTime - masterTimeWaitingStart; 
					masterTimeActiveStart = currentTime;
				}
				
				// if there are fewer Nets than threads left, 
				// remove a nodepool to free up memory
				while (netList.size() - completedNetCount < poolSize) {
					synchronized (nodePools) {
						if(DEBUG)
							System.out.println("Master: removed a nodepool!");
						nodePools.remove(0);
						storagePools.remove(0);
						poolSize--;
					}
				}
				
				List<Path> paths = net.getPaths();
				boolean invalidFound = false;
				boolean[] pathInvalid = new boolean[paths.size()];
				boolean unroutablePathFound = false;

				// Check all paths routed for validity
				for (int pathIdx = 0; pathIdx < paths.size(); pathIdx++) {
					Path path = paths.get(pathIdx);
					
					if (path.pathDone) {
						// this path was already entered into the map!
						continue;
					}
					
					// No route between the endpoints was found for this path
					if (path.nodesX == null || path.pathUnroutable) {
						if(DEBUG) {
							System.err.printf("Net \"%s\": Path routing failure! totalCost %d, Path will be left untouched.\n", path.segment.getNetName(), path.totalCost);
						}
						if (!unroutablePathFound){
							++unroutableNetCount;
							unroutablePathFound = true;
						}
						
						unroutablePathCount++;
						
						// ignore this path from now on!
						path.pathUnroutable = true;
						path.pathDone = true;
						net.pathDone[pathIdx] = true;

						continue;
					}

					for (int i = 0; i < path.nodesX.length; ++i) {
						int status = map.getStatus(path.nodesX[i], path.nodesY[i], path.nodesZ[i]);
						if (status != Map.CLEAR && status != net.getNetID()) {
							// Path is invalid because it's using blocked nodes
							invalidFound = true;
							pathInvalid[pathIdx] = true;
							break;
						}
					}
					// only invalid paths need to be recomputed
					if (!pathInvalid[pathIdx]) {
						net.pathDone[pathIdx] = true;
						path.pathDone = true;

						// The path is ok, so we set the new blockages
						map.setStatus(path.nodesX, path.nodesY, path.nodesZ, net.getNetID());
					}
				}
				
				shutdown = System.currentTimeMillis() > shutdownTime;
				
				if (invalidFound) {
					// There were invalid (but not unroutable) paths, so reroute them
					++invalidNetSum;
					
					if (DEBUG){
						int netInvalidLength =0;
						int invalidPathCount = 0;
						for (Path p: paths) {
							if (!p.pathDone && !p.pathUnroutable) {
								netInvalidLength += p.totalCost;
								invalidPathCount++;
							}
						}
						invalidPathSum += invalidPathCount;
						invalidLength += netInvalidLength;
						System.out.printf("AStarMaster: Net %3d contains %d invalid paths, rerouting it, invalid length: %d (net est.: %4d), invalid nets: %d, completed:%d/%d\n", net.getElectricNetID(), invalidPathCount, netInvalidLength, net.getLengthEstimate(), invalidNetSum,
								completedNetCount, netList.size());
					}
					
					if (!shutdown) {
						Goal goal = new Goal(net, map);

						AStarWorker worker = new AStarWorker(net, nodePools, storagePools, map, goal, shutdownTime);
						// Do it again
						netCompletionService.submit(worker);
					} else {
						// need to shutdown, so finish valid paths in this net
						for (Path path : paths) {
							if (path.pathDone && !path.pathUnroutable) {
								routeSegment(path);
							}
							++completedPathCount;
						}

						++completedNetCount;

						activeNetList.remove(net);

						if (DEBUG) {
							System.out.printf("AStarMaster: shutdown now, time after shutdown threshold:%dms (Net %3d is complete, completed nets:%3d/%d, completed paths:%3d/%d)\n", System.currentTimeMillis() - shutdownTime, net.getElectricNetID(), completedNetCount, netList.size(), completedPathCount, totalNumPaths);
						}
					}

				} else {
					// All paths were valid/unroutable, so finish this net and schedule the next
					for (Path path : paths) {
						if (path.pathDone && !path.pathUnroutable) {
							routeSegment(path);
						}
						++completedPathCount;
					}

					++completedNetCount;

					if (DEBUG)
						System.out.printf("AStarMaster: Net %3d is complete, completed nets:%3d/%d, completed paths:%3d/%d\n", net.getElectricNetID(), completedNetCount, netList.size(), completedPathCount, totalNumPaths);
					
					activeNetList.remove(net);
					
					if (!shutdown) {
						scheduleUnroutedNet();
					}
				}

			} catch (InterruptedException e) {
				System.err.printf("AStarMaster: Caught exception in Worker:\n%s\n", e.toString());
				e.getCause().printStackTrace();
				return;
			} catch (ExecutionException e) {
				System.err.printf("AStarMaster: Caught exception in Worker:\n%s\n", e.toString());
				e.getCause().printStackTrace();
				return;
			}
		}	
		
		assert activeNetList.isEmpty();

		if (DEBUG) {
			
			long currentTime = System.currentTimeMillis();
			System.out.printf("\nAStarMaster: Routing completed after %d ms, invalid/completed nets: %d/%d (%.1f %%), invalid/completed paths: %d/%d (%.1f %%)\n",
					currentTime - startTime, invalidNetSum, completedNetCount, (100f * invalidNetSum)
					/ (completedNetCount), invalidPathSum, completedPathCount, (100f * invalidPathSum)
					/ (completedPathCount));

			masterTimeActiveSum += currentTime - masterTimeActiveStart; 
			System.out.printf("AStarMaster: Master thread waiting time: %d, active time: %d (%.1f%%)\n", 
					masterTimeWaitingSum, masterTimeActiveSum, masterTimeActiveSum*100f / (currentTime - startTime));
		}
		
		if (DEBUG) {
			if (unroutableNetCount > 0)
				System.err.printf("Error: %d nets not routed, containing %d paths\n", unroutableNetCount, unroutablePathCount);
			evaluateRouting(invalidLength);
		}
	}
	
	/**
	 * looks at all Paths and marks all that were set pathUnroutable by EndpointMarker to pathDone, 
	 * so that they will be ignored by Worker
	 */
	private void processUnroutables() {
		for (Net net : netList) {

			List<Path> paths = net.getPaths();
			boolean unroutablePathFound = false;

			// Check all paths routed for validity
			for (int pathIdx = 0; pathIdx < paths.size(); pathIdx++) {
				Path path = paths.get(pathIdx);
					
				// DEBUG: this should not happen, pathDone should not be set before this loop
				 assert !path.pathDone : "processUnroutables(): pathDone was already set!";

				// No route between the endpoints was found for this path
				if (path.pathUnroutable) {
					if (DEBUG) {
						System.err.printf("Net \"%s\": Path unroutable, Path will be left untouched.\n", path.segment.getNetName());
					}
					if (!unroutablePathFound) {
						++unroutableNetCount;
						unroutablePathFound = true;
					}

					unroutablePathCount++;

					// ignore this path from now on!
					path.pathUnroutable = true;
					path.pathDone = true;
					net.pathDone[pathIdx] = true;
				}
			}
		}
	}

	/** Starts the execution of a net from the unrouted list, moving it to the active list */
	private void scheduleUnroutedNet() {
		if(unroutedNetList.isEmpty())
			return;
		
		long startTime = 0;
		if(DEBUG)
			startTime = System.nanoTime();
		
		// Determine the net with least overlap to the currently routed
		Net net = null;
		int minSum = Integer.MAX_VALUE;
		for(Net unroutedNet : unroutedNetList) {
			int sum = 0;
			for(Net activeNet : activeNetList) {
				sum += activeNet.getOverlapSum(unroutedNet);
			}
			if(sum < minSum) {
				minSum = sum;
				net = unroutedNet;
				
				if(minSum == 0)
					break;
			}
		}
		
		if(DEBUG) {
			if(minSum > 0)
				System.out.printf("AStarMaster: Net %d scheduled with overlap %d after %.3f ms\n", net.getElectricNetID(), minSum, (System.nanoTime()-startTime)/1e6f);
		}
		
		unroutedNetList.remove(net);
		activeNetList.add(net);
		
		Goal goal = new Goal(net, map);

		AStarWorker worker = new AStarWorker(net, nodePools, storagePools, map, goal, shutdownTime);
		netCompletionService.submit(worker);
	}

	/**
	 * Method to do routing
	 * 
	 * @param segmentsToRoute
	 *            a list of all routes that need to be made.
	 * @param allLayers
	 *            a list of all layers that can be used in routing.
	 * @param allContacts
	 *            a list of all contacts that can be used in routing.
	 * @param blockages
	 *            a list of all blockage geometry to consider.
	 */
	protected void runRouting(Cell cell, List<RoutingSegment> segmentsToRoute, List<RoutingLayer> allLayers, List<RoutingContact> allContacts,
			List<RoutingGeometry> blockages) {

		netList = new ArrayList<Net>();

		HashMap<Integer, Net> netMap = new HashMap<Integer, Net>();
		
		this.totalNumPaths = segmentsToRoute.size();

		// Create nets to combine the given segments
		for (RoutingSegment rs : segmentsToRoute) {
			int netID = rs.getNetID();
			Net net = netMap.get(netID);

			if (net == null) {
				net = new Net(rs.getNetID());
				netMap.put(rs.getNetID(), net);

				netList.add(net);
			}

			net.getPaths().add(new Path(rs, dispX, dispY, map.getScalingFactor()));
		}

		sortNets();
		
		unroutedNetList = new LinkedList<Net>(netList);

		this.allContacts = allContacts;

		run();
	}

	/** Calculate some statistic information about the routing process */
	private void evaluateRouting(int invalidLength) {
		int sum = 0;
		int estimate = 0;
		int netSum, netEstimate;
		int diff; //difference between optimum and actual cost
		for (Net net: netList) {
			netSum = 0;
			for (Path p: net.getPaths()) {
				if (p.pathDone && !p.pathUnroutable) {
					netSum += p.totalCost;
				}
			}
			sum += netSum;
			netEstimate = net.getRoutableLengthEstimate();
			estimate += netEstimate;
		}
		diff = sum-estimate;
		System.out.printf("total routing cost: %d, estimated: %d, diff: %d (%.02f%%), invalid length: %d (%.02f%%)\n", sum, estimate, diff, diff*100f/estimate, invalidLength, invalidLength*100f/sum);
	}

	/** Sort the nets by length to improve invalid routing costs */
	private void sortNets() {
		long start = 0;
		if (DEBUG) {
			start = System.currentTimeMillis();
		}

		Collections.sort(netList, new Comparator<Net>() {
			public int compare(Net n1, Net n2) {
				int len1 = n1.getLengthEstimate();
				int len2 = n2.getLengthEstimate();

				return (len1 - len2);
			}
		});

		if (DEBUG) {
			System.out.printf("Master: sortNets() took %d ms for %d nets\n", System.currentTimeMillis() - start, netList.size());
		}
	}

	/** Finally insert the path into the Electric data structures */
	private void routeSegment(Path path) {
		/*
		long start;
		if (DEBUG) {
			start = System.currentTimeMillis();
		}
		*/

		RoutingSegment segment = path.segment;
		int[] nodesX = path.nodesX;
		int[] nodesY = path.nodesY;
		int[] nodesZ = path.nodesZ;

		Point2D startLocation = segment.getStartEnd().getLocation();

		Point2D finishLocation = segment.getFinishEnd().getLocation();

		// Now that we have the result, we convert the path to wires and
		// wire ends and add them to the segment

		// Special handling for the start point
		RoutePoint previous = new RoutePoint(RoutingContact.STARTPOINT, startLocation, 0);
		segment.addWireEnd(previous);

		RoutingLayer layer = metalLayers[nodesZ[0]];
		RoutingContact contact = metalPins[nodesZ[0]];

		double scalingFactor = map.getScalingFactor();
		
		// Determine quadrant of start / finish point
		boolean startRight = path.startRight;
		boolean startAbove = path.startAbove;
		boolean finishRight = path.finishRight;
		boolean finishAbove = path.finishAbove;
		
		// A complicated scheme follows that carefully handles routing at the endpoints
		// special handling for all Paths shorter than 4 needed to avoid Notch errors
		if(nodesX.length < 4) {
			// Short nets need special care to avoid notch errors
			assert nodesX.length >= 1 : nodesX.length;
			
			//0: no connection, 1: L, 2: horizontal Z, 3: vertical Z, 4: M
			int connectionMode;
			
			if(nodesX.length == 1) {
				// Diagonal?
				if(startRight != finishRight && startAbove != finishAbove) {
					// Make Z connection
					connectionMode = 2;
				}
				else {
					// Make L connection
					connectionMode = 1;
				}
			}
			else if(nodesX.length == 2) {
				boolean horizontal = nodesY[0] == nodesY[1];
				boolean vertical = nodesX[0] == nodesX[1];
				boolean startInside = (horizontal && ((nodesX[0] < nodesX[1]) == startRight))
					|| (vertical && ((nodesY[0] < nodesY[1]) == startAbove));
				boolean finishInside = (horizontal && ((nodesX[0] > nodesX[1]) == finishRight))
				|| (vertical && ((nodesY[0] > nodesY[1]) == finishAbove));
				boolean sameSide = (vertical && (startRight == finishRight))
					|| (horizontal && (startAbove == finishAbove));
				
				if(startInside && finishInside || sameSide) {
					// Make L connection
					connectionMode = 1;
				}
				else {
					// Make Z connection
					if(horizontal)
						connectionMode = 2;
					else
						connectionMode = 3;
				}
			}
			else {
				assert nodesX.length == 3 : nodesX.length;
				if(nodesX[0] == nodesX[2])
					// Make Z connection
					connectionMode = 3;
				else if(nodesY[0] == nodesY[2])
					// Make Z connection
					connectionMode = 2;
				else {
					// Not straight
					int quadrantStartX = (int)((startLocation.getX() + dispX) / (scalingFactor / 2));
					int quadrantStartY = (int)((startLocation.getY() + dispY) / (scalingFactor / 2));
					int quadrantFinishX = (int)((finishLocation.getX() + dispX) / (scalingFactor / 2));
					int quadrantFinishY = (int)((finishLocation.getY() + dispY) / (scalingFactor / 2));
					double middleX = getUnscaledCoordinate(nodesX[1], dispX, scalingFactor);
					double middleY = getUnscaledCoordinate(nodesY[1], dispY, scalingFactor);
					if(Math.abs(quadrantStartX - quadrantFinishX) <= 1
						|| Math.abs(quadrantStartY - quadrantFinishY) <= 1) {
						// Make L connection
						connectionMode = 1;
					}
					else if(Math.abs(startLocation.getX() - middleX) <= scalingFactor
						|| Math.abs(finishLocation.getX() - middleX) <= scalingFactor) {
						// Make Z connection
						connectionMode = 3;
					}
					else if(Math.abs(startLocation.getY() - middleY) <= scalingFactor
						|| Math.abs(finishLocation.getY() - middleY) <= scalingFactor) {
						// Make Z connection
						connectionMode = 2;
					}
					else {
						// Make M connection
						connectionMode = 4;
					}
				}
			}
			
			if(connectionMode == 1) {
				// Make L connection
				RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), finishLocation.getY()), 0);
				segment.addWireEnd(point);
	
				RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
				segment.addWire(wire);
	
				previous = point;
			}
			else if(connectionMode == 2) {
				// Make horizontal Z connection
				double pointY = getUnscaledCoordinate((nodesX.length == 3 ? nodesY[1] : nodesY[0]), dispY, scalingFactor);
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), pointY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), pointY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if(connectionMode == 3) {
				// Make vertical Z connection
				double pointX = getUnscaledCoordinate((nodesX.length == 3 ? nodesX[1] : nodesX[0]), dispX, scalingFactor);
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(pointX, startLocation.getY()), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(pointX, finishLocation.getY()), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if(connectionMode == 4) {
				// Make M connection
				assert nodesX.length == 3;
				double pointX = getUnscaledCoordinate(nodesX[1], dispX, scalingFactor);
				double pointY = getUnscaledCoordinate(nodesY[1], dispY, scalingFactor);
				{
					RoutePoint point;
					if(nodesX[0] == nodesX[1])
						point = new RoutePoint(contact, new Point2D.Double(pointX, startLocation.getY()), 0);
					else
						point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), pointY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(pointX, pointY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				{
					RoutePoint point;
					if(nodesX[2] == nodesX[1])
						point = new RoutePoint(contact, new Point2D.Double(pointX, finishLocation.getY()), 0);
					else
						point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), pointY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
		}
		else { // nodesX.length >= 4
			int startOuterX = startRight ? nodesX[0]-1 : nodesX[0]+1;
			int startInnerX = startRight ? nodesX[0]+1 : nodesX[0]-1;
			int startOuterY = startAbove ? nodesY[0]-1 : nodesY[0]+1;
			int startInnerY = startAbove ? nodesY[0]+1 : nodesY[0]-1;
			int finishOuterX = finishRight ? nodesX[nodesX.length-1]-1 : nodesX[nodesX.length-1]+1;
			int finishInnerX = finishRight ? nodesX[nodesX.length-1]+1 : nodesX[nodesX.length-1]-1;
			int finishOuterY = finishAbove ? nodesY[nodesX.length-1]-1 : nodesY[nodesX.length-1]+1;
			int finishInnerY = finishAbove ? nodesY[nodesX.length-1]+1 : nodesY[nodesX.length-1]-1;
			
			double startGridX = getUnscaledCoordinate(nodesX[0], dispX, scalingFactor);
			double startGridY = getUnscaledCoordinate(nodesY[0], dispY, scalingFactor);
			double startInnerGridY = getUnscaledCoordinate(startInnerY, dispY, scalingFactor);
			double finishGridX = getUnscaledCoordinate(nodesX[nodesX.length-1], dispX, scalingFactor);
			double finishGridY = getUnscaledCoordinate(nodesY[nodesX.length-1], dispY, scalingFactor);
			double finishInnerGridY = getUnscaledCoordinate(finishInnerY, dispY, scalingFactor);
			
			if(nodesX[1] == startOuterX) {
				// Vertical or via?
				if(nodesX[2] == nodesX[1]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), startGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if(nodesY[1] == startOuterY) {
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), startGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				
				// Horizontal or via?
				if(nodesY[2] == nodesY[1]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startGridX, startGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if(nodesX[1] == startInnerX) {
				if(nodesY[2] == startInnerY) {
					if(nodesY[3] != nodesY[2]) {
						RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), startInnerGridY), 0);
						segment.addWireEnd(point);
			
						RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
						segment.addWire(wire);
			
						previous = point;
					}
				}
				// Vertical or via?
				else if(nodesX[2] == nodesX[1]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), startGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if (nodesY[1] == startInnerY) {
				// Vertical or via?
				if(nodesX[2] == nodesX[1]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), startInnerGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else {
				assert nodesZ[1] != nodesZ[0];
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startLocation.getX(), startGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(startGridX, startGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			
			// Iterate over all intermediate positions
			for (int i = 1; i < nodesX.length - 1; ++i) {
				// look out for layer changes
				if (nodesZ[i - 1] == nodesZ[i]) {
	
					if (i > 1 && i < (nodesX.length - 2)) {
	
						// If this is part of a long line, don't create a redundant
						// route point
						int x = nodesX[i];
						if (x == nodesX[i - 1] && x == nodesX[i + 1])
							continue;
	
						int y = nodesY[i];
						if (y == nodesY[i - 1] && y == nodesY[i + 1])
							continue;
					}
	
					// connection is on same layer
					contact = metalPins[nodesZ[i]];
				} else {
					contact = getVia(metalLayers[nodesZ[i - 1]], metalLayers[nodesZ[i]]);
				}

				 // SCALE!
				double pointX = getUnscaledCoordinate(nodesX[i], dispX, scalingFactor);
				double pointY = getUnscaledCoordinate(nodesY[i], dispY, scalingFactor);
				if(i == 1) {
					if(nodesX[1] == startOuterX) {
						// Horizontal?
						if(nodesX[2] != nodesX[1]) {
							pointX = startLocation.getX();
						}
					}
					else if(nodesY[1] == startOuterY) {
						// Vertical?
						if(nodesY[2] != nodesY[1]) {
							pointY = startGridY;
						}
					}
					else if(nodesX[1] == startInnerX) {
						if(nodesY[2] == startInnerY) {
							// Vertical?
							if(nodesY[3] != nodesY[2]) {
								pointY = startInnerGridY;
							}
							else {
								pointX = startLocation.getX();
								pointY = startInnerGridY;
							}
						}
						// Horizontal?
						else if(nodesX[2] != nodesX[1]) {
							pointX = startLocation.getX();
						}
					}
					else if (nodesY[1] == startInnerY) {
						// Horizontal?
						if(nodesX[2] != nodesX[1]) {
							pointX = startLocation.getX();
						}
					}
				}
				else if(i == (nodesX.length - 2)) {
					if(nodesX[nodesX.length-2] == finishOuterX) {
						// Horizontal?
						if(nodesX[nodesX.length-3] != nodesX[nodesX.length-2]) {
							pointX = finishLocation.getX();
						}
					}
					else if(nodesY[nodesX.length-2] == finishOuterY) {
						// Vertical?
						if(nodesY[nodesX.length-3] != nodesY[nodesX.length-2]) {
							pointY = finishGridY;
						}
					}
					else if(nodesX[nodesX.length-2] == finishInnerX) {
						if(nodesY[nodesX.length-3] == finishInnerY) {
							// Vertical?
							if(nodesY[nodesX.length-4] != nodesY[nodesX.length-3]) {
								pointY = finishInnerGridY;
							}
							else {
								pointX = finishLocation.getX();
								pointY = finishInnerGridY;
							}
						}
						// Horizontal?
						else if(nodesX[nodesX.length-3] != nodesX[nodesX.length-2]) {
							pointX = finishLocation.getX();
						}
					}
					else if (nodesY[nodesX.length-2] == finishInnerY) {
						// Horizontal?
						if(nodesX[nodesX.length-3] != nodesX[nodesX.length-2]) {
							pointX = finishLocation.getX();
						}
					}
				}
				RoutePoint point = new RoutePoint(contact, new Point2D.Double(pointX, pointY), 0);
				segment.addWireEnd(point);
	
				RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
				segment.addWire(wire);
	
				layer = metalLayers[nodesZ[i]];
	
				previous = point;
			}
			
			layer = metalLayers[nodesZ[nodesX.length-1]];
			contact = metalPins[nodesZ[nodesX.length-1]];
			
			// Special handling for the finish point
			if(nodesX[nodesX.length-2] == finishOuterX) {
				// Vertical or via?
				if(nodesX[nodesX.length-3] == nodesX[nodesX.length-2]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), finishGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if(nodesY[nodesX.length-2] == finishOuterY) {
				// Horizontal or via?
				if(nodesY[nodesX.length-3] == nodesY[nodesX.length-2]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishGridX, finishGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), finishGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if(nodesX[nodesX.length-2] == finishInnerX) {
				if(nodesY[nodesX.length-3] == finishInnerY) {
					if(nodesY[nodesX.length-4] != nodesY[nodesX.length-3]) {
						RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), finishInnerGridY), 0);
						segment.addWireEnd(point);
			
						RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
						segment.addWire(wire);
			
						previous = point;
					}
				}
				// Vertical or via?
				else if(nodesX[nodesX.length-3] == nodesX[nodesX.length-2]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), finishGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else if (nodesY[nodesX.length-2] == finishInnerY) {
				// Vertical or via?
				if(nodesX[nodesX.length-3] == nodesX[nodesX.length-2]) {
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), finishInnerGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
			else {
				assert nodesZ[nodesX.length-2] != nodesZ[nodesX.length-1];
				{
					RoutePoint point = new RoutePoint(getVia(metalLayers[nodesZ[nodesX.length-2]], metalLayers[nodesZ[nodesX.length-1]]), new Point2D.Double(finishGridX, finishGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(metalLayers[nodesZ[nodesX.length-2]], previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
				{
					RoutePoint point = new RoutePoint(contact, new Point2D.Double(finishLocation.getX(), finishGridY), 0);
					segment.addWireEnd(point);
		
					RouteWire wire = new RouteWire(layer, previous, point, layer.getMinWidth());
					segment.addWire(wire);
		
					previous = point;
				}
			}
		}

		RoutePoint finishPoint = new RoutePoint(RoutingContact.FINISHPOINT, finishLocation, 0);
		segment.addWireEnd(finishPoint);

		RouteWire wire = new RouteWire(layer, previous, finishPoint, layer.getMinWidth());
		segment.addWire(wire);

		/*
		if (DEBUG) {
			System.out.printf("Master: routeSegment(Path %s) took %d ms\n", path.segment.getNetName(), System.currentTimeMillis() - start);
		}
		*/
	}

	/**
	 * @return Coordinate inversely scaled, corrected by dispX or dispY and shifted to the middle of the "grid"
	 */
	private double getUnscaledCoordinate(int scaledCoord, int disp, double scalingFactor) {
		double shift = scalingFactor / 2d;

		return (scaledCoord*scalingFactor) - disp + shift;
	}

	/** @return The routing contact to route between the given layers */
	private RoutingContact getVia(RoutingLayer l1, RoutingLayer l2) {
		for (RoutingContact rc : allContacts) {
			if ((rc.getFirstLayer().equals(l1) && rc.getSecondLayer().equals(l2)) || (rc.getFirstLayer().equals(l2) && rc.getSecondLayer().equals(l1))) {
				return rc;
			}
		}
		return null;
	}
}
