/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: TechConversionResult.java
 * Technology Editor, conversion of technology libraries to technologies
 * Written by Steven M. Rubin.
 *
 * Copyright (c) 2007, 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.user.tecEdit;

import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.topology.NodeInst;
import com.sun.electric.database.variable.EditWindow_;
import com.sun.electric.database.variable.UserInterface;
import com.sun.electric.tool.Job;
import com.sun.electric.util.TextUtils;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Class to define error messages that arise during technology creation.
 * This class is necessary because errors occur during Jobs, but the display
 * of the errors must be done on the client side.  Therefore, objects of
 * this class are passed from the server to the client for display.
 */
public class TechConversionResult implements Serializable
{
	private boolean good;
	private String errorMessage;
	private NodeInst ni;
	private Cell cell;
	private List<Example> neList;
	private Sample ns;
	private double sampleCoord;
	private boolean xDir;

	public TechConversionResult() { good = true; }

	/**
	 * Method to return the success status.
	 * @return true if the conversion failed.
	 */
	public boolean failed() { return !good; }

	/**
	 * Method to mark a general error.
	 * @param ni the NodeInst that caused the error (may be null).
	 * @param cell the Cell where the error occurred (may be null).
	 * @param errorMessage the message to display.
	 */
	public void markError(NodeInst ni, Cell cell, String errorMessage)
	{
		good = false;
		this.cell = cell;
		this.ni = ni;
		this.errorMessage = errorMessage;
	}

	/**
	 * Method to mark an error in determining stretching rules.
	 * @param neList the Examples that invoked the error.
	 * @param ns the Sample on the main Example that failed.
	 * @param cell the Cell with the error.
	 * @param sampleCoord the coordinate (in X or Y) where the failure occurred.
	 * @param xDir true for an X-axis error, false for Y-axis.
	 */
	public void markStretchProblem(List<Example> neList, Sample ns, Cell cell, double sampleCoord, boolean xDir)
	{
		good = false;
		this.neList = neList;
		this.ns = ns;
		this.cell = cell;
		this.sampleCoord = sampleCoord;
		this.xDir = xDir;
	}

	/**
	 * Method to return the error message associated with this conversion.
	 * @return the error message associated with this conversion.
	 */
	public String getErrorMessage()
	{
		String fullErrorMsg = errorMessage;
		if (cell != null)
		{
    		String prefixErrorMsg = "Cell " + cell.describe(false);
    		if (ni != null) prefixErrorMsg += ", node " + ni.describe(false);
    		fullErrorMsg = prefixErrorMsg + ": " + fullErrorMsg;
		}
		return fullErrorMsg;
	}

	/**
	 * Method to highlight the error.
	 */
	public void showError()
	{
		// more complex errors have their own way to display
		if (errorMessage == null)
		{
			explainStretchProblem();
			return;
		}

		String msg = getErrorMessage();
		if (cell != null)
		{
			UserInterface ui = Job.getUserInterface();
//			Job j = Job.getRunningJob();
//			if (j != null && j.isExamine())
			{
				ui.displayCell(cell);
	    		EditWindow_ wnd = ui.getCurrentEditWindow_();
	    		if (wnd != null && ni != null)
	    		{
	    			wnd.clearHighlighting();
	    			wnd.addElectricObject(ni, cell);
	    			wnd.finishedHighlighting();
	    		}
			}
			ui.showErrorMessage(msg, "Analysis Failure");
		}
		System.out.println(msg);
	}

	/**
	 * Method to explain a stretching rule problem.
	 * @param neList the list of Examples
	 * @param ns the sample in the main Example
	 * @param np the Cell.
	 * @param sampleCoord the coordinate of the stretching failure.
	 * @param xDir true if this is in the X direction, false for Y.
	 */
	private void explainStretchProblem()
	{
		Job.getUserInterface().displayCell(cell);
		EditWindow_ wnd = Job.getUserInterface().getCurrentEditWindow_();
		if (wnd == null) return;
		wnd.clearHighlighting();
		Map<Sample,double[]> factorList = new HashMap<Sample,double[]>();
		double[] factors = showSampleInExample(wnd, ns, neList.get(0), sampleCoord, xDir, true);
		factorList.put(ns, factors);
		for(int n=1; n<neList.size(); n++)
		{
			Example e = neList.get(n);
			for(Sample s : e.samples)
			{
				if (s.assoc == ns)
				{
					factors = showSampleInExample(wnd, s, e, sampleCoord, xDir, false);
					factorList.put(s, factors);
				}
			}
		}

		// see if the offending sample can be guessed
		Set<Sample> allSamples = factorList.keySet();
		Map<Sample,Integer> offendingSample = new HashMap<Sample,Integer>();
		for(int i=0; i<4; i++)
		{
			double v1 = 0, v2 = 0;
			int v1Count = 0, v2Count = 0;
			Sample s1 = null, s2 = null;
			for(Sample s : allSamples)
			{
				factors = factorList.get(s);
				if (v1Count == 0)
				{
					v1Count++;
					v1 = factors[i];
					s1 = s;
					continue;
				}
				if (v1 == factors[i])
				{
					v1Count++;
					continue;
				}
				if (v2Count == 0)
				{
					v2Count++;
					v2 = factors[i];
					s2 = s;
					continue;
				}
				if (v2 == factors[i])
				{
					v2Count++;
					continue;
				}
			}
			if (v1Count+v2Count != allSamples.size()) continue;
			Sample offendingSamp = null;
			if (v1Count == 1) offendingSamp = s1;
			if (v2Count == 1) offendingSamp = s2;
			if (offendingSamp != null)
			{
				Integer prev = offendingSample.get(offendingSamp);
				if (prev == null) prev = Integer.valueOf(1); else prev = Integer.valueOf(prev.intValue()+1);
				offendingSample.put(offendingSamp, prev);
			}
		}
		Sample offendingSamp = null;
		int num = 0;
		for(Sample s : offendingSample.keySet())
		{
			int numTimes = offendingSample.get(s).intValue();
			if (numTimes > num)
			{
				num = numTimes;
				offendingSamp = s;
			}
		}

		// show the offending sample if found, all if not
		String err = "Cannot determine " + (xDir?"X":"Y") + " stretching rule for layer " + Info.getSampleName(ns.layer) +
			" in " + cell;
		String additional = "\nOne of these stretching rules must be the same for every example:" +
			"\nOut (the distance from the center to the point)" +
			"\nIn (the distance from the edge to the point)" +
			"\nPer (the percentage of the point from the center to the edge)" +
			"\nOpp (the distance from the opposite edge to the point)";
		if (offendingSamp == null)
		{
			offendingSamp = ns;
			for(Sample s : allSamples)
				wnd.addElectricObject(s.node, cell);
		} else
		{
			wnd.addElectricObject(offendingSamp.node, cell);
			additional += "\n\nThe error is probably with node " + offendingSamp.node.describe(false);
		}
		wnd.finishedHighlighting();

		Job.getUserInterface().showErrorMessage(err+additional, "Analysis Failure");
		System.out.println(err);
	}

	private double[] showSampleInExample(EditWindow_ wnd, Sample ns, Example e, double sampleCoord,
		boolean xDir, boolean mainExample)
	{
		Cell cell = ns.node.getParent();
		Rectangle2D exampleBounds = wnd.getCell().getBounds();
		double singleStep = Math.max(exampleBounds.getHeight(), exampleBounds.getWidth()) / 45;
		double doubleStep = singleStep * 2;
		double halfStep = singleStep / 2;
		double exampleHalfSize = (xDir ? (e.hx - e.lx) : (e.hy - e.ly)) / 2;
		double exampleCtr = (xDir ? (e.lx + e.hx) : (e.ly + e.hy)) / 2;

		// first draw bold bars above/below example at the edges and center
		Point2D pt1 = makeDisplayPoint(e, 0, exampleCtr-exampleHalfSize, xDir);
		Point2D pt2 = makeDisplayPoint(e, singleStep, exampleCtr-exampleHalfSize, xDir);
		wnd.addHighlightLine(pt1, pt2, cell, true, false);
		pt1 = makeDisplayPoint(e, 0, exampleCtr+exampleHalfSize, xDir);
		pt2 = makeDisplayPoint(e, singleStep, exampleCtr+exampleHalfSize, xDir);
		wnd.addHighlightLine(pt1, pt2, cell, true, false);
		pt1 = makeDisplayPoint(e, 0, exampleCtr, xDir);
		pt2 = makeDisplayPoint(e, doubleStep, exampleCtr, xDir);
		wnd.addHighlightLine(pt1, pt2, cell, true, false);

		Rectangle2D mainSampleBounds = ns.assoc.node.getBounds();
		Rectangle2D sampleBounds = ns.node.getBounds();
		boolean adjusted = false;
		if (mainExample) adjusted = true; else
		{
			if (xDir)
			{
				if (sampleCoord == mainSampleBounds.getMinX())
					{ adjusted = true;   sampleCoord = sampleBounds.getMinX(); }
				if (sampleCoord == mainSampleBounds.getMaxX())
					{ adjusted = true;   sampleCoord = sampleBounds.getMaxX(); }
			} else
			{
				if (sampleCoord == mainSampleBounds.getMinY())
					{ adjusted = true;   sampleCoord = sampleBounds.getMinY(); }
				if (sampleCoord == mainSampleBounds.getMaxY())
					{ adjusted = true;   sampleCoord = sampleBounds.getMaxY(); }
			}
		}

		// determine the 4 stretching factors (out, in, %, opp)
		double [] factors = new double[4];
		double percentOut = Math.abs(sampleCoord-exampleCtr) / exampleHalfSize;
		double distOut = Math.abs(sampleCoord-exampleCtr);
		factors[0] = distOut;
		factors[1] = exampleHalfSize-distOut;
		factors[2] = Math.round(percentOut*100);
		factors[3] = distOut+exampleHalfSize;

		if (adjusted)
		{
			double arrowCoord = sampleCoord;
			double arrowCoordInv = sampleCoord;
			double farEnd, closeEnd;
			if (sampleCoord > exampleCtr)
			{
				arrowCoord -= singleStep;
				arrowCoordInv += singleStep;
				closeEnd = exampleCtr + exampleHalfSize;
				farEnd = exampleCtr - exampleHalfSize;
			} else
			{
				arrowCoord += singleStep;
				arrowCoordInv -= singleStep;
				closeEnd = exampleCtr - exampleHalfSize;
				farEnd = exampleCtr + exampleHalfSize;
			}

			// now draw bar where problem is
			pt1 = makeDisplayPoint(e, halfStep, sampleCoord, xDir);
			pt2 = makeDisplayPoint(e, doubleStep+singleStep, sampleCoord, xDir);
			wnd.addHighlightLine(pt1, pt2, cell, false, false);

			// draw arrow from opposite edge to problem location
			if (sampleCoord != farEnd)
			{
				pt1 = makeDisplayPoint(e, singleStep, farEnd, xDir);
				pt2 = makeDisplayPoint(e, singleStep, sampleCoord, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
				pt1 = makeDisplayPoint(e, singleStep, sampleCoord, xDir);
				pt2 = makeDisplayPoint(e, singleStep-singleStep/3, arrowCoord, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
				pt1 = makeDisplayPoint(e, singleStep, sampleCoord, xDir);
				pt2 = makeDisplayPoint(e, singleStep+singleStep/3, arrowCoord, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
			}

			// draw arrow from center to problem location
			if (sampleCoord != exampleCtr)
			{
				pt1 = makeDisplayPoint(e, doubleStep, exampleCtr, xDir);
				pt2 = makeDisplayPoint(e, doubleStep, sampleCoord, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
				pt1 = makeDisplayPoint(e, doubleStep, sampleCoord, xDir);
				pt2 = makeDisplayPoint(e, doubleStep-singleStep/3, arrowCoord, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
				pt1 = makeDisplayPoint(e, doubleStep, sampleCoord, xDir);
				pt2 = makeDisplayPoint(e, doubleStep+singleStep/3, arrowCoord, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
			}

			// draw arrow from close edge to problem location
			if (sampleCoord != closeEnd)
			{
				pt1 = makeDisplayPoint(e, doubleStep, closeEnd, xDir);
				pt2 = makeDisplayPoint(e, doubleStep, sampleCoord, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
				pt1 = makeDisplayPoint(e, doubleStep, sampleCoord, xDir);
				pt2 = makeDisplayPoint(e, doubleStep-singleStep/3, arrowCoordInv, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
				pt1 = makeDisplayPoint(e, doubleStep, sampleCoord, xDir);
				pt2 = makeDisplayPoint(e, doubleStep+singleStep/3, arrowCoordInv, xDir);
				wnd.addHighlightLine(pt1, pt2, cell, false, false);
			}

			// write factors
			pt2 = makeDisplayPoint(e, doubleStep+halfStep, xDir ? exampleCtr : (exampleCtr-halfStep), xDir);
			wnd.addHighlightMessage(cell, "Out="+TextUtils.formatDouble(factors[0])+
				" Per="+((int)factors[2]), pt2);
			pt2 = makeDisplayPoint(e, doubleStep-halfStep, xDir ? closeEnd+halfStep : (closeEnd-halfStep), xDir);
			wnd.addHighlightMessage(cell, "In="+TextUtils.formatDouble(factors[1]), pt2);
			pt2 = makeDisplayPoint(e, singleStep+halfStep, xDir ? farEnd : (farEnd-halfStep), xDir);
			wnd.addHighlightMessage(cell, "Opp="+TextUtils.formatDouble(factors[3]), pt2);
		}
		return factors;
	}

	private Point2D makeDisplayPoint(Example e, double offsetOrtho, double sampleCoord, boolean xDir)
	{
		if (xDir) return new Point2D.Double(sampleCoord, e.hy+offsetOrtho);
		return new Point2D.Double(e.hx+offsetOrtho, sampleCoord);
	}

}
