/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: Telesis.java
 * Input/output tool: Telesis output
 * Written by Gilda Garreton, Oracle Inc.
 *
 * Copyright (c) 2013, 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.io.output;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;

import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.hierarchy.HierarchyEnumerator;
import com.sun.electric.database.hierarchy.View;
import com.sun.electric.database.network.Netlist;
import com.sun.electric.database.network.Network;
import com.sun.electric.database.prototype.PortProto;
import com.sun.electric.database.topology.NodeInst;
import com.sun.electric.database.topology.PortInst;
import com.sun.electric.database.variable.VarContext;
import com.sun.electric.tool.io.FileType;
import com.sun.electric.tool.simulation.test.TextUtils;
import com.sun.electric.database.variable.Variable;
import com.sun.electric.util.math.MutableInteger;

public class Telesis extends Geometry {

	private TelesisPreferences localPrefs;
	private TelesisParamSort sortFunction;
	private Map<Cell,Cell> visitedCells = new HashMap<Cell,Cell>();
	private Map<Cell,List<TelesisParam>> telesisParamsList = new HashMap<Cell,List<TelesisParam>>();
	private Cell topCell;
	private boolean topTelesisCell; // if top cell must be treated as a package cell
	private String outputPath;

	public static class TelesisPreferences extends OutputPreferences
	{
        public TelesisPreferences(boolean factory)
		{
			super(factory);
		}

		@Override
		public Output doOutput(Cell cell, VarContext context, String filePath)
		{
			if (cell.getView() != View.SCHEMATIC)
			{
				System.out.println("Can only write Telesis for schematics cells based on information found on icon cells");
				return null;
			}
			Telesis out = new Telesis(this);
			out.outputPath = filePath;
			out.topCell = cell;
			out.topTelesisCell = false;
			TelesisVisitor visitor = out.makeTelesisVisitor(getMaxHierDepth(cell));
            if (!out.writeCell(cell, context, visitor)) // no error
            {
                System.out.println(out.outputPath + " written");
                if (out.errorLogger.getNumErrors() != 0)
                    System.out.println(out.errorLogger.getNumErrors() + " Telesis errors found");
            }
			return out.finishWrite();
	   }
	}

	/**
	 * Method to extract root name of bus name and also returns the indices
	 * @param name
	 * @param f
	 * @param l
	 * @return
	 */
	private static String extractRootNameAndArrayIndices(String name, MutableInteger f, MutableInteger l)
	{
		f.setValue(-1);
		l.setValue(-1);
		int last = -1;
		int first = name.indexOf("["); // array
		if (first != -1)
		{
			String numberArray = name.substring(first);
			name = name.substring(0, first);
			first = -1;
			last = -1;
			// look for the indices
			StringTokenizer parse = new StringTokenizer(numberArray, "[]: ", false);
			while (parse.hasMoreTokens())
	        {
				String s = parse.nextToken();
				if (first == -1)
					first = TextUtils.atoi(s);
				else if (last == -1)
					last = TextUtils.atoi(s);
				else
					assert (false); // should not reach this point
	        }
		}
		f.setValue(first);
		l.setValue(last);
		return name; // root name
	}
	
    /*********************/
    /** TelesisParam    **/
    /*********************/
	private class TelesisParam
	{
		String rootName;
		List<String> ports;
		String order;
		int first; // to know if the array of elements must be sorted out
		boolean ignorePin; // skip port during netlisting.
		
		TelesisParam(String name, List<String> ports, int first, String use, boolean ignorePin)
		{
			this.rootName = name;
			this.ports = ports;
			this.order = use;
			this.first = first;
			this.ignorePin = ignorePin;
		}
		
		String getPorts()
		{
			String output = "";
			
			if (first != -1) // sort elements
				Collections.sort(ports);
			
			for (int i = 0; i < ports.size(); i++)
			{
				String elem = ports.get(i);
				int index = elem.indexOf("]");
				assert(index != -1);
				elem = elem.substring(index+1);
				output += elem + " ";
			}
			return output;
		}
		
//		String extractRealPortName(int number)
//		{
//			String p = ports.get(0); // if number == -1 -> i = 0;
//			if (ports.size() > 1 && number != -1)
//			{
//				String key = rootName + "[" + number + "]";
//				for (String s : ports)
//				{
//					if (s.startsWith(key))
//					{
//						p = s;
//						break;
//					}
//				}
//			}
//			// remove rootName + [] + extra ' from the name
//			int index = p.indexOf("'"); // first occurrence
//			assert(index != -1);
//			int last = p.lastIndexOf("'");
//			assert(last != -1);
//			return p.substring(index+1, last);
//		}
		
		String composePortName(String nodeName, String portName)
		{
			// remove rootName + [] + extra ' from the name
			int index = portName.indexOf("'"); // first occurrence of '
			assert(index != -1);
			portName = portName.substring(index); // remove extra [] if available and get it from the first '
			return "'" + nodeName + "'." + portName + " ";
		}
		
		String extractRealPortName(int number, String nodeName, MutableInteger count)
		{
			if (ignorePin) return ""; // nothing
			
			// Ports have already those extra '
			if (number == -1 || ports.size() == 1) // have to decompose the list regardless if number=-1 or 0
			{
				String finalName = "";
				for (String s : ports)
				{
					finalName += composePortName(nodeName, s);
				}
				count.setValue(ports.size());
				return finalName;
			}
			else if (ports.size() > 1 && number != -1)
			{
				String key = rootName + "[" + number + "]";
				for (String s : ports)
				{
					if (s.startsWith(key))
					{
						count.setValue(1);
						return composePortName(nodeName, s);
					}
				}
			}
			assert(false); // should it reach this level?
			return "";
		}
		
		String getOrder()
		{
			String output = "";
			for (int i = 0; i < ports.size() - 1; i++)
				output += order + " ";
			output += order; // last one without adding the extra space
			return output;
		}
	}
	
    /*********************/
    /** TelesisParamSort**/
    /*********************/
    /**
	 * Comparator class for sorting TelesisParam by name
	 */
    private static class TelesisParamSort implements Comparator<TelesisParam>
	{
		/**
		 * Method to sort TelesisParam by their variable name.
		 */
		public int compare(TelesisParam p1, TelesisParam p2)
		{
			return p1.rootName.compareTo(p2.rootName);
		}
	}
	
	private Telesis(TelesisPreferences gp)
	{
		localPrefs = gp;
		sortFunction = new TelesisParamSort();
	}

	@Override
	protected void start()
	{
		initOutput();
	}

	@Override
	protected void done() {
		// TODO Auto-generated method stub

	}

	@Override
	protected void writeCellGeom(CellGeom cellGeom) {
		// TODO Auto-generated method stub

	}
	
	/*************************** Telesis OUTPUT ROUTINES ***************************/

	/**
	 * Method to initialize various fields, get some standard values
	 */
	private void initOutput()
	{
	}

	/****************************** VISITOR SUBCLASS ******************************/

	private TelesisVisitor makeTelesisVisitor(int maxDepth)
	{
		TelesisVisitor visitor = new TelesisVisitor(this, maxDepth);
		return visitor;
	}

	/**
	 * Class to override the Geometry visitor and add bloating to all polygons.
	 * Currently, no bloating is being done.
	 */
	private class TelesisVisitor extends Geometry.Visitor
	{
		TelesisVisitor(Geometry outGeom, int maxHierDepth)
		{
			super(outGeom, maxHierDepth);
		}

		/**
		 * Allow to visit icon cells which contain the Telesis data
		 */
		@Override
		public boolean visitIcons() 
		{ 
			return true; 
		}
		
		/**
		 * Overriding this function to write top cell information on exist
		 */
		public void exitCell(HierarchyEnumerator.CellInfo info)
		{
			Cell cell = info.getCell();
			if (cell != topCell || topTelesisCell) return; // done
			
			String cellName = cell.getName();
			System.out.println("Dealing with top cell '" + cellName + "'");
			String fileName = outputPath + "/" + cellName + "." + FileType.TELESIS.getFirstExtension();
			
			Map<Cell,List<NodeInst>> subCellsMap = new HashMap<Cell,List<NodeInst>>();
			
			for (Iterator<NodeInst> it = cell.getNodes(); it.hasNext();)
			{
				NodeInst ni = it.next();
				if (!ni.isCellInstance()) continue; // no subcell
				Cell c = (Cell)ni.getProto();
				List<NodeInst> l = subCellsMap.get(c);
				if (l == null)
				{
					l = new ArrayList<NodeInst>();
					subCellsMap.put(c, l);
				}
				l.add(ni);
			}
			
			if (!openTextOutputStream(fileName))
			{
				printWriter.println("$PACKAGES");
				// list of instances
				for (Entry<Cell,List<NodeInst>> e : subCellsMap.entrySet())
				{
					printWriter.print("! " + e.getKey().getName() + " ; ");
					for (NodeInst ni : e.getValue())
						printWriter.print(ni.getName() + " ");
					printWriter.println(" (# instances " + e.getValue().size() + ")");
				}
				printWriter.println();
				// nets
				printWriter.println("$NETS");
				Netlist netlist = cell.getNetlist();
				
				for (Iterator<Network> it = netlist.getNetworks(); it.hasNext();)
				{
					Network net = it.next();
					int count = 0;
					String name = net.getName();
					int startBus = name.indexOf("[");
					int number = -1;
					if (startBus != -1) // conform bus name a[1] -> a__1
					{
						int endBus = name.indexOf("]");
						assert(endBus != -1);
						number = TextUtils.atoi(name.substring(startBus+1, endBus));
						String rootName = name.substring(0, startBus);
						name = rootName + "__" + number;
					}
					StringBuffer netString = new StringBuffer("'" + name + "' ; ");
					
					for (Iterator<PortInst> itP = net.getPorts(); itP.hasNext(); )
					{
						PortInst p = itP.next();
						NodeInst pi = p.getNodeInst();
						if (!pi.isCellInstance()) continue; // skipping non-subcell instances
						Cell c = (Cell)pi.getProto();
						if (c.isSchematic())
						{
							// look for icon cell since Telesis data is there
							Cell iconC = c.iconView();
							assert(iconC != null);
							c = iconC;
						}
						PortProto ex = p.getPortProto();
						String exName = ex.getName();
						String rootName = exName;
						startBus = exName.indexOf("[");
						if (startBus != -1) // only line is needed
						{
							assert(number != -1);
							rootName = exName.substring(0, startBus);
							exName = exName.substring(0, startBus) + "[" + number + "]";
						}
						List<TelesisParam> l = telesisParamsList.get(c);
						String theNodeName = p.getNodeInst().getName();
						String theFinalName = "'" + p.getNodeInst().getName() + "'.'" + exName + "' ";
						MutableInteger portCount = new MutableInteger(1);
						if (l != null)
						{
							for (TelesisParam ap : l)
							{
								if (ap.rootName.equals(rootName))
								{
									theFinalName = ap.extractRealPortName(number, theNodeName, portCount);
									break;
								}
							}
						}
						if (theFinalName.isEmpty()) // it is empty when port must be ignore
							continue;
						netString.append(theFinalName);
						count += portCount.intValue();
					}
					if (count == 0) continue; // not connected net
					printWriter.print(formatString(netString));
					printWriter.println(" (# of ports " + count + ")");
				}
				
				// closing the file
				closeTextOutputStream();
			}
		}
		
		/**
		 * Overriding this class allows us to write information for the corresponding icon cell
		 */
		public boolean enterCell(HierarchyEnumerator.CellInfo info)
        {
			Cell cell = info.getCell();
		
			if (visitedCells.get(cell) != null)
				return false; // done with this cell

			visitedCells.put(cell, cell);
			
			String cellName = cell.getName();
			String fileName = outputPath + "/" + cellName + "." + FileType.TELESIS.getFirstExtension();
			boolean telesisCell = false;

			Cell iconCell = (cell.isIcon()) ? cell: cell.iconView(); // trying to get its corresponding icon cell
			String packageInfo = "";
			String classInfo = "";
			List<TelesisParam> pinsList = new ArrayList<TelesisParam>();
			List<TelesisParam> pinsUndefinedList = new ArrayList<TelesisParam>();
			int totalNumberOfPins = 0;
			
			if (iconCell == null)
			{
				System.out.println("No icon cell found for cell '" + cellName + "'");
			}
			else
			{
				// check if cell has any Allegro variable. If not, it is considered as the top cell
				// Look for variables related to Allegro
				for (Iterator<Variable> it = iconCell.getVariables(); it.hasNext(); )
				{
					Variable var = it.next();
					boolean pinsHeadFound = false; // to allow multiple lines with pins info
					for (int i = 0; i < var.getLength(); i++)
					{
						String vS = (String)var.getObject(i); // I know it is a string 
						int index = vS.indexOf("-text"); // look for beginning of important info
						if (!pinsHeadFound) assert(index != -1);
						if (vS.toLowerCase().contains("allegro_package"))
							packageInfo = vS.substring(index+5).trim();
						else if (vS.toLowerCase().contains("allegro_class"))
							classInfo = vS.substring(index+5).trim();
						else if (vS.toLowerCase().contains("allegro_pins") || pinsHeadFound)
						{
							if (!pinsHeadFound)
								pinsHeadFound = vS.toLowerCase().contains("allegro_pins");
							String pinsInfo = (pinsHeadFound) ? vS.trim() : vS.substring(index+5).trim();
							int pos = 0;
							int len = pinsInfo.length();
							while (pos < len)
							{
								// look for set of elements {name pins type}
								int start = pinsInfo.indexOf("{", pos);
								if (start == -1) break; // done
								if (start < len && pinsInfo.charAt(start) == '{') start++;
								// look for matching '}' discarding intermediate pairs of {}
								int end = start;
								int numCurly = 1;
								while (end < len && numCurly != 0)
								{
									if (pinsInfo.charAt(end) == '{') numCurly++;
									else if (pinsInfo.charAt(end) == '}') numCurly--;
									end++;
								}
								String elem = pinsInfo.substring(start, --end).trim(); // trim in case there are empty space at the end or start
								pos = end++; // ++ to remove the last '}'
								// get the 3 elements from elem. The second might have extra {}
								// get the name first. If name has [] -> break it now
								int first = elem.indexOf(" "); // look for first white space
								int last = elem.lastIndexOf(" "); // last space
								assert(first != -1 && last != -1);
								String name = elem.substring(0, first).trim();
								String port = elem.substring(first, last).trim();
								String use = elem.substring(last, elem.length()).trim();
								
								// special cases -> if use = {} or port=ignorenilpin
								boolean ignorePin = port.toLowerCase().startsWith("ignorenilpin");
								
								if (use.toLowerCase().equals("fiducial"))
									use = "unspec";
								boolean undefined = use.toLowerCase().equals("unspec");
								
								MutableInteger f = new MutableInteger(-1);
								MutableInteger l = new MutableInteger(-1);
								name = extractRootNameAndArrayIndices(name, f, l);								
								last = l.intValue();
								first = f.intValue();

								StringTokenizer parse = new StringTokenizer(port, "{} ", false);
								boolean descend = (first > last);
								int count = first;
								// No need of storing A[0], A[1] as string since those names are not used at the end
								List<String> list = new ArrayList<String>();
								while (parse.hasMoreTokens())
						        {
									String s = parse.nextToken();
									String number = name;
									number += "[" + count + "]";
									if (first != -1)
									{
										// the [<number>] is needed for the sorting. Cadence needs A[0], A[10], A[11], ....
										if (descend) count--; else count++;
									}
									// check if s is not an array
									s = extractRootNameAndArrayIndices(s, f, l);
									int localFirst = f.intValue();
									int localLast = l.intValue();
									if (localFirst == -1) // just one element
										list.add(number + "'" + s + "'"); // it will extract real name after ']'
									else
									{
										boolean localDesc = (localFirst > localLast);
										int localLen = (localDesc) ? (localFirst - localLast + 1) : (localLast - localFirst + 1);
										int localCount = localFirst;
										for (int j = 0; j < localLen; j++)
										{
											String localNumer = name + "[" + localCount + "]";
											list.add(localNumer + "'" + s + "[" + localCount + "]'"); // it will extract real name after ']'
											if (localDesc) localCount--; else localCount++;
										}
									}
						        }
								TelesisParam param = new TelesisParam(name, list, first, use, ignorePin);
								if (!ignorePin) totalNumberOfPins += list.size();
								if (undefined)
									pinsUndefinedList.add(param);
								else
									pinsList.add(param);
							}
						}
					}
				}
				// Sort pins alphabetically CADENCE style
				Collections.sort(pinsList, sortFunction);
				Collections.sort(pinsUndefinedList, sortFunction);
				
				pinsList.addAll(pinsUndefinedList);
				
				telesisCell = pinsList.size() > 0 || !packageInfo.isEmpty() || !classInfo.isEmpty();
			}

			if (cell == topCell)
				topTelesisCell = telesisCell;
			
			if (!telesisCell)
			{
				System.out.println("No Allegro information found in '" + cellName + "'. Skipping cell");
				return super.enterCell(info); // nothing to do here
			}

			System.out.println("Writing Telesis format for Cell " + cellName);
			if (!openTextOutputStream(fileName))
			{

				printWriter.println("(Device file for " + cellName + ")");
				telesisParamsList.put(iconCell, pinsList);
				
				StringBuffer order = new StringBuffer("PINORDER '" + cellName + "' ");
				StringBuffer uses = new StringBuffer("PINUSE '" + cellName + "' ");
				StringBuffer ports = new StringBuffer("FUNCTION G1 '" + cellName + "' ");
				for (TelesisParam p : pinsList)
				{
					if (p.ignorePin) continue;
					
					order.append(p.getPorts());
					uses.append(p.getOrder() + " ");
					ports.append(p.getPorts());
				}
				printWriter.println("PACKAGE '" + packageInfo + "'");
				printWriter.println("CLASS " + classInfo);
				printWriter.println("PINCOUNT " + totalNumberOfPins);
				printWriter.println(formatString(order));
				printWriter.println(formatString(uses));
				printWriter.println(formatString(ports));
				printWriter.println("END");
				closeTextOutputStream();
			}
			return super.enterCell(info);
        }
	}
	
	private static String formatString(StringBuffer sb)
	{
		int maxLen = 68; // the value has been adjusted to match SUE output
		int len = sb.length();
		if (len <= maxLen) 
			return sb.toString();
		
		int front = sb.indexOf(" "); // cut at white spaces
		int last = 0;
		assert(front != -1);
		int lineCount = last;
		while (front < len)
		{
			front = sb.indexOf(" ", last + 1); // cut at white spaces
			if (front == -1) // done
				break;
			int tmp = lineCount + front - last;
			if (tmp > maxLen) // cut line in the previous
			{
				sb.insert(last+1, ",\n");
				lineCount = 0;
				last = front;
			}
			else
			{
				last = front;
				lineCount = tmp;
			}
		}
		return sb.toString();
	}
}
