/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: ErrorLogger.java
 *
 * 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.user;

import com.sun.electric.database.ImmutableNodeInst;
import com.sun.electric.database.geometry.EPoint;
import com.sun.electric.database.geometry.PolyBase;
import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.hierarchy.EDatabase;
import com.sun.electric.database.hierarchy.Export;
import com.sun.electric.database.hierarchy.Library;
import com.sun.electric.database.hierarchy.View;
import com.sun.electric.database.id.CellId;
import com.sun.electric.database.id.IdReader;
import com.sun.electric.database.id.IdWriter;
import com.sun.electric.database.topology.Geometric;
import com.sun.electric.database.variable.VarContext;
import com.sun.electric.tool.Job;
import com.sun.electric.util.TextUtils;
import com.sun.electric.util.collections.ArrayIterator;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Class for logging errors.
 * Holds a log of errors:
 * <p>ErrorLogger errorLogger = ErrorLogger.newInst(String s): get new logger for s
 * <p>MessageLog errorLog = errorLogger.logError(string msg, cell c, int k):
 * Create a new log with message 'msg', for cell 'c', with sortKey 'k'.
 * <p>Various methods for adding highlights to errorLog:
 * <p>To end logging, call errorLogger.termLogging(boolean explain).
 */
public class ErrorLogger implements Serializable
{
//	private final static int MAX_MESSAGELOGS = 100000;
	
    /**
     * Method to replace invalid characters in XML such as > or <
     * @param message
     * @return corrected String.
     */
    private static String correctXmlString(String message)
    {
        String m = message.replaceAll(">", "&gt;");
        m = m.replaceAll("<", "&lt;");
        m = m.replaceAll("&", "&amp;");
        return m;
    }

    /**
     * Function to write all headers related to ErrorLogger classes.
     * @param indent
     * @param ps
     */
    public static void writeXmlHeader(String indent, PrintStream ps)
    {
        // ErrorLogger and GroupLog
        ps.println(indent + "<!ELEMENT ErrorLogger (GroupLog|MessageLog|WarningLog)*>");
        ps.println(indent + "<!ELEMENT GroupLog (MessageLog|WarningLog)*>");

        ps.println(indent + "<!ATTLIST ErrorLogger");
        ps.println(indent + "   errorSystem CDATA #REQUIRED");
        ps.println(indent + ">");

        ps.println(indent + "<!ATTLIST GroupLog");
        ps.println(indent + "   message CDATA #REQUIRED");
        ps.println(indent + ">");

        // For MessageLog and WarningLog
        ps.println(indent + "<!ELEMENT MessageLog ("+ ErrorHighlight.getImplementedXmlHeaders() +")*>");

        ps.println(indent + "<!ATTLIST MessageLog");
        ps.println(indent + "   message CDATA #REQUIRED");
        ps.println(indent + "   cellName CDATA #REQUIRED");
        ps.println(indent + ">");

        ps.println(indent + "<!ELEMENT WarningLog ANY>");
        ps.println(indent + "<!ATTLIST WarningLog");
        ps.println(indent + "   message CDATA #REQUIRED");
        ps.println(indent + "   cellName CDATA #IMPLIED"); // only WarningLogs can have no cells
        ps.println(indent + ">");
    }

    /**
     * Create a Log of a single message.
     */
    public static class MessageLog implements Comparable<MessageLog>, Serializable {
        private final String message;
        final CellId logCellId;                // cell associated with log (not really used)
        private final int    sortKey;
        private final ErrorHighlight[] highlights;
        protected int    index;

        public MessageLog(String message, Cell cell, int sortKey, List<ErrorHighlight> highlights) {
            this(message, cell != null ? (CellId)cell.getId() : null, sortKey, highlights);
        }

        public MessageLog(String message, CellId logCellId, int sortKey, List<ErrorHighlight> highlights) {
            this.message = message;
            this.logCellId = logCellId;
            this.sortKey = sortKey;
            this.highlights = highlights.toArray(ErrorHighlight.NULL_ARRAY);
            index = 0;
        }

        public Cell getCell() 
        { 
        	if (logCellId == null) return null;
        	
        	EDatabase db = EDatabase.currentDatabase();
        	if (db == null) return null;
        	Cell c = db.getCell(logCellId);
        	if (c != null) return c;
        	
        	// try the server Database?
        	if (EDatabase.serverDatabase() != null)
        		c = EDatabase.serverDatabase().getCell(logCellId);
        	return c;
        }

        // cellName is required so an empty string will be used if not cell is found
        private String getCellName()
        {
            Cell c = getCell();
            return (c != null ? c.describe(false) : "NotFound{lay}");
        }

        public String getMessageString() { return message; }

        public int getNumHighlights() {return highlights.length;}

        public Iterator<ErrorHighlight> getHighlights()
        {
        	return ArrayIterator.iterator(highlights);
        }

        public int getSortKey() { return sortKey; }

        /**
         * Compare objects lexicographically based on string comparator CASE_INSENSITIVE_ORDER
         * This method doesn't guarantee (compare(x, y)==0) == (x.equals(y))
         * @param log1
         * @return Returns a negative integer, zero, or a positive integer as the
         * first message has smaller than, equal to, or greater than the second lexicographically
         */
        public int compareTo(MessageLog log1)
        {
            return (String.CASE_INSENSITIVE_ORDER.compare(message, log1.message));
        }

        public boolean findGeometries(Geometric geo1, Cell cell1, Geometric geo2, Cell cell2)
        {
            boolean eh1found = false;
            boolean eh2found = false;

            for(ErrorHighlight eh : highlights)
            {
                if (eh.containsObject(cell1, geo1))
                    eh1found = true;
                if (eh.containsObject(cell2, geo2))
                    eh2found = true;
                if (eh1found && eh2found)
                    return (true);
            }
            return (false);
        }

        /**
         * Method to describe this error.
         */
        public String getMessage()
        {
            return "["+index+"] "+message;
        }

        protected void writeXmlDescription(PrintStream msg)
        {
            String className = this.getClass().getSimpleName();
            String cellInfo = "cellName=\"" + getCellName() + "\"";

            // replace those characters that XML defines as special such as ">" and "&"
            String m = correctXmlString(message);
            msg.append("\t<" + className + " message=\"" + m + "\" " + cellInfo + ">\n");
            // In case the ErrorLogger was created while using Electric's GUI
            for(ErrorHighlight eh : highlights)
            {
                eh.writeXmlDescription("\t\t", msg, EDatabase.currentDatabase());
            }
            msg.append("\t</" + className + ">\n");
        }

        /**
         * Returns true if this error log is still valid
         * (In a linked Cell, and all highlights are still valid)
         */
        public boolean isValid(EDatabase database) {
            if (logCellId == null) return true;
            if (database.getCell(logCellId) == null) return false;
            // check validity of highlights
            boolean allValid = true;
            for (ErrorHighlight erh : highlights) {
                if (!erh.isValid(database)) { allValid = false; break; }
            }
            return allValid;
        }

        void write(IdWriter writer) throws IOException {
            boolean isWarning = this instanceof WarningLog;
            writer.writeBoolean(isWarning);
            writer.writeString(message);
            boolean hasCellId = logCellId != null;
            writer.writeBoolean(hasCellId);
            if (hasCellId)
                writer.writeNodeProtoId(logCellId);
            writer.writeInt(sortKey);
            writer.writeInt(highlights.length);
            for (int i = 0; i < highlights.length; i++)
                highlights[i].write(writer);
            writer.writeInt(index);
        }

        private static MessageLog read(IdReader reader) throws IOException {
            boolean isWarning = reader.readBoolean();
            String message = reader.readString();
            boolean hasCellId = reader.readBoolean();
            CellId cellId = hasCellId ? (CellId)reader.readNodeProtoId() : null;
            int sortKey = reader.readInt();
            ErrorHighlight[] highlights = new ErrorHighlight[reader.readInt()];
            for (int i = 0; i < highlights.length; i++)
                highlights[i] = ErrorHighlight.read(reader);
            MessageLog log;
            if (isWarning)
                log = new WarningLog(message, cellId, sortKey, Arrays.asList(highlights));
            else
                log = new MessageLog(message, cellId, sortKey, Arrays.asList(highlights));
            log.index = reader.readInt();
            return log;
        }
    }

    private static class ErrorLogOrder implements Comparator<MessageLog>
    {
    	public int compare(MessageLog el1, MessageLog el2)
        {
            int sortedKey = el1.sortKey - el2.sortKey;
            if (sortedKey == 0) // Identical, compare lexicographically
                sortedKey = el1.compareTo(el2);
            return sortedKey; //el1.sortKey - el2.sortKey;
        }
    }

    /**
     * Create a Log of a single warning.
     */
    public static class WarningLog extends MessageLog
    {
       public WarningLog(String message, Cell cell, int sortKey, List<ErrorHighlight> highlights) { super(message, cell, sortKey, highlights); }
       public WarningLog(String message, CellId cellId, int sortKey, List<ErrorHighlight> highlights) { super(message, cellId, sortKey, highlights); }
    }

//	private boolean alreadyExplained;
    private int errorLimit;
    private List<MessageLog> allErrors = new ArrayList<MessageLog>();
	private List<WarningLog> allWarnings = new ArrayList<WarningLog>();
    private boolean limitExceeded;
    private String errorSystem;
    private boolean terminated;
    private boolean persistent; // cannot be deleted
    private boolean disablePopups;
    private Map<Integer,String> sortKeysToGroupNames; // association of sortKeys to GroupNames
    Map<Geometric,Set<MessageLog>> findLogs = new HashMap<Geometric,Set<MessageLog>>();

    public Map<Integer,String> getSortKeyToGroupNames() { return sortKeysToGroupNames; }

    public String getSystem() { return errorSystem; }

    public boolean isPersistent() { return persistent; }

    public boolean isPopupsDisabled() { return disablePopups; }

    public void disablePopups() { disablePopups = true; }

    public ErrorLogger() {}

    public void write(IdWriter writer) throws IOException {
        writer.writeDiffs();
        writer.writeInt(errorLimit);
        int numErrors = allErrors.size();
        writer.writeInt(numErrors);
        for (int i = 0; i < numErrors; i++)
            allErrors.get(i).write(writer);
        int numWarnings = allWarnings.size();
        writer.writeInt(numWarnings);
        for (int i = 0; i < numWarnings; i++)
            allWarnings.get(i).write(writer);
        writer.writeString(errorSystem);
        writer.writeBoolean(terminated);
        writer.writeBoolean(persistent);
        if (sortKeysToGroupNames != null) {
            writer.writeInt(sortKeysToGroupNames.size());
            for (Map.Entry<Integer,String> e: sortKeysToGroupNames.entrySet()) {
                writer.writeInt(e.getKey().intValue());
                writer.writeString(e.getValue());
            }
        } else {
            writer.writeInt(-1);
        }
    }

    public static ErrorLogger read(IdReader reader) throws IOException {
        reader.readDiffs();
        ErrorLogger logger = new ErrorLogger();
        logger.errorLimit = reader.readInt();
        int numErrors = reader.readInt();
        for (int i = 0; i < numErrors; i++)
            logger.allErrors.add(MessageLog.read(reader));
        int numWarnings = reader.readInt();
        for (int i = 0; i < numWarnings; i++)
            logger.allWarnings.add((WarningLog)MessageLog.read(reader));
        logger.errorSystem = reader.readString();
        logger.terminated = reader.readBoolean();
        logger.persistent = reader.readBoolean();
        int numGroups = reader.readInt();
        if (numGroups >= 0) {
            logger.sortKeysToGroupNames = new HashMap<Integer,String>();
            for (int i = 0; i < numGroups; i++) {
                Integer sortKey = Integer.valueOf(reader.readInt());
                String groupName = reader.readString();
                logger.sortKeysToGroupNames.put(sortKey, groupName);
            }
        }
        return logger;
    }

    /**
     * Create a new ErrorLogger instance.
     * @return a new ErrorLogger for logging errors
     */
    public static ErrorLogger newInst(String system) {
        return newInst(system, false);
    }

    /**
     * Create a new ErrorLogger instance.
     * @return a new ErrorLogger for logging errors
     */
    public static ErrorLogger newInst(String system, boolean persistent)
    {
        ErrorLogger logger = new ErrorLogger();
        logger.limitExceeded = false;
        logger.errorSystem = system;
        logger.errorLimit = User.getErrorLimit();
        logger.terminated = false;
        logger.persistent = persistent;
        logger.disablePopups = false;
//        logger.alreadyExplained = false;
        logger.sortKeysToGroupNames = null;
        return logger;
    }

    public void addMessages(List<MessageLog> messages)
    {
        if (messages == null) return; // to avoid to create empty lists during incremental checking
        for (MessageLog m: messages) {
            if (m instanceof WarningLog)
                allWarnings.add((WarningLog)m);
            else
                allErrors.add(m);
            hashMessage(m);
        }
//        if (persistent) Job.getUserInterface(). wantToRedoErrorTree();
    }

    public void addMessages(ErrorLogger logger)
    {
        allWarnings.addAll(logger.allWarnings);
        for(MessageLog ml : logger.allWarnings) hashMessage(ml);
        allErrors.addAll(logger.allErrors);
        for(MessageLog ml : logger.allErrors) hashMessage(ml);
    }

    public void deleteMessages(List<MessageLog> messages)
    {
        if (messages == null) return; // to avoid to increase empty lists during incremental checking
        for (MessageLog m: messages) {
            if (m instanceof WarningLog)
                allWarnings.remove(m);
            else
                allErrors.remove(m);
            unHashMessage(m);
        }
//        if (persistent) Job.getUserInterface(). wantToRedoErrorTree();
    }

    private void hashMessage(MessageLog ml)
    {
        for(ErrorHighlight eh : ml.highlights)
        {
        	Object obj = eh.getObject(EDatabase.currentDatabase());
        	if (obj == null) continue;
        	if (obj instanceof Geometric)
        	{
        		Geometric geom = (Geometric)obj;
            	Set<MessageLog> theList = findLogs.get(geom);
            	if (theList == null) findLogs.put(geom, theList = new HashSet<MessageLog>());
            	theList.add(ml);
        	}
        }
    }

    private void unHashMessage(MessageLog ml)
    {
        for(ErrorHighlight eh : ml.highlights)
        {
        	Object obj = eh.getObject(EDatabase.currentDatabase());
        	if (obj == null) continue;
        	if (obj instanceof Geometric)
        	{
        		Geometric geom = (Geometric)obj;
            	Set<MessageLog> theList = findLogs.get(geom);
            	if (theList == null) continue;
            	theList.remove(ml);
        	}
        }
    }

    /**
     * Factory method to create an error message and log.
     * with the given text "message" applying to cell "cell".
     * Returns a pointer to the message (0 on error) which can be used to add highlights.
     */
    private synchronized MessageLog logAnError(String message, Cell cell, int sortKey, List<ErrorHighlight> highlights) {
        CellId cellId = cell != null ? cell.getId() : null;
        return logAnError(message, cellId, sortKey, highlights);
    }

    /**
     * Factory method to create an error message and log.
     * with the given text "message" applying to cell "cell".
     * Returns a pointer to the message (0 on error) which can be used to add highlights.
     */
    private synchronized MessageLog logAnError(String message, CellId cellId, int sortKey, List<ErrorHighlight> highlights)
    {
        if (terminated && !persistent) {
            System.out.println("WARNING: "+errorSystem+" already terminated, should not log new error");
        }

        // if too many errors, don't save it
        // errorLimit = 0 -> oo
        if ((errorLimit > 0 && getNumErrors() >= errorLimit))
        //if ((errorLimit > 0 && getNumErrors() >= errorLimit) || (errorLimit == 0 && getNumErrors() > MAX_MESSAGELOGS))
        {
            if (!limitExceeded)
            {
                System.out.println("WARNING: more than " + errorLimit + " errors found, ignoring the rest");
                limitExceeded = true;
            }
            return null;
        }

        // create a new ErrorLog object
        MessageLog el = new MessageLog(message, cellId, sortKey, highlights);

        // add the ErrorLog into the global list
        allErrors.add(el);
//        currentLogNumber = allErrors.size()-1;

//        if (persistent) Job.getUserInterface().wantToRedoErrorTree();
        return el;
    }

    /**
     * Factory method to log an error message.
     * @param message the string to display.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logError(String message, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
    	logAnError(message, (CellId)null, sortKey, h);
    }

    /**
     * Factory method to log an error message.
     * @param message the string to display.
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logError(String message, Cell cell, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
    	logAnError(message, cell, sortKey, h);
    }

    /**
     * Factory method to log an error message.
     * @param message the string to display.
     * @param cellId the Id of the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logError(String message, CellId cellId, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
    	logAnError(message, cellId, sortKey, h);
    }

    /**
     * Factory method to log an error message.
     * @param message the string to display.
     * @param geom the node or arc to display
     * @param cell the cell in which this message applies.
     * @param context the VarContext of the Cell.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logError(String message, Geometric geom, Cell cell, VarContext context, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
        h.add(ErrorHighlight.newInst(context, geom));
    	logAnError(message, cell, sortKey, h);
    }

    /**
     * Factory method to log an error message.
     * @param message the string to display.
     * @param pp the Export to display
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logError(String message, Export pp, int sortKey)
    {
        Cell cell = pp.getParent();
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
        h.add(new ErrorHighExport(null, pp));
    	logAnError(message, cell, sortKey, h);
    }

    /**
     * Factory method to log an error message.
     * @param message the string to display.
     * @param pt the point to display
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logError(String message, EPoint pt, Cell cell, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
        h.add(new ErrorHighPoint(cell, pt));
    	logAnError(message, cell, sortKey, h);
    }

    /**
     * Factory method to log an error message.
     * @param message the string to display.
     * @param poly the polygon to display
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logError(String message, PolyBase poly, Cell cell, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
        Point2D [] points = poly.getPoints();
        for(int i=0; i<points.length; i++)
        {
            int prev = i-1;
            if (i == 0) prev = points.length-1;
            h.add(ErrorHighlight.newInst(cell, points[prev], points[i]));
        }
    	logAnError(message, cell, sortKey, h);
    }

    /**
     * Factory method to log an error or warning message.
     * @param message the string to display.
     * @param objs a collection of nodes, arcs, exports or polygons, points to display. Must not be null.
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     * @param isErrorMsg true if an error message is logged
     */
    public synchronized void logMessage(String message, Collection<?> objs, Cell cell, int sortKey, boolean isErrorMsg)
    {
        logMessageWithLines(message, objs, null, cell, sortKey, isErrorMsg);
    }

    /**
     * Factory method to log an error or warning message with extra lines.
     * @param message the string to display.
     * @param objs a collection of nodes, arcs, exports or polygons, points to display. Must not be null.
     * @param lineList a collection of points defining a set of lines (may be null)
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     * @param isErrorMsg true if an error message is logged
     */
    public synchronized MessageLog logMessageWithLines(String message, Collection<?> objs, List<EPoint> lineList,
                                                 Cell cell, int sortKey, boolean isErrorMsg)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();

        if (objs != null)
        {
            for (Object obj : objs)
            {
                if (obj instanceof Geometric)
                {
                     h.add(ErrorHighlight.newInst(null, (Geometric)obj));
                }
                else if (obj instanceof ImmutableNodeInst)
                {
                    h.add(ErrorHighlight.newInst(cell.getId(), (ImmutableNodeInst)obj));
                }
                else if (obj instanceof Export)
                {
                    h.add(new ErrorHighExport(null, (Export)obj));
                }
                else if (obj instanceof EPoint)
                {
                    h.add(new ErrorHighPoint(cell, (EPoint)obj));
                }
                else if (obj instanceof Rectangle2D)
                {
                	Rectangle2D rect = (Rectangle2D)obj;
                	List<ErrorHighlight> l = new ArrayList<ErrorHighlight>();
                	// points clockwise
                	EPoint p1 = EPoint.fromLambda(rect.getX(), rect.getY());
                	EPoint p2 = EPoint.fromLambda(rect.getX(), rect.getMaxY());
                	EPoint p3 = EPoint.fromLambda(rect.getMaxX(), rect.getMaxY());
                	EPoint p4 = EPoint.fromLambda(rect.getMaxX(), rect.getY());
                	//  p1-p2
                	l.add(new ErrorHighLine(cell, p1, p2, true));
                	//  p2-p3
                	l.add(new ErrorHighLine(cell, p2, p3, true));
                	//  p3-p4
                	l.add(new ErrorHighLine(cell, p3, p4, true));
                	//  p4-p1
                	l.add(new ErrorHighLine(cell, p4, p1, true));
                	h.add(new ErrorHighPoly(cell, null, l));
                }
                else if (obj instanceof PolyBase)
                {
                    PolyBase poly = (PolyBase)obj;
                    Point2D [] points = poly.getPoints();
                    List<ErrorHighlight> l = new ArrayList<ErrorHighlight>();

                    for(int i=0; i<points.length; i++)
                    {
                        int prev = i-1;
                        if (i == 0) prev = points.length-1;
                        l.add(new ErrorHighLine(cell, EPoint.fromLambda(points[prev].getX(), points[prev].getY()),
                        		EPoint.fromLambda(points[i].getX(), points[i].getY()), true));
                    }
                    h.add(new ErrorHighPoly(cell, null, l));
                }
                else
                    assert(false);
            }
        }
        if (lineList != null)
        {
    		for(int i=0; i<lineList.size(); i += 2)
    		{
    			if (i < lineList.size() - 1)
    				h.add(new ErrorHighLine(cell, lineList.get(i), lineList.get(i+1), true));
    			else
    				System.out.println("Out of range for lineList. Check this list with i=" + i + " size=" + lineList.size() 
    						+ " elements associated to message '" + message + "'.");
    		}
        }
        if (isErrorMsg) return logAnError(message, cell, sortKey, h);
        return logAWarning(message, cell, sortKey, h);
    }

    /**
     * Factory method to log an error or a warning message.
     * @param message the string to display.
     * @param geomList a list of nodes or arcs to display (may be null).
     * @param polyList a list of polygons to display (may be null).
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     * @param errorMsg
     */
    public synchronized void logMessage(String message, List<Geometric> geomList, List<PolyBase> polyList,
                                        Cell cell, int sortKey, boolean errorMsg)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
        boolean matches = geomList != null && polyList != null && geomList.size() == polyList.size();
    	if (geomList != null && !matches)
    	{
    		for(Geometric geom : geomList)
                h.add(ErrorHighlight.newInst(null, geom));
    	}
    	if (polyList != null)
    	{
            // must match number of elements
            for(PolyBase poly : polyList)
    		{
    	        Point2D [] points = poly.getPoints();
                List<ErrorHighlight> list = new ArrayList<ErrorHighlight>();

                for(int i=0; i<points.length; i++)
    	        {
    	            int prev = i-1;
    	            if (i == 0) prev = points.length-1;
    	            list.add(new ErrorHighLine(cell, EPoint.fromLambda(points[prev].getX(), points[prev].getY()),
    	            		EPoint.fromLambda(points[i].getX(), points[i].getY()), true));
                }
                h.add(new ErrorHighPoly(cell, null, list));
            }
    	}

        if (errorMsg)
            logAnError(message, cell, sortKey, h);
        else
            logAWarning(message, cell, sortKey, h);
    }

    /**
     * Factory method to log a warning message.
     * @param message the string to display.
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     */
    private synchronized MessageLog logAWarning(String message, Cell cell, int sortKey, List<ErrorHighlight> highlights)
    {
        if (terminated && !persistent) {
            System.out.println("WARNING: "+errorSystem+" already terminated, should not log new warning");
        }

        // if too many errors, don't save it
        if (errorLimit > 0 && getNumWarnings() >= errorLimit)
        {
            if (!limitExceeded)
            {
                System.out.println("WARNING: more than " + errorLimit + " warnings found, ignoring the rest");
                limitExceeded = true;
            }
            return null;
        }

        // create a new ErrorLog object
        WarningLog el = new WarningLog(message, cell, sortKey, highlights);

//        // store information about the error
//        el.highlights = new ArrayList<ErrorHighlight>();

        // add the ErrorLog into the global list
        allWarnings.add(el);
        hashMessage(el);

//        if (persistent) Job.getUserInterface().wantToRedoErrorTree();
        return el;
    }

    /**
     * Factory method to log a warning message.
     * @param message the string to display.
     * @param cell the cell in which this message applies.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logWarning(String message, Cell cell, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
    	logAWarning(message, cell, sortKey, h);
    }

    /**
     * Factory method to log a warning message.
     * @param message the string to display.
     * @param geom a node or arc to display.
     * @param cell the cell in which this message applies.
     * @param context the VarContext of the Cell.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logWarning(String message, Geometric geom, Cell cell, VarContext context, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
        h.add(ErrorHighlight.newInst(context, geom));
    	logAWarning(message, cell, sortKey, h);
    }

    /**
     * Factory method to log a warning message.
     * @param message the string to display.
     * @param pp an Exports to display.
     * @param cell the cell in which this message applies.
     * @param context the VarContext of the Cell.
     * @param sortKey the sorting order of this message.
     */
    public synchronized void logWarning(String message, Export pp, Cell cell, VarContext context, int sortKey)
    {
    	List<ErrorHighlight> h = new ArrayList<ErrorHighlight>();
        h.add(new ErrorHighExport(context, pp));
    	logAWarning(message, cell, sortKey, h);
    }

    public synchronized int getNumMessages(Cell cell, boolean searchInError) {
        int numErrors = 0;

        if (searchInError)
        {
            for (int i=0; i<allErrors.size(); i++)
            {
                MessageLog el = allErrors.get(i);
                if (el.logCellId == cell.getId())
                    numErrors++;
            }
        }
        else
        {
            for (int i=0; i<allWarnings.size(); i++)
            {
                MessageLog el = allWarnings.get(i);
                if (el.logCellId == cell.getId())
                    numErrors++;
            }
        }

        return numErrors;
    }

    /**
	 * Method to determine if existing report was not logged already
	 * as error or warning
	 */
	public synchronized boolean findMessage(Cell cell, Geometric geom1, Cell cell2, Geometric geom2, boolean searchInError)
	{
		Set<MessageLog> theList = findLogs.get(geom1);
		if (theList != null)
		{
			for(MessageLog ml : theList)
				if (ml.findGeometries(geom1, cell, geom2, cell2)) return true;
		}
		theList = findLogs.get(geom2);
		if (theList != null)
		{
			for(MessageLog ml : theList)
				if (ml.findGeometries(geom1, cell, geom2, cell2)) return true;
		}

//		List<?> list = (searchInError) ? allErrors : allWarnings;
//		for (Object e : list)
//		{
//			MessageLog el = (MessageLog)e;
//			if (el.findGeometries(geom1, cell, geom2, cell2))
//				return true;
//		}
		return false;
	}

    /**
     * Method to remove all errors and warnings
     */
    public synchronized void clearAllLogs()
    {
        allErrors.clear();
        allWarnings.clear();
        findLogs.clear();
    }

    /**
     * Method to retrieve all MessageLogs associated with a given Cell
     * @param cell the Cell to examine.
     * @return all MessageLogs associated with the Cell.
     */
    public synchronized List<MessageLog> getAllLogs(Cell cell)
    {
        CellId cellId = cell.getId();
        List<MessageLog> msgLogs = new ArrayList<MessageLog>();
        // Searching errors
        for (MessageLog log : allErrors) {
            if (log.logCellId == cellId)
                msgLogs.add(log);
        }
        // Searching warnings
        for (WarningLog log : allWarnings) {
            if (log.logCellId == cellId)
                msgLogs.add(log);
        }
        return msgLogs;
    }

    /**
     * Removes all errors and warnings associated with Cell cell.
     * @param cell the cell for which errors and warnings will be removed
     * @return true if any log was removed.
     */
    public synchronized boolean clearLogs(Cell cell) {
        CellId cellId = cell.getId();
        List<MessageLog> errLogs = new ArrayList<MessageLog>();
        // Errors
        boolean removed = false;
        for (MessageLog log : allErrors) {
            if (log.logCellId != cellId)
                errLogs.add(log);
            else {
                removed = true;
                unHashMessage(log);
            }
        }
        allErrors = errLogs;

	    List<WarningLog> warndLogs = new ArrayList<WarningLog>();
        // Warnings
        for (WarningLog log : allWarnings) {
            if (log.logCellId != cellId)
                warndLogs.add(log);
            else {
                removed = true;
                unHashMessage(log);
            }
        }
        allWarnings = warndLogs;

        return removed;
    }

    public void exportErrorLogger(String filePath)
    {
         PrintStream buffWriter = null;
         try
         {
            buffWriter = new PrintStream(new FileOutputStream(filePath));

            // ErrorHighArc.class is treaded with same header as  ErrorHighNode
            Class<?>[] errorTypes = {ErrorHighLine.class, ErrorHighPoint.class, ErrorHighPoly.class, ErrorHighNode.class, ErrorLogger.class};

            // Creating header
            buffWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            buffWriter.println();
            buffWriter.println("<!DOCTYPE ErrorLogger");
            buffWriter.println(" [");

            for (int i = 0; i < errorTypes.length; i++)
            {
                Class<?> c = errorTypes[i];
                try {
                    String indent = " ";
                    java.lang.reflect.Method set = c.getMethod("writeXmlHeader", new Class[] {String.class, PrintStream.class});
                    set.invoke(c, new Object[] {indent, buffWriter});
                } catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            buffWriter.println(" ]>");
            buffWriter.println();
            String className = this.getClass().getSimpleName();
            buffWriter.println("<" + className + " errorSystem=\"" + errorSystem + "\">");

            if (sortKeysToGroupNames != null)
            {
                // The keys must be sorted to keep same order as in the original ErrorLogger
                Set<Integer> set = sortKeysToGroupNames.keySet();
                List<Integer> sortedInt = new ArrayList<Integer>(set.size());
                sortedInt.addAll(set); // adding to a list to sort them
                Collections.sort(sortedInt);
                for (Integer i : sortedInt)
                {
                    String groupName = sortKeysToGroupNames.get(i);
                    buffWriter.println("    <GroupLog message=\"" + correctXmlString(groupName) + "\">");
                    // Errors
                    for (MessageLog log : allErrors) {
                        if (log.getSortKey() == i.intValue())
                            log.writeXmlDescription(buffWriter);
                    }
                    // Warnings
                    for (WarningLog log : allWarnings) {
                        if (log.getSortKey() == i.intValue())
                            log.writeXmlDescription(buffWriter);
                    }
                    buffWriter.println("    </GroupLog>");
                }
            }
            else // plain style
            {
                // Errors
                for (MessageLog log : allErrors) {
                    log.writeXmlDescription(buffWriter);
                }
                // Warnings
                for (WarningLog log : allWarnings) {
                    log.writeXmlDescription(buffWriter);
                }
            }
            buffWriter.println("</" + className + ">");
            buffWriter.close();
            System.out.println(filePath + " written");
         } catch (Exception e)
         {
             e.printStackTrace();
             System.out.println("Error opening " + filePath);
             return; // error opening the file
         }
    }

    /**
     * Set a group name for a sortKey.  Doing so causes all errors with
     * this sort key to be put in a sub-tree of the error tree with
     * the groupName as the title of the sub-tree.
     * @param sortKey the error log sortKey
     * @param groupName the group name
     */
    public void setGroupName(int sortKey, String groupName) {
        if (sortKeysToGroupNames == null) {
            sortKeysToGroupNames = new HashMap<Integer,String>();
        }
        sortKeysToGroupNames.put(Integer.valueOf(sortKey), groupName);
    }

    /**
     * Get a group name for a sortKey. It creates the hash map if it
     * doesn't exist
     * @param sortKey the error log sortKey
     * @return the group name. Null if no group name was found
     */
    public String getGroupName(int sortKey)
    {
        if (sortKeysToGroupNames == null) {
            sortKeysToGroupNames = new HashMap<Integer,String>();
        }
        return sortKeysToGroupNames.get(Integer.valueOf(sortKey));
    }

    /**
     * Method called when all errors are logged.  Initializes pointers for replay of errors.
     */
    public synchronized void termLogging(boolean explain)
    {
//        termLogging_(true);
        Job.getUserInterface().termLogging(this, explain, true);
//        alreadyExplained = true;
    }

    public synchronized void termLogging_(boolean terminate)
    {
        // enumerate the errors
        int errs = 0;
        for(MessageLog el : allErrors)
        {
            el.index = ++errs;
        }
	    for(WarningLog el : allWarnings)
        {
            el.index = ++errs;
        }
        if (terminate)
            terminated = true;
    }

    /**
     * Method to retrieve general information about the errorLogger.
     * @return general information about the errorLogger.
     */
    public String getInfo()
    {
        return (errorSystem + " found "+getNumErrors()+" errors, "+getNumWarnings()+" warnings!");
    }

    /**
     * Method to sort the errors by their "key" (a value provided to "logerror()").
     * Obviously, this should be called after all errors have been reported.
     */
    public synchronized void sortLogs()
    {
        Collections.sort(allErrors, new ErrorLogOrder());
	    Collections.sort(allWarnings, new ErrorLogOrder());
    }

    /**
     * Method to tell the number of logged errors.
     * @return the number of "ErrorLog" objects logged.
     */
    public synchronized int getNumErrors() { return allErrors.size(); }

    /**
     * Method to tell the number of logged errors.
     * @return the number of "ErrorLog" objects logged.
     */
    public synchronized int getNumWarnings() { return allWarnings.size(); }

    /**
     * Method to tell the number of logged errors.
     * @return the number of "ErrorLog" objects logged.
     */
    public synchronized int getNumLogs() { return getNumWarnings() + getNumErrors(); }

    public MessageLog getLog(int i) {
        return i < allErrors.size() ? allErrors.get(i) : allWarnings.get(i - allErrors.size());
    }

    public int getLogIndex(MessageLog log)
    {
        int index = allErrors.indexOf(log);
        if (index != -1)
            return index;
        index = allWarnings.indexOf(log);
        if (index != -1)
            return index + allErrors.size();
        assert(false); // it should not reach one
        return -1;
    }

    /**
     * Method to list all logged errors and warnings.
     * @return an Iterator over all of the "ErrorLog" objects.
     */
    public synchronized Iterator<MessageLog> getLogs() {
        List<MessageLog> copy = new ArrayList<MessageLog>();
        for (MessageLog ml : allErrors) {
            copy.add(ml);
        }
	    for (WarningLog wl : allWarnings) {
            copy.add(wl);
        }
        return copy.iterator();
    }

    public synchronized void deleteLog(int i) {
        if (i < allErrors.size()) {
        	unHashMessage(allErrors.get(i));
            allErrors.remove(i);
        } else {
        	unHashMessage(allWarnings.get(i - allErrors.size()));
            allWarnings.remove(i - allErrors.size());
        }
    }

    // ----------------------------- Explorer Tree Stuff ---------------------------

//     public void databaseEndChangeBatch(Undo.ChangeBatch batch) {
//         // check if any errors need to be deleted
//         boolean changed = false;
//         for (Iterator<MessageLog> it = getLogs(); it.hasNext(); ) {
//             MessageLog err = it.next();
//             if (!err.isValid()) {
//                 deleteLog(err);
//                 changed = true;
//             }
//         }
//         if (changed)
//             WindowFrame.wantToRedoErrorTree();
//     }

//     public void databaseChanged(Undo.Change evt) {}

//     public boolean isGUIListener() {
//         return true;
//     }

    public static class XMLParser
    {
        public ErrorLogger process(URL fileURL, boolean verbose) throws Exception
        {
            // Factory call
            SAXParserFactory factory = SAXParserFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setValidating(true);
            // create the parser
            SAXParser parser = factory.newSAXParser();
            URLConnection urlCon = fileURL.openConnection();
            InputStream inputStream = urlCon.getInputStream();
            if (verbose) System.out.println("Parsing XML file \"" + fileURL + "\"");
            XMLHandler handler = new XMLHandler();
            parser.parse(inputStream, handler);
            if (verbose) System.out.println("End Parsing XML file ...");
            return handler.logger;
        }

        private class XMLHandler extends DefaultHandler
        {
            private ErrorLogger logger = null;
            private Cell curCell;
            private String message = "";
        	private List<ErrorHighlight> highlights;
            private List<ErrorHighlight> currentList;
            private Set<String> badCellNames = new HashSet<String>();
            private int theSortLayer = -1, sortGroups = 1; // start from 1 so the errors without group would be all together

            XMLHandler()
            {
            }

            public InputSource resolveEntity (String publicId, String systemId) throws IOException, SAXException
            {
                System.out.println("It shouldn't reach this point!");
                return null;
            }

            /**
             * Method to finish the logger including counting of elements.
             * @throws SAXException
             */
            public void endDocument () throws SAXException
            {
                logger.termLogging(true);
            }

            public void endElement (String uri, String localName, String qName)
            {
                boolean errorLogBody = qName.equals("MessageLog");
                boolean warnLogBody = qName.equals("WarningLog");
                boolean grpLogBody = qName.equals("GroupLog");
                boolean errorPolyBody = ErrorHighlight.isErrorPoly(qName);

                if (errorLogBody || warnLogBody)
                {
                    int sortLayer = 0;
                    if (theSortLayer != -1) // use the group information from the file
                       sortLayer = theSortLayer;
                    else if (curCell != null) sortLayer = curCell.hashCode(); // sort by cell
                    if (errorLogBody)
                        logger.logAnError(message, curCell, sortLayer, highlights);
                    else
                        logger.logAWarning(message, curCell, sortLayer, highlights);
                    message = "";
                }
                else if (grpLogBody)
                {
                    theSortLayer = -1; // reset to null again
                }
                else if (errorPolyBody)
                {
                    this.currentList = this.highlights;
                }
            }

            public void startElement (String uri, String localName, String qName, Attributes attributes)
            {
                boolean loggerBody = qName.equals("ErrorLogger");
                boolean groupBody = qName.equals("GroupLog");
                boolean errorLogBody = qName.equals("MessageLog");
                boolean warnLogBody = qName.equals("WarningLog");
                boolean errorHighlighBody = ErrorHighlight.isErrorHighlightBody(qName);

                if (!loggerBody && !errorLogBody && !warnLogBody && !errorHighlighBody
                        && !groupBody) return;

                String cellName = null, geomName = null, viewName = null, libraryName = null;
                EPoint p1 = null, p2 = null;

                for (int i = 0; i < attributes.getLength(); i++)
                {
                    if (attributes.getQName(i).equals("errorSystem"))
                    {
                        // Ignore the rest of the attribute and generate the logger
                        logger = ErrorLogger.newInst(attributes.getValue(i));
                        return;
                    }
                    else if (attributes.getQName(i).startsWith("message"))
                        message = attributes.getValue(i);
                    else if (attributes.getQName(i).startsWith("cell"))
                    {
                        String origName = attributes.getValue(i);
                        String[] names = TextUtils.parseString(attributes.getValue(i), "{}");
                        cellName = origName; // names[0];
                        viewName = names.length > 1 ? names[1] : null;
                        // cellName might contain library name
                        names = TextUtils.parseString(cellName, ":");
                        if (names.length > 1)
                        {
                            libraryName = names[0];
                            cellName = names[1];
                        }
                    }
                    else if (attributes.getQName(i).startsWith("geom"))
                        geomName = attributes.getValue(i);
                    else if (attributes.getQName(i).startsWith("p1") || attributes.getQName(i).startsWith("pt"))
                    {
                        String[] points = TextUtils.parseString(attributes.getValue(i), "(,)");
                        double x = Double.parseDouble(points[0]);
                        double y = Double.parseDouble(points[1]);
                        p1 = EPoint.fromLambda(x, y);
                    }
                    else if (attributes.getQName(i).startsWith("p2"))
                    {
                        String[] points = TextUtils.parseString(attributes.getValue(i), "(,)");
                        double x = Double.parseDouble(points[0]);
                        double y = Double.parseDouble(points[1]);
                        p2 = EPoint.fromLambda(x, y);
                    }
                    else if (attributes.getQName(i).startsWith("center"))
                    {
//                        String[] points = TextUtils.parseString(attributes.getValue(i), "(,)");
                    }
                    else
                        new Error("Invalid attribute in XMLParser");
                }
                if (groupBody)
                {
                    assert (message != null);
                    theSortLayer = sortGroups;
                    logger.setGroupName(sortGroups++, message);
                }
                else
                {
                    if (viewName != null)
                    {
                        View view = View.findView(viewName);
                        curCell = Library.findCellInLibraries(cellName, view, libraryName);
                        if ((curCell == null || !curCell.isLinked()))
                        {
                            if (!badCellNames.contains(cellName))
                            {
                                badCellNames.add(cellName);
                                System.out.println("Cannot find cell: " + cellName);
                            }
                            //return;
                        }
                    }
                    if (errorLogBody || warnLogBody)
                    {
                        highlights = new ArrayList<ErrorHighlight>();
                        currentList = highlights;
                    }
                    else if (errorHighlighBody)
                    {
                        currentList = ErrorHighlight.addErrorHighlight(qName, curCell, geomName, p1, p2, currentList);
                    }
                    else
                        new Error("Invalid attribute in XMLParser");
                }
            }

            public void fatalError(SAXParseException e)
            {
                System.out.println("Parser Fatal Error on line " + e.getLineNumber() + ": " + e.getMessage());
                e.printStackTrace();
            }

            public void warning(SAXParseException e)
            {
                System.out.println("Parser Warning on line " + e.getLineNumber() + ": " + e.getMessage());
                e.printStackTrace();
            }

            public void error(SAXParseException e)
            {
                System.out.println("Parser Error on line " + e.getLineNumber() + ": " + e.getMessage());
                e.printStackTrace();
            }
        }
    }
}
