/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: Name.java
 * Written by Eric Kim.
 *
 * 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.simulation.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A Name is a text-parsing object for port, node and arc names.
 * These names can use bus notation:<BR>
 * <CENTER>name = itemname { ',' itemname }</CENTER>
 * <CENTER>itemname = simplename { '[' index ']' }</CENTER>
 * <CENTER>index = indexitem { ',' indexitem ']' }</CENTER>
 * <CENTER>indexitem = simplename | number ':' number</CENTER><BR>
 * <CENTER>simplename = basename [ numericalSuffix ]</CENTER><BR>
 * <CENTER>basename = string</CENTER><BR>
 * <CENTER>numericalSuffix = number</CENTER><BR>
 * string doesn't contain '[', ']', ',', ':'.
 * Bus names are expanded into a list of subnames.
 */
public class Name implements Comparable
{
	/** the original name */	private final String ons;
	/** the canonical name */	private final String ns;
	/** the lowercase name */	private final Name lowerCase;
	/** list of subnames */		private Name[] subnames;
	/** basename */				private final Name basename;
	/** numerical suffix */     private final int numSuffix;
	/** the flags */			private int flags;
	
	/** Map String -> Name */	private static Map allNames = new HashMap();

	/**
	 * Method to return the name object for this string.
	 * @param ns given string
	 * @return the name object for the string.
	 */
	public static synchronized final Name findName(String ns) { return findTrimmedName(trim(ns)); }

	/**
	 * Method to check whether or not string is a valid name.
	 * @param ns given string
	 * @return the error description or null if string is correct name.
	 */
	public static String checkName(String ns)
	{
		try
		{
			checkNameThrow(ns);
			return null;
		} catch (NumberFormatException e)
		{
			return e.getMessage();
		}
	}

	/**
	 * Returns a printable version of this Name.
	 * @return a printable version of this Name.
	 */
	public final String toString() { return ons; }

	/**
	 * Returns lowerCase equivalent of this Name.
	 * @return lowerCase equivalent of this Name.
	 */
	public final Name lowerCase() { return lowerCase; }

    /**
     * Compares this Name with the specified Name for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.<p>
     * @param   name the Name to be compared.
     * @return  a negative integer, zero, or a positive integer as this object
     *		is less than, equal to, or greater than the specified object.
     */
    public int compareTo(Name name)
	{
		if (lowerCase == name.lowerCase) return 0;
		return lowerCase.ns.compareTo(name.lowerCase.ns);
	}

    /**
     * Compares this Name with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.<p>
     * @param   o the Object to be compared.
     * @return  a negative integer, zero, or a positive integer as this object
     *		is less than, equal to, or greater than the specified object.
     * 
     * @throws ClassCastException if the specified object's type prevents it
     *         from being compared to this Object.
     */
    public int compareTo(Object o)
	{
		return compareTo((Name)o);
	}

	/**
     * Compares this <code>Name</code> to another <code>Name</code>,
     * ignoring case considerations.  Two strings are considered equal
     * ignoring case if they are of the same length, and corresponding
     * characters in the two strings are equal ignoring case.
     * @return  <code>true</code> if names are equal,
     *          ignoring case; <code>false</code> otherwise.
	 */
	public boolean equals(Object anObject)
	{
		if (this == anObject) return true;
		if (anObject instanceof Name)
		{
			Name anotherName = (Name)anObject;
			return lowerCase == anotherName.lowerCase;
		}
		return false;
	}

    /**
     * Returns a hash code for this TextDescriptor. The hash code for a
     * <code>TextDescriptor</code> object is computed as sum of its fields.
     * @return  a hash code value for this object.
     */
    public int hashCode() { return lowerCase == this ? super.hashCode() : lowerCase.hashCode(); }

	/**
	 * Tells whether or not this Name is a valid bus or signal name.
	 * @return true if Name is a valid name.
	 */
	public final boolean isValid() { return (flags & ERROR) == 0; }

	/**
	 * Tells whether or not this Name is a temporary name
	 * @return true if Name is a temporary name.
	 */
	public final boolean isTempname() { return (flags & TEMP) != 0; }

	/**
	 * Tells whether Name has duplicate subnames.
	 * @return true if Name has duplicate subnames.
	 */
	public final boolean hasDuplicates() { return (flags & DUPLICATES) != 0; }

	/**
	 * Tells whether Name has duplicate subnames.
	 * @return true if Name has duplicate subnames.
	 */
	public final boolean hasEmptySubnames() { return (flags & HAS_EMPTIES) != 0; }

	/**
	 * Tells whether or not this Name is a list of names separated by comma.
	 * @return true if Name is a list of names separated by comma.
	 */
	public final boolean isList() { return (flags & LIST) != 0; }

	/**
	 * Tells whether or not this Name is a bus name.
	 * @return true if name is a bus name.
	 */
	public final boolean isBus() { return subnames != null; }

	/**
	 * Returns subname of a bus name.
	 * @param i an index of subname.
	 * @return the view part of a parsed Cell name.
	 */
	public final Name subname(int i) { return subnames == null ? this : subnames[i]; }

	/**
	 * Returns number of subnames of a bus.
	 * @return the number of subnames of a bus.
	 */
	public final int busWidth() { return subnames == null ? 1 : subnames.length; }

	/**
	 * Returns basename of simple Name.
	 * Returns null if not simple Name.
	 * @return base of name.
	 */
	public final Name getBasename() { return basename; }

	/**
	 * Returns numerical suffix of simple Name.
	 * Returns zero if numerical suffix is absent or name is not simple.
	 * @return numerical suffix.
	 */
	public final int getNumSuffix() { return numSuffix; }

	/**
	 * Returns the name obtained from base of this simple name by adding numerical suffix.
	 * Returns null if name is not simple or if i is negative.
	 * @param i numerical suffix
	 * @return suffixed name.
	 */
	public final Name findSuffixed(int i)
	{
		if (i < 0 || basename == null) return null;
		return findName(basename.toString()+i);
	}

	// ------------------ protected and private methods -----------------------

	private static final int ERROR			= 0x01;
	private static final int LIST			= 0x02;
	private static final int BUS			= 0x04;
	private static final int SIMPLE			= 0x08;
	private static final int TEMP			= 0x10;
	private static final int DUPLICATES		= 0x20;
	private static final int HAS_EMPTIES	= 0x40;

	/**
	 * Returns the name object for this string, assuming that is is trimmed.
	 * @param ns given trimmed string
	 * @return the name object for the string.
	 */
	private static Name findTrimmedName(String ns)
	{
		Name name = (Name)allNames.get(ns);
		if (name == null && ns != null)
		{
			name = new Name(ns);
			allNames.put(name.ons, name);
		}
		return name;
	}

	/**
	 * Returns the trimmed string for given string.
	 * @param ns given string
	 * @return trimmed string, or null if argument is null
	 */
	private static String trim(String ns)
	{
        if (ns == null) return null;
		int len = ns.length();
		int newLen = 0;
		for (int i = 0; i < len; i++)
		{
			if (ns.charAt(i) > ' ') newLen++;
		}
		if (newLen == len) return ns;

		StringBuffer buf = new StringBuffer(newLen);
		for (int i = 0; i < len; i++)
		{
			if (ns.charAt(i) > ' ') buf.append(ns.charAt(i));
		}
		return buf.toString();
	}

	/**
	 * Returns the trimmed string for given string.
	 * @param ns given string
	 * @return trimmed string.
	 */
	private static String trimPlusMinus(String ns)
	{
		int len = ns.length();
		int newLen = 0;
		for (int i = 0; i < len; i++)
		{
			char ch = ns.charAt(i);
			if (ch != '+' && ch != '-') newLen++;
		}
		if (newLen == len) return ns;

		StringBuffer buf = new StringBuffer(newLen);
		for (int i = 0; i < len; i++)
		{
			char ch = ns.charAt(i);
			if (ch != '+' && ch != '-') buf.append(ns.charAt(i));
		}
		return buf.toString();
	}

	/**
	 * Constructs a <CODE>Name</CODE> (cannot be called).
	 */
	private Name(String ons)
	{
		this.ons = ons;
		this.ns = trimPlusMinus(ons);
        int suffix = 0;
        Name base = this;
		String lower = ns.toLowerCase();
		this.lowerCase = (ns.equals(lower) ? this : findTrimmedName(lower));
		try
		{
			flags = checkNameThrow(ns);
		} catch (NumberFormatException e)
		{
			flags = ERROR;
		}
		if ((flags & SIMPLE) != 0)
		{
			int l = ns.length();
			while (l > 0 && TextUtils.isDigit(ns.charAt(l-1))) l--;
			if (l == ns.length())
			{
                base = this;
			} else {
				base = findTrimmedName(ns.substring(0,l));
				suffix = TextUtils.atoi(ns.substring(l));
			}
		}
        this.numSuffix = suffix;
        this.basename = base;
        if (flags == ERROR) return;
		if ((flags & BUS) == 0) return;

		// Make subnames
		if (isList())
		{
			makeListSubNames();
			return;
		}
		int split = ns.indexOf('[');
		if (split == 0) split = ns.lastIndexOf('[');
		if (split == 0)
			makeBracketSubNames();
		else
			makeSplitSubNames(split);
	}

	/**
	 * Makes subnames of a bus whose name is a list of names separated by commas.
	 */
	private void makeListSubNames()
	{
		List subs = new ArrayList();
		for (int beg = 0; beg <= ns.length(); )
		{
			int end = beg;
			while (end < ns.length() && ns.charAt(end) != ',')
			{
				if (ns.charAt(end) == '[')
				{
					while (ns.charAt(end) != ']') end++;
				}
				end++;
			}
			Name nm = findTrimmedName(ns.substring(beg,end));
			for (int j = 0; j < nm.busWidth(); j++)
				subs.add(nm.subname(j));
			beg = end + 1;
		}
		setSubnames(subs);
	}

	/**
	 * Makes subnames of a bus whose name is indices list in brackets.
	 */
	private void makeBracketSubNames()
	{
		List subs = new ArrayList();
		for (int beg = 1; beg < ns.length(); )
		{
			int end = ns.indexOf(',', beg);
			if (end < 0) end = ns.length() - 1; /* index of ']' */
			int colon = ns.indexOf(':', beg);
			if (colon < 0 || colon >= end)
			{
				Name nm = findTrimmedName("["+ns.substring(beg,end)+"]");
				subs.add(nm);
			} else
			{
				int ind1 = Integer.parseInt(ns.substring(beg, colon));
				int ind2 = Integer.parseInt(ns.substring(colon+1, end));
				if (ind1 < ind2)
				{
					for (int i = ind1; i <= ind2; i++)
						subs.add(findTrimmedName("["+i+"]"));
				} else
				{
					for (int i = ind1; i >= ind2; i--)
						subs.add(findTrimmedName("["+i+"]"));
				}
			}
			beg = end+1;
		}
		setSubnames(subs);
	}

	private void setSubnames(List subs)
	{
		subnames = new Name[subs.size()];
		subs.toArray(subnames);

		// check duplicates
		Name[] sorted = new Name[subs.size()];
		subs.toArray(sorted);
		Arrays.sort(sorted);
		for (int i = 1; i < sorted.length; i++)
		{
			if (sorted[i].equals(sorted[i-1]))
			{
				flags |= DUPLICATES;
				break;
			}
		}
	}

	/**
	 * Makes subnames of a bus whose name consists of simpler names.
	 * @param split index dividing name into simpler names.
	 */
	private void makeSplitSubNames(int split)
	{
		// if (ns.length() == 0) return;
		if (split < 0 || split >= ns.length())
		{
			System.out.println("HEY! string is '"+ns+"' but want index "+split);
			return;
		}
		Name baseName = findTrimmedName(ns.substring(0,split));
		Name indexList = findTrimmedName(ns.substring(split));
		subnames = new Name[baseName.busWidth()*indexList.busWidth()];
		for (int i = 0; i < baseName.busWidth(); i++)
		{
			String bs = baseName.subname(i).toString();
			for (int j = 0; j < indexList.busWidth(); j++)
			{
				String is = indexList.subname(j).toString();
				subnames[i*indexList.busWidth()+j] = findTrimmedName(bs+is);
			}
		}
		if (baseName.hasDuplicates() || indexList.hasDuplicates())
			flags |= DUPLICATES;
	}

	/**
	 * Method to check whether or not string is a valid name.
	 * Throws exception on invaliod string
	 * @param ns given string
	 * @return flags describing the string.
	 */
	private static int checkNameThrow(String ns) throws NumberFormatException
	{
		int flags = SIMPLE;
		
		int bracket = -1;
		boolean wasBrackets = false;
		int colon = -1;
		if (ns.length() == 0) flags |= HAS_EMPTIES;
		for (int i = 0; i < ns.length(); i++)
		{
			char c = ns.charAt(i);
			if (bracket < 0)
			{
				colon = -1;
				if (c == ']') throw new NumberFormatException("unmatched ']' in name");
				if (c == ':') throw new NumberFormatException("':' out of brackets");
				if (c == '[')
				{
					bracket = i;
					flags &= ~SIMPLE;
					if (i == 0 || ns.charAt(i-1) == ',') flags |= HAS_EMPTIES;
					wasBrackets = true;
				} else if (c == ',')
				{
					flags |= (LIST|BUS);
					flags &= ~SIMPLE;
					if (i == 0 || ns.charAt(i-1) == ',') flags |= HAS_EMPTIES;
					wasBrackets = false;
				} else if (wasBrackets) throw new NumberFormatException("Wrong character after brackets");
				if (c == '@') flags |= TEMP;
				continue;
			}
			if (c == '[') throw new NumberFormatException("nested bracket '[' in name");
			if (c == ':')
			{
				if (colon >= 0) throw new NumberFormatException("too many ':' inside brackets");
				if (i == bracket + 1) throw new NumberFormatException("has missing start of index range");
				if (ns.charAt(bracket+1) == '-') throw new NumberFormatException("has negative start of index range");
				for (int j = bracket + 1; j < i; j++)
				{
					if (!TextUtils.isDigit(ns.charAt(j)))
						 throw new NumberFormatException("has nonnumeric start of index range");
				}
				colon = i;
				flags |= BUS;
			}
			if (colon >= 0 && (c == ']' || c == ','))
			{
				if (i == colon + 1) throw new NumberFormatException("has missing end of index range");
				if (ns.charAt(colon+1) == '-') throw new NumberFormatException("has negative end of index range");
				for (int j = colon + 1; j < i; j++)
				{
					if (!TextUtils.isDigit(ns.charAt(j)))
						 throw new NumberFormatException("has nonnumeric end of index range");
				}
				if (Integer.parseInt(ns.substring(bracket+1,colon)) == Integer.parseInt(ns.substring(colon+1,i)))
					throw new NumberFormatException("has equal start and end indices");
				colon = -1;
			}
			if (c == ']') bracket = -1;
			if (c == ',')
			{
				bracket = i;
				flags |= BUS;
			}
			if (c == '@') throw new NumberFormatException("'@' in brackets");
		}
		if ((flags & TEMP) != 0 && (flags & LIST) != 0) throw new NumberFormatException("list of temporary names");
		if (bracket != -1) throw new NumberFormatException("Unclosed bracket");
		return flags;
	}
}
