/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: PlacementMinCut.java
 *
 * Copyright (c) 2009, Static Free Software. 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.placement;

import com.sun.electric.tool.Job;
import com.sun.electric.tool.placement.PlacementAdapter.PlacementExport;
import com.sun.electric.util.math.MutableInteger;
import com.sun.electric.util.math.Orientation;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Placement algorithm to do Min-Cut placement.
 */
public class PlacementMinCut extends PlacementFrame
{
	private final double PADDING = 5;
	private final boolean DEBUG = false;

	private Map<PlacementNode,Map<PlacementNode,MutableInteger>> connectivityMap;

	/**
	 * Method to return the name of this placement algorithm.
	 * @return the name of this placement algorithm.
	 */
	public String getAlgorithmName() { return "Min-Cut"; }

	/**
	 * Method to do Min-Cut Placement.
	 * @param nodesToPlace a list of all nodes that are to be placed.
	 * @param allNetworks a list of all networks that connect the nodes.
	 * @param cellName the name of the cell being placed.
	 * @param job the Job (for testing abort).
	 */
	public void runPlacement(List<PlacementNode> nodesToPlace, List<PlacementNetwork> allNetworks,
		List<PlacementExport> exportsToPlace, String cellName, Job job)
	{
		// build the connectivity map with the number of connections between any two PlacementNodes
		connectivityMap = new HashMap<PlacementNode,Map<PlacementNode,MutableInteger>>();
		for(PlacementNetwork plNet : allNetworks)
		{
			// add all combinations of the nodes on this net to the connectivity map
			List<PlacementPort> portsInNetwork = plNet.getPortsOnNet();
			for(int i=0; i<portsInNetwork.size(); i++)
			{
				PlacementPort plPort1 = portsInNetwork.get(i);
				PlacementNode plNode1 = plPort1.getPlacementNode();
				for(int j=i+1; j<portsInNetwork.size(); j++)
				{
					PlacementPort plPort2 = portsInNetwork.get(j);
					PlacementNode plNode2 = plPort2.getPlacementNode();

					incrementMap(plNode1, plNode2);
					incrementMap(plNode2, plNode1);
				}
			}
		}

		// make initial partition with all nodes
		List<PlacementNode> singletons = new ArrayList<PlacementNode>();
		Partition topPart = new Partition(0);
		for(PlacementNode plNode : nodesToPlace)
		{
			boolean connected = false;
			for(PlacementPort plPort : plNode.getPorts())
			{
				PlacementNetwork net = plPort.getPlacementNetwork();
				if (net == null) continue;
				for(PlacementPort otherPort : net.getPortsOnNet())
				{
					if (otherPort.getPlacementNode() != plNode) { connected = true;   break; }
				}
				if (connected) break;
			}
			if (connected) topPart.allNodes.add(plNode); else
				singletons.add(plNode);
		}

//		// determine standard cell size
//		double scSize = getStandardCellSize(nodesToPlace, null);
//		double macroSize = scSize * scSize * 100;
//		System.out.println("Standard cell size is " + scSize + " so macro cells are " + macroSize + " square units or more");

		// build list of partitions to organize
		List<Partition> partitionsToOrganize = new ArrayList<Partition>();
		partitionsToOrganize.add(topPart);

		// iteratively go through list and organize them
		while (partitionsToOrganize.size() > 0)
		{
			// get partition
			Partition part = partitionsToOrganize.get(0);
			partitionsToOrganize.remove(0);
			if (part.allNodes.size() <= 2) continue;

			// split the partition randomly
			part.splitRandomly();
			if (DEBUG)
			{
				System.out.print("INITIAL NODES IN GROUP 1:");
				for(PlacementNode plNode : part.part1.allNodes) System.out.print(" "+plNode);
				System.out.println();
				System.out.print("INITIAL NODES IN GROUP 2:");
				for(PlacementNode plNode : part.part2.allNodes) System.out.print(" "+plNode);
				System.out.println();
			}

			// organize the two halves properly
			part.organize();
			if (DEBUG)
			{
				System.out.print("FINAL NODES IN GROUP 1:");
				for(PlacementNode plNode : part.part1.allNodes) System.out.print(" "+plNode);
				System.out.println();
				System.out.print("FINAL NODES IN GROUP 2:");
				for(PlacementNode plNode : part.part2.allNodes) System.out.print(" "+plNode);
				System.out.println();
			}

			if (part.part1.allNodes.size() > 2) partitionsToOrganize.add(part.part1);
			if (part.part2.allNodes.size() > 2) partitionsToOrganize.add(part.part2);
		}

		Point2D lastOffset = placePartitions(topPart, new Point2D.Double(0, 0));
		double x = lastOffset.getX(), y = lastOffset.getY();
		for(PlacementNode plNode : singletons)
		{
			plNode.setPlacement(x, y);
			x += PADDING;
		}
	}

	private Point2D placePartitions(Partition part, Point2D offset)
	{
		String indent = "";
		if (DEBUG)
		{
			for(int i=0; i<part.depth; i++) indent += "   ";
			indent += part.depth + ": ";
			System.out.println(indent+"PARTITION AT OFFSET ("+offset.getX()+","+offset.getY()+")...");
		}
		Point2D off;
		if (part.part1 != null && part.part2 != null)
		{
			if (DEBUG) System.out.println(indent+"PLACING SUBPART 1 AT ("+offset.getX()+","+offset.getY()+")");
			Point2D off1 = placePartitions(part.part1, offset);
			if ((part.depth&1) == 0)
			{
				Point2D nextOff = new Point2D.Double(offset.getX(), offset.getY() + off1.getY());
				if (DEBUG) System.out.println(indent+"PLACING SUBPART 2 AT ("+nextOff.getX()+","+nextOff.getY()+")");
				Point2D off2 = placePartitions(part.part2, nextOff);
				off = new Point2D.Double(Math.max(off1.getX(), off2.getX()), off1.getY() + off2.getY());
			} else
			{
				Point2D nextOff = new Point2D.Double(offset.getX() + off1.getX(), offset.getY());
				if (DEBUG) System.out.println(indent+"PLACING SUBPART 2 AT ("+nextOff.getX()+","+nextOff.getY()+")");
				Point2D off2 = placePartitions(part.part2, nextOff);
				off = new Point2D.Double(off1.getX() + off2.getX(), Math.max(off1.getY(), off2.getY()));
			}
		} else
		{
			double widestX = 0, widestY = 0;
			double placeX = offset.getX(), placeY = offset.getY();
			for(PlacementNode plNode : part.allNodes)
			{
				double width = plNode.getWidth();
				double height = plNode.getHeight();
				width = height = Math.max(width, height);
				Point2D thisOff = new Point2D.Double(placeX, placeY);
				if ((part.depth&1) != 0)
				{
					widestX += width + PADDING;
					placeX += width + PADDING;
					widestY = Math.max(widestY, height + PADDING);
				} else
				{
					widestX = Math.max(widestX, width + PADDING);
					widestY += height + PADDING;
					placeY += height + PADDING;
				}
				plNode.setPlacement(thisOff.getX(), thisOff.getY());
			}
			Map<PlacementNode,Orientation> properOrientation = findOrientations(part.allNodes);
			for(PlacementNode plNode : part.allNodes)
			{
				Orientation or = properOrientation.get(plNode);
				if (or != null)
					plNode.setOrientation(or);
			}
			off = new Point2D.Double(widestX, widestY);
		}
		if (DEBUG) System.out.println(indent+"NEW OFFSET ("+off.getX()+","+off.getY()+")");
		return off;
	}

	private static class OrientationConnection
	{
		PlacementPort thisPP;
		PlacementNode otherPN;
		PlacementPort otherPP;
	}

	/**
	 * Method to find the ideal orientation for all of the nodes at the bottom point.
	 * @param allNodes a List of PlacementNodes that have location, but not ideal orientation.
	 * @return a Map assigning orientation to each of the PlacementNodes in the list.
	 */
	private Map<PlacementNode,Orientation> findOrientations(List<PlacementNode> allNodes)
	{
		Map<PlacementNode,Orientation> properOrientation = new HashMap<PlacementNode,Orientation>();
		if (allNodes.size() > 1)
		{
//boolean debug = currentCellName.equals("spiceHier{sch}");
//if (debug) System.out.println("FINDING ORIENTATIONS FOR PARTITION WITH "+allNodes.size()+" NODES");
			Map<PlacementNode,List<OrientationConnection>> allPossibilities = new HashMap<PlacementNode,List<OrientationConnection>>();
			for(PlacementNode plNode : allNodes)
			{
				// create a List of OrientationConnection objects for this NodeInst
				List<OrientationConnection> oc = new ArrayList<OrientationConnection>();
				allPossibilities.put(plNode, oc);
			}

			// add all of the connections to other nodes in the partition
			for(PlacementNode plNode : allNodes)
			{
				List<OrientationConnection> oc = allPossibilities.get(plNode);

				for(PlacementPort plPort : plNode.getPorts())
				{
					PlacementNetwork plNet = plPort.getPlacementNetwork();
					if (plNet == null) continue;
					for(PlacementPort otherPlPort : plNet.getPortsOnNet())
					{
						PlacementNode otherPlNode = otherPlPort.getPlacementNode();
						if (otherPlNode == plNode) continue;
						if (allPossibilities.get(otherPlNode) == null) continue;

						OrientationConnection orc = new OrientationConnection();
						orc.thisPP = plPort;
						orc.otherPN = otherPlNode;
						orc.otherPP = otherPlPort;
						oc.add(orc);
					}
				}
			}

			// now find the optimal orientation choice for each NodeInst
			Orientation [] standardEight = new Orientation[] {Orientation.IDENT, Orientation.R, Orientation.RR, Orientation.RRR,
				Orientation.X, Orientation.XR, Orientation.XRR, Orientation.XRRR};
//			if (allNodes.size() == 2)
//			{
//				// try all combinations of the two
//				PlacementNode plNode1 = allNodes.get(0);
//				PlacementNode plNode2 = allNodes.get(1);
//				List<OrientationConnection> oc = allPossibilities.get(plNode1);
//				Orientation betterOrientation1 = null;
//				Orientation betterOrientation2 = null;
//				double bestDist = Double.MAX_VALUE;
//				for(int i=0; i<standardEight.length; i++)
//				{
//					plNode1.setOrientation(standardEight[i]);
//					for(int j=0; j<standardEight.length; j++)
//					{
//						plNode2.setOrientation(standardEight[j]);
//						double length = 0;
//						for(OrientationConnection con : oc)
//						{
//							Point2D pt1 = new Point2D.Double(plNode1.getPlacementX() + con.thisPP.getRotatedOffX(),
//								plNode1.getPlacementY() + con.thisPP.getRotatedOffY());
//							Point2D pt2 = new Point2D.Double(plNode2.getPlacementX() + con.otherPP.getRotatedOffX(),
//								plNode2.getPlacementY() + con.otherPP.getRotatedOffY());
//							double dist = pt1.distance(pt2);
//							length += dist;
//						}
//						if (betterOrientation1 == null || length < bestDist)
//						{
//							bestDist = length;
//							betterOrientation1 = standardEight[i];
//							betterOrientation2 = standardEight[j];
//						}
//					}
//				}
//				if (betterOrientation1 != null)
//				{
//					plNode1.setOrientation(betterOrientation1);
//					properOrientation.put(plNode1, betterOrientation1);
//					plNode2.setOrientation(betterOrientation2);
//					properOrientation.put(plNode2, betterOrientation2);
//				}
//			} else
			{
				for(PlacementNode plNode : allNodes)
				{
					List<OrientationConnection> oc = allPossibilities.get(plNode);
					double bestDist = Double.MAX_VALUE;
					Orientation betterOrientation = null;
					for(int i=0; i<standardEight.length; i++)
					{
						plNode.setOrientation(standardEight[i]);

//if (debug) System.out.println("COMPUTING LENGTH FOR NODE "+plNode+" ORIENTATION "+standardEight[i]);
						double length = 0;
						for(OrientationConnection con : oc)
						{
							Point2D pt = new Point2D.Double(plNode.getPlacementX() + con.thisPP.getRotatedOffX(),
								plNode.getPlacementY() + con.thisPP.getRotatedOffY());
							Point2D otherPt = new Point2D.Double(con.otherPN.getPlacementX() + con.otherPP.getRotatedOffX(),
								con.otherPN.getPlacementY() + con.otherPP.getRotatedOffY());
							double dist = pt.distance(otherPt);
//if (debug) System.out.println("  ("+pt.getX()+","+pt.getY()+") TO ("+otherPt.getX()+","+otherPt.getY()+") = "+dist);
							length += dist;
						}
//if (debug) System.out.println("  ======= LENGTH="+length);
						if (betterOrientation == null || length < bestDist)
						{
							bestDist = length;
							betterOrientation = standardEight[i];
						}
					}
					if (betterOrientation != null)
					{
						plNode.setOrientation(betterOrientation);
						properOrientation.put(plNode, betterOrientation);
					}
				}
			}
		}
		return properOrientation;
	}

	private class Partition
	{
		List<PlacementNode> allNodes;
		Partition part1, part2;
		int depth;

		Partition(int depth)
		{
			this.depth = depth;
			allNodes = new ArrayList<PlacementNode>();
		}

		void splitRandomly()
		{
			part1 = new Partition(depth+1);
			part2 = new Partition(depth+1);
			boolean putIn1 = true;
			for(PlacementNode plNode : allNodes)
			{
				if (putIn1) part1.allNodes.add(plNode); else
					part2.allNodes.add(plNode);
				putIn1 = !putIn1;
			}
		}

		void organize()
		{
			// make a list of swaps
			List<SwapNodes> allSwaps = new ArrayList<SwapNodes>();

			for(;;)
			{
				// determine the baseline connection count
				int startingCuts = findNumCuts();

				// now try swapping all nodes
				int bestGain = 0;
				int group1Member = -1, group2Member = -1;
				for(int i=0; i<part1.allNodes.size(); i++)
				{
					PlacementNode plNode1 = part1.allNodes.get(i);
					for(int j=0; j<part2.allNodes.size(); j++)
					{
						PlacementNode plNode2 = part2.allNodes.get(j);

						// swap them
						part1.allNodes.set(i, plNode2);
						part2.allNodes.set(j, plNode1);

						// see if the swap produces a gain
						int newCuts = findNumCuts();
						int gain = startingCuts - newCuts;
						if (gain > bestGain)
						{
							// gain found
							bestGain = gain;
							group1Member = i;
							group2Member = j;
						}

						// restore order
						part1.allNodes.set(i, plNode1);
						part2.allNodes.set(j, plNode2);
					}
				}

				// stop now if no gain was found
				if (bestGain == 0) break;

				// enter the gain, remove these nodes, and continue to search
				PlacementNode plNode1 = part1.allNodes.get(group1Member);
				PlacementNode plNode2 = part2.allNodes.get(group2Member);
				if (DEBUG) System.out.println("SWAPPING NODES "+plNode1+" AND "+plNode2+" FOR A GAIN OF "+bestGain);
				part1.allNodes.remove(group1Member);
				part2.allNodes.remove(group2Member);
				SwapNodes g = new SwapNodes(plNode1, plNode2);
				allSwaps.add(g);
			}

			// put the swaps back in, swapped
			for(SwapNodes g : allSwaps)
			{
				part1.allNodes.add(g.plNode2);
				part2.allNodes.add(g.plNode1);
			}
		}

		/**
		 * Method to scan two halves of the Partition and return the total number of
		 * connections that separate them.
		 * @return the number of connections between the two groups.
		 */
		private int findNumCuts()
		{
			int cuts = 0;
			for(PlacementNode plNode1 : part1.allNodes)
			{
				for(PlacementNode plNode2 : part2.allNodes)
					cuts += getConnectivity(plNode1, plNode2);
			}
			return cuts;
		}
	}

	private static class SwapNodes
	{
		PlacementNode plNode1, plNode2;

		SwapNodes(PlacementNode n1, PlacementNode n2)
		{
			plNode1 = n1;
			plNode2 = n2;
		}
	}

	/**
	 * Method to return the number of connections between two PlacementNodes.
	 * @param plNode1 the first PlacementNode.
	 * @param plNode2 the second PlacementNode.
	 * @return the number of connections between the PlacementNodes.
	 */
	private int getConnectivity(PlacementNode plNode1, PlacementNode plNode2)
	{
		Map<PlacementNode,MutableInteger> destMap = connectivityMap.get(plNode1);
		if (destMap == null) return 0;
		MutableInteger mi = destMap.get(plNode2);
		if (mi == null) return 0;
		return mi.intValue();
	}

	/**
	 * Method to build the connectivity map by adding a connection between two PlacementNodes.
	 * This method is usually called twice with the PlacementNodes in both orders because
	 * the mapping is not symmetric.
	 * @param plNode1 the first PlacementNode.
	 * @param plNode2 the second PlacementNode.
	 */
	private void incrementMap(PlacementNode plNode1, PlacementNode plNode2)
	{
		Map<PlacementNode,MutableInteger> destMap = connectivityMap.get(plNode1);
		if (destMap == null)
			connectivityMap.put(plNode1, destMap = new HashMap<PlacementNode,MutableInteger>());
		MutableInteger mi = destMap.get(plNode2);
		if (mi == null) destMap.put(plNode2, mi = new MutableInteger(0));
		mi.increment();
	}
}
