/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: DXF.java
 * Input/output tool: DXF output
 * Written by Steven M. Rubin.
 *
 * Copyright (c) 2004, 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.io.output;

import com.sun.electric.database.geometry.Poly;
import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.prototype.NodeProto;
import com.sun.electric.database.text.TextUtils;
import com.sun.electric.database.topology.NodeInst;
import com.sun.electric.database.variable.VarContext;
import com.sun.electric.database.variable.Variable;
import com.sun.electric.technology.Layer;
import com.sun.electric.technology.PrimitiveNode;
import com.sun.electric.technology.Technology;
import com.sun.electric.technology.technologies.Artwork;
import com.sun.electric.tool.io.IOTool;
import com.sun.electric.util.JavaCompatiblity;
import com.sun.electric.util.math.FixpTransform;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * This is the netlister for DXF.
 */
public class DXF extends Output
{
	/** key of Variable holding DXF layer name. */			public static final Variable.Key DXF_LAYER_KEY = Variable.newKey("IO_dxf_layer");
	/** key of Variable holding DXF header text. */			public static final Variable.Key DXF_HEADER_TEXT_KEY = Variable.newKey("IO_dxf_header_text");
	/** key of Variable holding DXF header information. */	public static final Variable.Key DXF_HEADER_ID_KEY = Variable.newKey("IO_dxf_header_ID");
	private int dxfEntityHandle;
	private Set<Cell> cellsSeen;
	private TextUtils.UnitScale dxfDispUnit;
	private String defaultDXFLayerName;
	private static String [] ignorefromheader = {"$DWGCODEPAGE", "$HANDSEED", "$SAVEIMAGES"};

	private DXFPreferences localPrefs;

	public static class DXFPreferences extends OutputPreferences
    {
        // DXF Settings
		int dxfScale = IOTool.getDXFScale();
        public Technology tech;

        public DXFPreferences(boolean factory) {
            super(factory);
            tech = Technology.getCurrent();
        }

        @Override
        public Output doOutput(Cell cell, VarContext context, String filePath)
        {
    		DXF out = new DXF(this);
    		if (out.openTextOutputStream(filePath)) return out.finishWrite();

    		out.writeDXF(cell);

    		if (out.closeTextOutputStream()) return out.finishWrite();
    		System.out.println(filePath + " written");
            return out.finishWrite();
        }
    }

	/**
	 * Creates a new instance of the DXF netlister.
	 */
	DXF(DXFPreferences dp) { localPrefs = dp; }

	private void writeDXF(Cell cell)
	{
		// set the scale
		dxfDispUnit = TextUtils.UnitScale.findFromIndex(localPrefs.dxfScale);

		// write the header
		Variable varheadertext = cell.getLibrary().getVar(DXF_HEADER_TEXT_KEY);
		Variable varheaderid = cell.getLibrary().getVar(DXF_HEADER_ID_KEY);
		Layer defLay = Artwork.tech().findLayer("Graphics");
		defaultDXFLayerName = defLay.getDXFLayer();
		if (varheadertext != null && varheaderid != null)
		{
			printWriter.print("  0\nSECTION\n");
			printWriter.print("  2\nHEADER\n");
			int len = Math.min(varheadertext.getLength(), varheaderid.getLength());
			for(int i=0; i<len; i++)
			{
				// remove entries that confuse the issues
				String pt = (String)varheadertext.getObject(i);
				int code = ((Integer)varheaderid.getObject(i)).intValue();
				if (code == 9 && i <= len-2)
				{
					boolean found = false;
					for(int j=0; j<ignorefromheader.length; j++)
					{
						if (pt.equals(ignorefromheader[j]) || pt.substring(1).equals(ignorefromheader[j]))
						{
							found = true;
							break;
						}
					}
					if (found)
					{
						i++;
						continue;
					}
				}

				// make sure Autocad version is correct
				if (pt.equals("$ACADVER") && i <= len-2)
				{
					printWriter.print(getThreeDigit(code) + "\n" + pt + "\n");
					printWriter.print("  1\nAC1009\n");
					i++;
					continue;
				}

				printWriter.print(getThreeDigit(code) + "\n" + pt + "\n");
			}
			printWriter.print("  0\nENDSEC\n");
		}

		// write any subcells
		dxfEntityHandle = 0x100;
		printWriter.print("  0\nSECTION\n");
		printWriter.print("  2\nBLOCKS\n");

		cellsSeen = new HashSet<Cell>();
		for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
		{
			NodeInst ni = it.next();
			if (ni.isCellInstance())
			{
				NodeProto np = ni.getProto();
				if (!cellsSeen.contains(np)) writeDXFCell((Cell)np, true);
			}
		}

		printWriter.print("  0\nENDSEC\n");
		printWriter.print("  0\nSECTION\n");
		printWriter.print("  2\nENTITIES\n");
		writeDXFCell(cell, false);
		printWriter.print("  0\nENDSEC\n");
		printWriter.print("  0\nEOF\n");
	}

	/**
	 * Method to transform double into String and control number of digits to write. Introduced while
	 * porting to Java 8.
	 * @param value Double to transform
	 * @return String representing the double provided
	 */
	private String formatDoubleJavaVersion(double value)
	{
		// do rounding (in Java8, some rounding issues have been detected. round to 2 digits)
		double scale = (JavaCompatiblity.JAVA8 ? 100 : 1000);
		double rounded = Math.round(value * scale) / scale;
		String str = TextUtils.formatDouble(rounded);
		return str;
	}
	
	/**
	 * Method to write the contents of cell "np".  If "subCells" is nonzero, do a recursive
	 * descent through the subcells in this cell, writing out "block" definitions.
	 */
	private void writeDXFCell(Cell cell, boolean subCells)
	{
		if (subCells)
		{
			cellsSeen.add(cell);
			for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
			{
				NodeInst ni = it.next();
				NodeProto np = ni.getProto();
				if (ni.isCellInstance() && !cellsSeen.contains(np))
					writeDXFCell((Cell)np, true);
			}
			printWriter.print("  0\nBLOCK\n");
			printWriter.print("  2\n" + getDXFCellName(cell) + "\n");
			printWriter.print(" 10\n0\n");
			printWriter.print(" 20\n0\n");
			printWriter.print(" 30\n0\n");
			printWriter.print(" 70\n0\n");
		}

		for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
		{
			NodeInst ni = it.next();
			NodeProto np = ni.getProto();

			// handle instances
			if (ni.isCellInstance())
			{
				Cell subCell = (Cell)np;
				printWriter.print("  0\nINSERT\n");
				printWriter.print("  8\nUNKNOWN\n");
				printWriter.print("  5\n" + getThreeDigitHex(dxfEntityHandle++) + "\n");
				printWriter.print("  2\n" + getDXFCellName(subCell) + "\n");
				Rectangle2D cellBounds = subCell.getBounds();
				double xC = TextUtils.convertDistance(ni.getAnchorCenterX() - cellBounds.getCenterX(), localPrefs.tech, dxfDispUnit);
				double yC = TextUtils.convertDistance(ni.getAnchorCenterY() - cellBounds.getCenterY(), localPrefs.tech, dxfDispUnit);
				printWriter.print(" 10\n" + formatDoubleJavaVersion(xC) + "\n");
				printWriter.print(" 20\n" + formatDoubleJavaVersion(yC) + "\n");
				printWriter.print(" 30\n0\n");
				double rot = ni.getAngle() / 10.0;
				printWriter.print(" 50\n" + formatDoubleJavaVersion(rot) + "\n");
				continue;
			}

			// determine layer name for this node
			String layerName = defaultDXFLayerName;
			Variable var = ni.getVar(DXF_LAYER_KEY);
			if (var != null) layerName = var.getPureValue(-1); else
			{
				// examine technology for proper layer name
				if (!ni.isCellInstance())
				{
					PrimitiveNode pNp = (PrimitiveNode)ni.getProto();
					Technology.NodeLayer [] nodeLayers = pNp.getNodeLayers();
					for(int i=0; i<nodeLayers.length; i++)
					{
						Technology.NodeLayer nl = nodeLayers[i];
						Layer layer = nl.getLayer();
						String dxfLayerName = layer.getDXFLayer();
						if (dxfLayerName != null && dxfLayerName.length() > 0)
						{
							layerName = dxfLayerName;
							break;
						}
					}
				}
			}

			// get the node center
			double xC = ni.getAnchorCenterX();
			double yC = ni.getAnchorCenterY();

			// handle circles and arcs
			if (np instanceof PrimitiveNode && ((PrimitiveNode)np).isPartialCircle())
			{
				double [] angles = ni.getArcDegrees();
				double startOffset = angles[0];
				double endAngle = angles[1];
				if (startOffset != 0.0 || endAngle != 0.0) printWriter.print("  0\nARC\n"); else
					printWriter.print("  0\nCIRCLE\n");
				printWriter.print("  8\n" + layerName + "\n");
				printWriter.print("  5\n" + getThreeDigitHex(dxfEntityHandle++) + "\n");
				xC = TextUtils.convertDistance(xC, localPrefs.tech, dxfDispUnit);
                yC = TextUtils.convertDistance(yC, localPrefs.tech, dxfDispUnit);
				double zC = TextUtils.convertDistance(ni.getYSize() / 2, localPrefs.tech, dxfDispUnit);
				printWriter.print(" 10\n" + formatDoubleJavaVersion(xC) + "\n");
				printWriter.print(" 20\n" + formatDoubleJavaVersion(yC) + "\n");
				printWriter.print(" 30\n0\n");
				printWriter.print(" 40\n" + formatDoubleJavaVersion(zC) + "\n");
				if (startOffset != 0.0 || endAngle != 0.0)
				{
					startOffset = startOffset * 180.0 / Math.PI;
					endAngle = endAngle * 180.0 / Math.PI;
					double startAngle = ni.getAngle() / 10.0 + startOffset;
					if (ni.isXMirrored() != ni.isYMirrored())
					{
						startAngle = 270.0 - startAngle - endAngle;
						if (startAngle < 0.0) startAngle += 360.0;
					}
					endAngle += startAngle;
					if (endAngle >= 360.0) endAngle -= 360.0;
					printWriter.print(" 50\n" + formatDoubleJavaVersion(startAngle) + "\n");
					printWriter.print(" 51\n" + formatDoubleJavaVersion(endAngle) + "\n");
				}
				continue;
			}

			// handle polygons
			if (np == Artwork.tech().openedPolygonNode || np == Artwork.tech().openedDashedPolygonNode ||
				np == Artwork.tech().closedPolygonNode)
			{
				FixpTransform trans = ni.rotateOut();
				Point2D [] points = ni.getTrace();
				int len = points.length;
				if (len == 2 && np != Artwork.tech().closedPolygonNode)
				{
					// line
					printWriter.print("  0\nLINE\n");
					printWriter.print("  8\n" + layerName + "\n");
					printWriter.print("  5\n" + getThreeDigitHex(dxfEntityHandle++) + "\n");
					Point2D pt = new Point2D.Double(points[0].getX() + xC, points[0].getY() + yC);
					trans.transform(pt, pt);
					double x = TextUtils.convertDistance(pt.getX(), localPrefs.tech, dxfDispUnit);
					double y = TextUtils.convertDistance(pt.getY(), localPrefs.tech, dxfDispUnit);
					printWriter.print(" 10\n" + formatDoubleJavaVersion(x) + "\n");
					printWriter.print(" 20\n" + formatDoubleJavaVersion(y) + "\n");
					printWriter.print(" 30\n0\n");
					pt = new Point2D.Double(points[1].getX() + xC, points[1].getY() + yC);
					trans.transform(pt, pt);
					x = TextUtils.convertDistance(pt.getX(), localPrefs.tech, dxfDispUnit);
					y = TextUtils.convertDistance(pt.getY(), localPrefs.tech, dxfDispUnit);
					printWriter.print(" 11\n" + formatDoubleJavaVersion(x) + "\n");
					printWriter.print(" 21\n" + formatDoubleJavaVersion(y) + "\n");
					printWriter.print(" 31\n0\n");
				} else
				{
					// should write a polyline here
					for(int i=0; i<len-1; i++)
					{
						// line
						if (points[i] == null || points[i+1] == null) continue;
						printWriter.print("  0\nLINE\n");
						printWriter.print("  8\n" + layerName + "\n");
						printWriter.print("  5\n" + getThreeDigitHex(dxfEntityHandle++) + "\n");
						Point2D pt = new Point2D.Double(points[i].getX() + xC, points[i].getY() + yC);
						trans.transform(pt, pt);
						double x = TextUtils.convertDistance(pt.getX(), localPrefs.tech, dxfDispUnit);
						double y = TextUtils.convertDistance(pt.getY(), localPrefs.tech, dxfDispUnit);
						printWriter.print(" 10\n" + formatDoubleJavaVersion(x) + "\n");
						printWriter.print(" 20\n" + formatDoubleJavaVersion(y) + "\n");
						printWriter.print(" 30\n0\n");
						pt = new Point2D.Double(points[i+1].getX() + xC, points[i+1].getY() + yC);
						trans.transform(pt, pt);
						x = TextUtils.convertDistance(pt.getX(), localPrefs.tech, dxfDispUnit);
						y = TextUtils.convertDistance(pt.getY(), localPrefs.tech, dxfDispUnit);
						printWriter.print(" 11\n" + formatDoubleJavaVersion(x) + "\n");
						printWriter.print(" 21\n" + formatDoubleJavaVersion(y) + "\n");
						printWriter.print(" 31\n0\n");
					}
					if (np == Artwork.tech().closedPolygonNode)
					{
						printWriter.print("  0\nLINE\n");
						printWriter.print("  8\n" + layerName + "\n");
						printWriter.print("  5\n" + getThreeDigitHex(dxfEntityHandle++) + "\n");
						Point2D pt = new Point2D.Double(points[len-1].getX() + xC, points[len-1].getY() + yC);
						trans.transform(pt, pt);
						double x = TextUtils.convertDistance(pt.getX(), localPrefs.tech, dxfDispUnit);
						double y = TextUtils.convertDistance(pt.getY(), localPrefs.tech, dxfDispUnit);
						printWriter.print(" 10\n" + formatDoubleJavaVersion(x) + "\n");
						printWriter.print(" 20\n" + formatDoubleJavaVersion(y) + "\n");
						printWriter.print(" 30\n0\n");
						pt = new Point2D.Double(points[0].getX() + xC, points[0].getY() + yC);
						trans.transform(pt, pt);
						x = TextUtils.convertDistance(pt.getX(), localPrefs.tech, dxfDispUnit);
						y = TextUtils.convertDistance(pt.getY(), localPrefs.tech, dxfDispUnit);
						printWriter.print(" 11\n" + formatDoubleJavaVersion(x) + "\n");
						printWriter.print(" 21\n" + formatDoubleJavaVersion(y) + "\n");
						printWriter.print(" 31\n0\n");
					}
				}
				continue;
			}

			// write all other nodes
			Poly [] polys = ni.getProto().getTechnology().getShapeOfNode(ni);
			FixpTransform trans = ni.rotateOut();
			for(int i=0; i<polys.length; i++)
			{
				Poly poly = polys[i];
				poly.transform(trans);
				if (poly.getStyle() == Poly.Type.FILLED)
				{
					printWriter.print("  0\nSOLID\n");
					printWriter.print("  8\n" + layerName + "\n");
					Point2D [] points = poly.getPoints();
					for(int j=0; j<points.length; j++)
					{
						printWriter.print(" 1" + j + "\n" + formatDoubleJavaVersion(points[j].getX()) + "\n");
						printWriter.print(" 2" + j + "\n" + formatDoubleJavaVersion(points[j].getY()) + "\n");
						printWriter.print(" 3" + j + "\n0\n");
					}
				}
			}
		}
		if (subCells)
			printWriter.print("  0\nENDBLK\n");
	}

	private String getDXFCellName(Cell cell)
	{
		if (cell.getName().equalsIgnoreCase(cell.getLibrary().getName()))
		{
			// use another name
			String buf = null;
			for(int i=1; i<1000; i++)
			{
				buf = cell.getName() + i;
				boolean found = false;
				for(Iterator<Cell> it = cell.getLibrary().getCells(); it.hasNext(); )
				{
					Cell oCell = it.next();
					if (oCell.getName().equalsIgnoreCase(buf)) { found = true;   break; }
				}
				if (!found) break;
			}
			return buf;
		}

		// just return the cell name
		return cell.getName();
	}

	private String getThreeDigit(int value)
	{
		String result = Integer.toString(value);
		while (result.length() < 3) result = " " + result;
		return result;
	}

	private String getThreeDigitHex(int value)
	{
		String result = Integer.toHexString(value).toUpperCase();
		while (result.length() < 3) result = " " + result;
		return result;
	}
}
