/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: DetailedRouterWorker.java
 * Written by: Alexander Herzog, Martin Fietz (Team 4)
 *
 * 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.experimentalLeeMoore2;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import com.sun.electric.tool.routing.RoutingFrame.RoutingLayer;
import com.sun.electric.tool.routing.experimentalLeeMoore2.DetailedRouter.DetailedRoutingSolution;
import com.sun.electric.tool.routing.experimentalLeeMoore2.GlobalRouterV3.RegionToRoute;
import com.sun.electric.tool.routing.experimentalLeeMoore2.RoutingFrameLeeMoore.Coordinate;
import com.sun.electric.tool.routing.experimentalLeeMoore2.RoutingFrameLeeMoore.ManhattenAlignment;

public final class DetailedRouterWorker implements Runnable {

	/** field */
	private MazeField mf;
	private final double offsetX;
	private final double offsetY;
	private final int offsetZ;
	private final int lengthX;
	private final int lengthY;
	private final int lengthZ;

	private static boolean enableOutput = false;

	final static int HORIZONTAL = 1;
	final static int VERTICAL = 0;

	private RegionToRoute region;
	private final DetailedRoutingSolution allSolutions;
	private final DetailedRoutingSolution curSolutions;

	private final double tileSize;
	private double wireSpacing;

	private boolean isDone;

	private static ManhattenAlignment aStart;
	private static ManhattenAlignment aFinish;

	public DetailedRouterWorker(RegionToRoute region,
			RoutingLayer[] metalLayers, double tileSize) {
		this.tileSize = tileSize;
		wireSpacing = Double.MIN_VALUE;
		for (RoutingLayer lay : metalLayers) {
			if (null == lay) {
				continue;
			}
			wireSpacing = Math.max(lay.getMinSpacing(lay), wireSpacing);
		}
		offsetX = region.bounds.getMinX();
		offsetY = region.bounds.getMinY();
		offsetZ = 1;
		lengthX = (int) Math.round(region.bounds.getWidth() / tileSize) + 1;
		lengthY = (int) Math.round(region.bounds.getHeight() / tileSize) + 1;
		lengthZ = metalLayers.length - 1;
		allSolutions = new DetailedRoutingSolution();
		curSolutions = new DetailedRoutingSolution();
	}

	public void setRegion(RegionToRoute region) {
		this.region = region;
	}

	public void run() {
		isDone = false;
		curSolutions.clear();
		List<SegPart> unrouted = region.segments_to_route;
		if (unrouted.size() > 0) {
			mf = new MazeField(lengthX, lengthY, lengthZ);
			mf.resetAll();

			for (List<Coordinate> solution : allSolutions.values()) {
				//mf.addBlockage(toMazePoint(solution));
				mf.addBlockage(getPointsToBlock(toMazePoint(solution)));
			}

			// reserve start- and end-point tiles
			for (SegPart segs : unrouted) {
				MazePoint start = toMazePoint(segs.segment_part.get(0));
				MazePoint finish = toMazePoint(segs.segment_part.get(1));
				if (mf.canBeReserved(start) && mf.canBeReserved(finish)) {
					mf.reserve(start);
					mf.reserve(finish);
				}
			}

			for (SegPart sp : unrouted) {
				mf.reset();
				MazePoint start = toMazePoint(sp.segment_part.get(0));
				aStart = sp.segment_part.get(0).alignment;
				MazePoint finish = toMazePoint(sp.segment_part.get(1));
				aFinish = sp.segment_part.get(1).alignment;
				if (mf.getValue(start) == MazeField.POINT_RESERVED
						&& mf.getValue(finish) == MazeField.POINT_RESERVED
						&& mf.setStart(start) && mf.setFinish(finish)) {
					if (mf.wavefront()) {
						debug("1");
						List<MazePoint> ws = mf.backtracking();
						//mf.addBlockage(ws);
						mf.addBlockage(getPointsToBlock(ws));
						curSolutions.put(sp, toCoordinate((ws)));
					} else {
						debug("0");
					}
				} else {
					debug("?");
				}
			}
		}
		mf = null; // let the garbage collector do its magic
		isDone = true;
	}
	
	private List<MazePoint> getPointsToBlock( List<MazePoint> lmp ) {
		List<MazePoint> result = new ArrayList<MazePoint>();
		for( MazePoint mp : lmp ) {
			result.add(mp);
		}
		
		if( lmp.size() > 0 && mf.contains(lmp.get(0)) ) {
			result.add(lmp.get(0));
		}
		if( lmp.size() > 0 && mf.contains(lmp.get(lmp.size() - 1)) ) {
			result.add(lmp.get(lmp.size() - 1));
		}
		return result;
	}

	private static void debug(String s) {
		if (enableOutput) {
			System.out.print(s);
		}
	}

	public void enableOutput() {
		enableOutput = true;
	}

	public DetailedRoutingSolution getSolution() {
		return this.curSolutions;
	}

	public void removeSolutions(List<Integer> ids) {
		for (Iterator<Map.Entry<SegPart, List<Coordinate>>> it = curSolutions
				.entrySet().iterator(); it.hasNext();) {
			Map.Entry<SegPart, List<Coordinate>> s = it.next();
			if (ids.contains(s.getKey().id)) {
				it.remove();
			} else {
				allSolutions.put(s.getKey(), s.getValue());
			}
		}
	}

	public boolean isDone() {
		return isDone;
	}

	private List<MazePoint> toMazePoint(final List<Coordinate> lc) {
		List<MazePoint> lm = new ArrayList<MazePoint>(lc.size());
		for (Coordinate c : lc)
			lm.add(toMazePoint(c));
		return lm;
	}

	private MazePoint toMazePoint(Coordinate c) {
		int x = (int) Math.floor((c.x - offsetX + 0.5 * tileSize) / tileSize);
		int y = (int) Math.floor((c.y - offsetY + 0.5 * tileSize) / tileSize);
		int z = c.layer - offsetZ;
		return new MazePoint(x, y, z);
	}

	private List<Coordinate> toCoordinate(final List<MazePoint> lm) {
		List<Coordinate> result = new ArrayList<Coordinate>(lm.size());
		for (MazePoint mp : lm) {
			double x = mp.x * tileSize + offsetX;
			double y = mp.y * tileSize + offsetY;
			int z = mp.z + offsetZ;
			result.add(new Coordinate(x, y, z));
		}
		return result;
	}

	public final static class MazePoint {
		public final int x, y, z;

		public MazePoint(int i, int j, int z) {
			this.x = i;
			this.y = j;
			this.z = z;
		}
	}

	public final static class MazeField {
		/** POINT VALUES */
		private final static int POINT_UNDEFINED = 0;
		private final static int POINT_START = -1;
		private final static int POINT_FINISH = -2;
		private final static int POINT_BLOCK = -4;
		private final static int POINT_BORDER = -8;
		private final static int POINT_RESERVED = -16;

		private final int[][][] field;

		private MazePoint start;
		private MazePoint finish;

		public MazeField(int sizeX, int sizeY, int sizeZ) {
			field = new int[sizeX][sizeY][sizeZ];
		}

		public MazeField reset() {
			for (int x = 0; x < field.length; x++) {
				for (int y = 0; y < field[x].length; y++) {
					for (int z = 0; z < field[x][y].length; z++) {
						if (getValue(x, y, z) != POINT_BLOCK
								&& getValue(x, y, z) != POINT_BORDER
								&& getValue(x, y, z) != POINT_RESERVED) {
							setValue(x, y, z, POINT_UNDEFINED);
						}
					}
				}
			}
			if (start != null && getValue(start) != POINT_BLOCK) {
				setValue(start, POINT_BORDER);
			}
			if (finish != null && getValue(finish) != POINT_BLOCK) {
				setValue(finish, POINT_BORDER);
			}
			return this;
		}

		public MazeField resetAll() {
			for (int x = 0; x < field.length; x++) {
				for (int y = 0; y < field[x].length; y++) {
					for (int z = 0; z < field[x][y].length; z++) {
						if (x == 0 || x == field.length - 1 || y == 0
								|| y == field[x].length - 1) {
							setValue(x, y, z, POINT_BORDER);
						} else {
							setValue(x, y, z, POINT_UNDEFINED);
						}
					}
				}
			}
			return this;
		}

		public boolean setStart(MazePoint mp) {
			if (contains(mp)
					&& (getValue(mp) == POINT_BORDER
							|| getValue(mp) == POINT_UNDEFINED || getValue(mp) == POINT_RESERVED)) {
				start = mp;
				setValue(mp, POINT_START);
				return true;
			} else {
				return false;
			}
		}

		public boolean reserve(MazePoint mp) {
			if (contains(mp)
					&& (getValue(mp) == POINT_BORDER || getValue(mp) == POINT_UNDEFINED)) {
				setValue(mp, POINT_RESERVED );
				return true;
			} else {
				return false;
			}
		}

		public boolean canBeReserved(MazePoint mp) {
			if (contains(mp)) {
				return (getValue(mp) == POINT_BORDER || getValue(mp) == POINT_UNDEFINED);
			} else {
				return false;
			}
		}

		public boolean setFinish(MazePoint mp) {
			if (contains(mp)
					&& (getValue(mp) == POINT_BORDER
							|| getValue(mp) == POINT_UNDEFINED || getValue(mp) == POINT_RESERVED)) {
				finish = mp;
				setValue(mp, POINT_FINISH);
				return true;
			} else {
				return false;
			}
		}

		public void addBlockage(List<MazePoint> blockList) {
			for (MazePoint block : blockList)
				addBlockage(block);
		}

		public void addBlockage(MazePoint mp) {
			addBlockage(mp.x, mp.y, mp.z);
		}

		public void addBlockage(int x, int y, int z) {
			setValue(x, y, z, POINT_BLOCK);
		}

		public void setValue(MazePoint mp, int i) {
			setValue(mp.x, mp.y, mp.z, i);
		}

		public void setValue(int x, int y, int z, int value) {
			if (contains(x, y, z)) {
				field[x][y][z] = value;
			}
		}

		public int getValue(final MazePoint mp) {
			return getValue(mp.x, mp.y, mp.z);
		}

		public int getDistance(MazePoint m, MazePoint n) {
			return Math.abs(m.x - n.x) + Math.abs(m.y - n.y)
					+ Math.abs(m.z - n.z);
		}

		public int getValue(int x, int y, int z) {
			if (contains(x, y, z)) {
				return field[x][y][z];
			} else {
				return POINT_BLOCK;
			}
		}

		public List<MazePoint> getNeighbours(MazePoint mp) {
			return getNeighbours(mp.x, mp.y, mp.z);
		}

		public List<MazePoint> getNeighbours(int x, int y, int z) {
			ArrayList<MazePoint> neighbours = new ArrayList<MazePoint>();
			/*int[] dx, dy;
			if ((z + 1) % 2 == 0) // even layer
			{
				dx = new int[] { 0, 0, 0, 0 };
				dy = new int[] { -1, 1, 0, 0 };
			} else // odd layer
			{
				dx = new int[] { -1, 1, 0, 0 };
				dy = new int[] { 0, 0, 0, 0 };
			}
			int[] dz = { 0, 0, -1, 1 }; */
			int dx[] = { -1, 1, 0, 0, 0, 0 };
			int dy[] = { 0, 0, -1, 1, 0, 0 };
			int dz[] = { 0, 0, 0, 0, -1, 1 };
			for (int i = 0; i < dx.length; i++) {
				MazePoint n = new MazePoint(x + dx[i], y + dy[i], z + dz[i]);
				if (contains(n)) {
					neighbours.add(n);
				}
			}
			return neighbours;
		}

		private boolean contains(MazePoint mp) {
			return contains(mp.x, mp.y, mp.z);
		}

		private boolean contains(int x, int y, int z) {
			if (0 <= x && x < field.length && 0 <= y && y < field[x].length
					&& 0 <= z && z < field[x][y].length)
				return true;
			else
				return false;
		}

		public boolean wavefront() {
			/** add first element to queue */
			MazeQueue queue = new MazeQueue();
			queue.add(getDistance(start, finish), start);
			/**
			 * go on while there are elements in queue or return when finish
			 * reached
			 */
			while (queue.size() > 0) {
				/** get next element to work with */
				MazePoint curP = queue.removeNext();
				for (MazePoint nextP : getNeighbours(curP)) {
					int nextValue = getValue(curP) + 1 /*+ Math.abs(nextP.x 
							- field.length / 2) + Math.abs(nextP.y - field[0].length / 2)*/;
					if (getValue(curP) == POINT_START) {
						nextValue = 1;
					}
					switch (getValue(nextP)) {
					case POINT_START:
					case POINT_BLOCK:
					case POINT_BORDER:
					case POINT_RESERVED:
						break;
					case POINT_FINISH:
						return true;
					case POINT_UNDEFINED:
						setValue(nextP, nextValue);
						queue.add(getDistance(nextP, finish), nextP);
						break;
					default: // we have already visited this point
						if (nextValue < getValue(nextP)) {
							setValue(nextP, nextValue);
							queue.add(getDistance(nextP, finish), nextP);
						}
						break;
					}
				}
			}
			// debugPrintField();
			// wavefront could not reach any finish point
			return false;
		}

		/** second part of algorithm */
		public List<MazePoint> backtracking() {
			List<MazePoint> route = new ArrayList<MazePoint>();

			/** add finish to route */
			route.add(new MazePoint(finish.x, finish.y, finish.z));

			/** while start not reached */
			MazePoint curPoint = finish;
			int curValue = Integer.MAX_VALUE;
			while (curValue != POINT_START) {
				MazePoint nextPoint = curPoint;
				List<MazePoint> neighbours = getNeighbours(nextPoint);
				setValue( curPoint, POINT_BLOCK );
				for (MazePoint neighbour : neighbours) {
					int value = getValue(neighbour);
					switch (value) {
					case POINT_UNDEFINED:
					case POINT_BLOCK:
					case POINT_BORDER:
					case POINT_FINISH:
					case POINT_RESERVED:
						continue;
					default:
						if (value < curValue) {
							nextPoint = neighbour;
						}
						break;
					}
				}
				if (nextPoint == curPoint) {
					debug("oops, this should never ever happen!\n");
				} else {
					curPoint = nextPoint;
					curValue = getValue(nextPoint);
					route.add(nextPoint);
				}
			}
			Collections.reverse(route);
			return route;
		}

		/** for debugging */
		public void debugPrintField() {
			debug("\nstart: ");
			printMazePoint(start);
			if (aStart == ManhattenAlignment.ma_horizontal)
				debug(" h");
			if (aStart == ManhattenAlignment.ma_vertical)
				debug(" v");
			if (aStart == ManhattenAlignment.ma_undefined)
				debug(" u");
			debug("\n");
			debug("finish: ");
			printMazePoint(finish);
			if (aFinish == ManhattenAlignment.ma_horizontal)
				debug(" h");
			if (aFinish == ManhattenAlignment.ma_vertical)
				debug(" v");
			if (aFinish == ManhattenAlignment.ma_undefined)
				debug(" u");
			debug("\n");
			for (int z = 0; z < field[0][0].length; z++) {
				debug("z: " + z + " "
						+ ((z + 1) % 2 == 0 ? "vertical" : "horizontal") + "\n");
				for (int y = 0; y < field[0].length; y++) {
					for (int x = 0; x < field.length; x++) {
						switch (getValue(x, y, z)) {
						case POINT_START:
							debug("=S=");
							break;
						case POINT_FINISH:
							debug("=F=");
							break;
						case POINT_BORDER:
							debug(" B ");
							break;
						case POINT_BLOCK:
							debug(" L ");
							break;
						case POINT_RESERVED:
							debug(" R ");
							break;
						default:
							int value = getValue(x, y, z);
							debug((value < 10 ? " " : "") + value + " ");
						}
					}
					debug("\n");
				}
			}
		}

		public static void debugPrintMazePointList(List<MazePoint> points) {
			for (MazePoint mp : points)
				printMazePoint(mp);
			debug(".\n");
		}

		public static void printMazePoint(MazePoint mp) {
			debug("(" + mp.x + "," + mp.y + "," + mp.z + ") ");
		}

		private static void debug(String s) {
			if (enableOutput) {
				System.out.print(s);
			}
		}

		final static class MazeQueue extends TreeMap<Integer, List<MazePoint>> {

			private static final long serialVersionUID = -9114852150292907187L;

			public void add(int distance, MazePoint mp) {
				if (!this.containsKey(distance)) {
					this.put(distance, new ArrayList<MazePoint>());
				}
				this.get(distance).add(mp);
			}

			public MazePoint removeNext() {
				while (this.get(this.firstKey()).size() == 0) {
					this.remove(this.firstKey());
				}
				List<MazePoint> next = this.get(this.firstKey());
				MazePoint result = next.remove(0);
				if (next.size() == 0) {
					this.remove(this.firstKey());
				}
				return result;
			}
		}
	}
}