/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: AStarWorker.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.util.List;
import java.util.concurrent.Callable;

/** 
 * Does the actual A* routing for the given net
 * 
 * @author Jonas Thedering
 * @author Christian Jülg
 */
public class AStarWorker implements Callable<Net> {

	private boolean DEBUG = false;

	private Storage storage;

	// only used by run(), use nodePool instead
	private List<ObjectPool<Node>> nodePools;
	private List<ObjectPool<Storage>> storagePools;
	private ObjectPool<Node> nodePool;
	private ObjectPool<Storage> storagePool;
	
	private Net net; // contains paths

	private final Map map;
	private Goal goal;

	private long shutdownTime;

	public AStarWorker(Net net, List<ObjectPool<Node>> nodePools, 
		List<ObjectPool<Storage>> storagePools, Map map, Goal goal, long shutdownTime) {
		this.nodePools = nodePools;
		this.storagePools = storagePools;
		this.net = net;
		this.map = map;
		this.goal = goal;
		this.shutdownTime = shutdownTime;
		
		DEBUG &= AStarRoutingFrame.getInstance().isOutputEnabled();
	}

	/** Connects a child for the given position to the parent, if passable */
	private void linkChild(int x, int y, int z, Node parent) {
		if (x < 0 || x >= map.getWidth() || y < 0 || y >= map.getHeight() || z < 0 || z >= map.getLayers())
			return;
		
		if (goal.isTileOK(parent.x, parent.y, parent.z, x, y, z)) {
			int g = parent.g + goal.getTileCost(parent.x, parent.y, parent.z, x, y, z);

			Node child = storage.get(x, y, z);
			if (child != null) {
				parent.children[parent.childCount++] = child;

				if (g < child.g) {
					child.parent = parent;
					child.g = g;
					if(storage.isNodeInOpen(child))
						storage.decreaseCost(child, g + child.h);
					else
						updateChildren(child);
				}
				return;
			}

			// No node existing for that position, so create new one
			child = nodePool.alloc();
			child.initialize(x, y, z);
			child.parent = parent;
			child.g = g;
			child.h = goal.distanceToGoal(x, y, z);
			child.f = (g + child.h);
			parent.children[parent.childCount++] = child;
			storage.addToOpen(child);
		}
	}

	/** Walks through the children of the given node to update their cost */
	private void updateChildren(Node parent) {
		for (int i = 0; i < parent.childCount; ++i) {
			Node child = parent.children[i];

			int g = parent.g
					+ goal.getTileCost(parent.x, parent.y, parent.z, child.x, child.y, child.z);
			if (g < child.g) {
				child.g = g;
				child.parent = parent;

				if(storage.isNodeInOpen(child))
					storage.decreaseCost(child, g + child.h);
				else
					updateChildren(child);
			}
		}
	}

	public Net call() throws Exception {
		
		//sync on nodepools, is unsynced list
		synchronized (nodePools) {
			storagePool = storagePools.remove(0);

			assert nodePools.size() > 0;
			this.nodePool = nodePools.remove(0);
		}
		storage = storagePool.alloc();
		
		int index;
		// Skip paths already routed
		for (index=0; index<net.pathDone.length; index++){
			if (!net.pathDone[index]){
				break;
			}
		}
		
		boolean shutdown = System.currentTimeMillis() > shutdownTime;
		
		// Iterates over the not yet routed paths of the net
		while(!goal.isRoutingComplete() && !shutdown) {
			storage.initialize(map.getWidth(), map.getHeight(), map.getLayers());

			Node startNode = nodePool.alloc();
			int[] startLoc = goal.getNextStart(); // returns an int[] {x,y,z}
			startNode.initialize(startLoc[0], startLoc[1], startLoc[2]);

			startNode.g = 0;
			startNode.h = goal.distanceToGoal(startNode.x, startNode.y, startNode.z);
			startNode.f = startNode.h;
			// If the start node is invalid, don't start the algorithm
			int status = map.getStatus(startNode.x, startNode.y, startNode.z);
			assert status != Map.CLEAR : "Start position not marked!";
			if (status == net.getNetID())
				storage.addToOpen(startNode);

			Node finishNode = null;
			int count = 0;
			long startTime=0;
			if(DEBUG)
				startTime = System.currentTimeMillis();

			// run A* until finish is found or all nodes inspected
			while (!storage.isOpenEmpty()) {
				++count;
				Node current = storage.shiftCheapestNode();
				
				int x = current.x;
				int y = current.y;
				int z = current.z;

				if (goal.isFinishPosition(x, y, z)) {
					finishNode = current;
					break;
				}
				
				linkChild(x, y, z - 1, current);
				linkChild(x, y, z + 1, current);
				
				int spaceAround = map.getSpaceAround(x, y, z);
				
				if(spaceAround == 0) {
					// We are in unknown territory, move conservatively
					linkChild(x - 1, y, z, current);
					linkChild(x + 1, y, z, current);
					linkChild(x, y - 1, z, current);
					linkChild(x, y + 1, z, current);
				}
				else {
					int spaceDiff = spaceAround-1;
					int offX = x % spaceAround;
					int offY = y % spaceAround;
					
					boolean xInside = offX > 0 && offX < spaceDiff;
					boolean yInside = offY > 0 && offY < spaceDiff;
					
					if(xInside && yInside) {
						// We are in the middle of clear space, move to the margins
						linkChild(x - offX, y, z, current);
						linkChild(x - offX + spaceDiff, y, z, current);
						linkChild(x, y - offY, z, current);
						linkChild(x, y - offY + spaceDiff, z, current);
					}
					else {
						// We are at a margin, move by 1 along the margin and to the outside,
						// jump to the margin on the other side 
						if(offX == spaceDiff && yInside)
							linkChild(x - spaceDiff, y, z, current);
						else
							linkChild(x - 1, y, z, current);
						
						if(offX == 0 && yInside)
							linkChild(x + spaceDiff, y, z, current);
						else
							linkChild(x + 1, y, z, current);
						
						if(offY == spaceDiff && xInside)
							linkChild(x, y - spaceDiff, z, current);
						else
							linkChild(x, y - 1, z, current);
						
						if(offY == 0 && xInside)
							linkChild(x, y + spaceDiff, z, current);
						else
							linkChild(x, y + 1, z, current);
					}
				}
			}

			// get Path to enter routing result
			Path path = net.getPaths().get(index);
			path.initialize(); // only segment set before initialize
			if(DEBUG) {
				long time = System.currentTimeMillis()-startTime;
				long speed = -2;
				if(time > 0){
					speed = count / time;
				} else{
					speed = (count>0)? 0 : -1;
				}
				System.out.printf("AStarWorker: %8d iterations in %4d ms (%4d it/ms) for path in \"%s\"\n", count, time, speed, path.segment.getNetName());
			}
			
			if (finishNode != null) {
				// Path was routed successfully, create the nodesX/Y/Z arrays by backtracking from the finish node
				
				int pathLength = 1;
				{
					Node current = finishNode;
					while (current != startNode) {
						pathLength += Math.abs(current.x - current.parent.x)
							+ Math.abs(current.y - current.parent.y)
							+ Math.abs(current.z - current.parent.z);
						current = current.parent;
					}
				}
				
				path.totalCost = finishNode.g;

				path.nodesX = new int[pathLength];
				path.nodesY = new int[pathLength];
				path.nodesZ = new int[pathLength];

				Node current = finishNode;
				int i = pathLength - 1;
				while (current != startNode) {
					if(current.x < current.parent.x)
						for(int x = current.x; x < current.parent.x; ++x) {
							path.nodesX[i] = x;
							path.nodesY[i] = current.y;
							path.nodesZ[i] = current.z;
							--i;
						}
					else if(current.x > current.parent.x)
						for(int x = current.x; x > current.parent.x; --x) {
							path.nodesX[i] = x;
							path.nodesY[i] = current.y;
							path.nodesZ[i] = current.z;
							--i;
						}
					else if(current.y < current.parent.y)
						for(int y = current.y; y < current.parent.y; ++y) {
							path.nodesX[i] = current.x;
							path.nodesY[i] = y;
							path.nodesZ[i] = current.z;
							--i;
						}
					else if(current.y > current.parent.y)
						for(int y = current.y; y > current.parent.y; --y) {
							path.nodesX[i] = current.x;
							path.nodesY[i] = y;
							path.nodesZ[i] = current.z;
							--i;
						}
					else if(current.z < current.parent.z)
						for(int z = current.z; z < current.parent.z; ++z) {
							path.nodesX[i] = current.x;
							path.nodesY[i] = current.y;
							path.nodesZ[i] = z;
							--i;
						}
					else if(current.z > current.parent.z)
						for(int z = current.z; z > current.parent.z; --z) {
							path.nodesX[i] = current.x;
							path.nodesY[i] = current.y;
							path.nodesZ[i] = z;
							--i;
						}
					else
						assert false;
					
					// nodesX/Y will be translated to cellGrid in runRouting
					// translating here confuses AStarMaster
					current = current.parent;
				}
				
				path.nodesX[0] = startNode.x;
				path.nodesY[0] = startNode.y;
				path.nodesZ[0] = startNode.z;
			} else {
				// path was unroutable, set path.totalCost to improve error messages in Master
				
				path.totalCost = count;
				int finishStatus = goal.getFinishPositionStatus();
				if (finishStatus != net.getNetID()){
					//finish is marked incorrectly
					path.totalCost = -2;
				}
			}
			
			// Return "borrowed" node objects
			storage.freeNodes(nodePool);
			
			// Skip paths already routed
			for (index=index+1; index<net.getPaths().size(); index++){
				if (!net.pathDone[index]){
					break;
				}
			}
			
			shutdown = System.currentTimeMillis() > shutdownTime;
		}
		
		if (DEBUG && shutdown) {
			System.out.printf("AStarWorker: shutdown now\n");
		}

		storagePool.free(storage);
		
		// sync on nodepools, is unsynced list
		synchronized (nodePools) {
			nodePools.add(nodePool);
			storagePools.add(storagePool);
		}

		return net;
	}
}
