/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: Quick.java
 *
 * Copyright (c) 2004, 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.drc;

import com.sun.electric.technology.*;
import com.sun.electric.tool.Consumer;
import com.sun.electric.tool.Job;
import com.sun.electric.tool.user.ErrorLogger;
import com.sun.electric.util.math.DBMath;
import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.hierarchy.HierarchyEnumerator;
import com.sun.electric.database.hierarchy.Nodable;
import com.sun.electric.database.geometry.*;
import com.sun.electric.database.variable.VarContext;
import com.sun.electric.database.topology.NodeInst;
import com.sun.electric.database.topology.ArcInst;
import com.sun.electric.database.network.Network;
import com.sun.electric.database.prototype.NodeProto;
import com.sun.electric.util.math.FixpTransform;
import com.sun.electric.util.math.MutableInteger;

import java.util.*;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Area;

/**
 * User: Gilda Garreton
 * Date: Dec 11, 2007
 */

public class MTDRCAreaTool extends MTDRCTool
{
//    /** Miscellanous data for DRC */                            private DRC.ReportInfo reportInfo;

    public MTDRCAreaTool(DRC.DRCPreferences dp, Cell c, Consumer<MTDRCResult> consumer)
    {
        super("Design-Rule Area Check " + c, dp, c, consumer);
    }

    @Override
    boolean checkArea() {return true;}

    static int checkMinArea(Layer theLayer, Cell topC, DRC.ReportInfo rI, XMLRules areaRules,
                            CellLayersContainer cellLayersC, Job job, String msg)
    {
        DRCTemplate minAreaRule = areaRules.getMinValue(theLayer, DRCTemplate.DRCRuleType.MINAREA);
        DRCTemplate enclosedAreaRule = areaRules.getMinValue(theLayer, DRCTemplate.DRCRuleType.MINENCLOSEDAREA);
        DRCTemplate spaceRule = areaRules.getSpacingRule(theLayer, null, theLayer, null, true, -1, -1.0, -1.0); // UCONSPA, CONSPA or SPACING
        if (minAreaRule == null && enclosedAreaRule == null && spaceRule == null)
            return 0;

        if (msg != null)
        {
            System.out.println("Min Area DRC for " + msg + " in thread " + Thread.currentThread().getName());
        }
        HierarchyEnumerator.Visitor quickArea = new LayerAreaEnumerator(theLayer, minAreaRule, enclosedAreaRule,
            spaceRule, topC, rI, GeometryHandler.GHMode.ALGO_SWEEP,
                cellLayersC, job);
        HierarchyEnumerator.enumerateCell(topC, VarContext.globalContext, quickArea);
        return rI.errorLogger.getNumErrors() + rI.errorLogger.getNumWarnings();
    }

    @Override
    public MTDRCResult runTaskInternal(Layer theLayer)
    {
        ErrorLogger errorLogger = DRC.getDRCErrorLogger(true, ", Layer " + theLayer.getName());
        String msg = "Cell " + topCell.getName() + " , layer " + theLayer.getName();
        DRC.ReportInfo reportInfo = new DRC.ReportInfo(errorLogger, topCell.getTechnology(), dp, false);

        Date lastAreaGoodDate = DRC.getLastDRCDateBasedOnBits(topCell, false, -1, !reportInfo.inMemory);

        if (DRC.isCellDRCDateGood(topCell, lastAreaGoodDate))
            System.out.println("The cell seems to be MinArea OK. Should I run the code?");

        int totalNumErrors = checkMinArea(theLayer, topCell, reportInfo, rules, cellLayersCon, this, msg);
        HashSet<Cell> goodAreaDRCDate = new HashSet<Cell>();
        HashSet<Cell> cleanAreaDRCDate = new HashSet<Cell>();

        timer.end();
        int errorCount = errorLogger.getNumErrors();
        int warnCount = errorLogger.getNumWarnings();
        System.out.println(errorCount + " errors and " + warnCount + " warnings found in " + msg
                + " (took " + timer + " in thread " + Thread.currentThread().getName() + ")");
        System.out.println("Accumulative time " + globalStartTime.currentTimeString());
        if (totalNumErrors == 0)
        {
            goodAreaDRCDate.add(topCell);
        }
        else
        {
            cleanAreaDRCDate.add(topCell);
            errorLogger.termLogging(true);
        }
        return new MTDRCResult(errorCount, warnCount, !checkAbort(), new HashSet<Cell>(), new HashSet<Cell>(),
            goodAreaDRCDate, cleanAreaDRCDate, null);
    }

    /**************************************************************************************************************
	 *  LayerAreaEnumerator class
	 **************************************************************************************************************/

    public static class GeometryHandlerLayerBucket
    {
        GeometryHandler local;
        boolean merged = false;

        GeometryHandlerLayerBucket(GeometryHandler.GHMode mode)
        {
            local = GeometryHandler.createGeometryHandler(mode, 1);
        }

        void addElementLocal(Poly poly, Layer layer)
        {
            local.add(layer, poly);
        }

        void mergeGeometry(Cell cell, Map<Cell, GeometryHandlerLayerBucket> cellsMap)
        {
            if (!merged)
            {
                merged = true;
                for (Iterator<NodeInst> it = cell.getNodes(); it.hasNext();)
                {
                    NodeInst ni = it.next();
                    if (!ni.isCellInstance()) continue; // only cell instances
                    FixpTransform trans = ni.transformOut();
                    Cell protoCell = (Cell) ni.getProto();
                    GeometryHandlerLayerBucket bucket = cellsMap.get(protoCell);
                    if (bucket != null) // it is null when the layer was not found in the subcell
                        local.addAll(bucket.local, trans);
                }
                local.postProcess(true);
            } else
            {
                assert (false); // It should not happen
            }
        }
    }

    /**************************************************************************************************************
	 *  LayerAreaEnumerator class
	 **************************************************************************************************************/
    /**
     * Class that uses local GeometryHandler to calculate the area per cell
     */
    private static class LayerAreaEnumerator extends HierarchyEnumerator.Visitor
    {
        private Layer theLayer;
        private Job job;
        private Map<Cell,GeometryHandlerLayerBucket> cellsMap;
        private Layer.Function.Set thisLayerFunction;
        private GeometryHandler.GHMode mode;
        private Collection<PrimitiveNode> nodesList; // NodeProto that contains this layer
        private Collection<ArcProto> arcsList; // ArcProto that contains this layer
        private CellLayersContainer cellLayersCon;
        private DRCTemplate minAreaRule, enclosedAreaRule, spacingRule, minAreaEnclosedRule;
//        private ErrorLogger errorLogger;
        private Cell topCell;
        private DRC.ReportInfo reportInfo;

        LayerAreaEnumerator(Layer layer, DRCTemplate minAreaR, DRCTemplate enclosedAreaR, DRCTemplate spaceR,
                            Cell topC, DRC.ReportInfo rI,
                            GeometryHandler.GHMode m, CellLayersContainer cellLayersC, Job j)
        {
            // This is required so the poly arcs will be properly merged with the transistor polys
            // even though non-electrical layers are retrieved
            this.minAreaRule = minAreaR;
            this.enclosedAreaRule = enclosedAreaR;
            this.spacingRule = spaceR;
            this.topCell = topC;
            this.reportInfo = rI;
            this.theLayer = layer;
            this.thisLayerFunction = Layer.getMultiLayersSet(theLayer);
            this.mode = m;
            cellsMap = new HashMap<Cell,GeometryHandlerLayerBucket>();
            nodesList = new ArrayList<PrimitiveNode>();
            this.cellLayersCon = cellLayersC;
            this.job = j;

            // Store the min value betwen min area and min enclosure to speed up the process
            minAreaEnclosedRule = minAreaRule;
            if (enclosedAreaRule != null &&
                (minAreaRule == null || enclosedAreaRule.getValue(0) < minAreaRule.getValue(0)))
                minAreaEnclosedRule = enclosedAreaRule;

            // determine list of PrimitiveNodes that have this particular layer
            for (PrimitiveNode node : topCell.getTechnology().getNodesCollection())
            {
                for (Technology.NodeLayer nLayer : node.getNodeLayers())
                {
                    if (thisLayerFunction.contains(nLayer.getLayer().getFunction(), theLayer.getFunctionExtras()))
                    {
                        nodesList.add(node);
                        break; // found
                    }
                }
            }
            arcsList = new ArrayList<ArcProto>();
            for (ArcProto ap : topCell.getTechnology().getArcsCollection())
            {
                for (int i = 0; i < ap.getNumArcLayers(); i++)
                {
                    if (ap.getLayer(i) == theLayer)
                    {
                        arcsList.add(ap);
                        break; // found
                    }
                }
            }
        }

        public boolean enterCell(HierarchyEnumerator.CellInfo info)
        {
            // Checking if the job was aborted by the user
            if (job.checkAbort()) return false;

            Cell cell = info.getCell();
            Set<Layer> set = cellLayersCon.getLayersSet(cell);

            // The cell doesn't contain the layer
            if (set != null && !set.contains(theLayer))
            {
                return false;
            }

            GeometryHandlerLayerBucket bucket = cellsMap.get(cell);
            if (bucket == null)
            {
                bucket = new GeometryHandlerLayerBucket(mode);
                cellsMap.put(cell, bucket);
            }
            else
            {
                assert(bucket.merged);
                return false; // done with this cell
            }

            for(Iterator<ArcInst> it = info.getCell().getArcs(); it.hasNext(); )
            {
                ArcInst ai = it.next();
                Network aNet = info.getNetlist().getNetwork(ai, 0);

                // aNet is null if ArcProto is Artwork
                if (aNet == null)
                    continue;
                ArcProto ap = ai.getProto();
                boolean notFound = !arcsList.contains(ap);

                if (notFound)
                    continue; // primitive doesn't contain this layer

                Technology tech = ap.getTechnology();
                Poly[] arcInstPolyList = tech.getShapeOfArc(ai, thisLayerFunction);
                int tot = arcInstPolyList.length;

                for(int i=0; i<tot; i++)
                {
                    Poly poly = arcInstPolyList[i];
                    bucket.addElementLocal(poly, theLayer);
                }
            }

            return true;
        }

        public void exitCell(HierarchyEnumerator.CellInfo info)
        {
            Cell cell = info.getCell();
            boolean isTopCell = cell == topCell;
            GeometryHandlerLayerBucket bucket = cellsMap.get(cell);

            bucket.mergeGeometry(cell, cellsMap);

            if (isTopCell)
            {
                for (Layer layer : bucket.local.getKeySet())
                {
//                    int oldV = checkMinAreaLayerWithTree(bucket.local, topCell, layer);
                    checkMinAreaLayerWithLoops(bucket.local, topCell, layer);
                }
            }
        }

        public boolean visitNodeInst(Nodable no, HierarchyEnumerator.CellInfo info)
        {
            // Checking if the job was aborted
            if (job.checkAbort()) return false;

            // Facet or special elements
			NodeInst ni = no.getNodeInst();
            if (NodeInst.isSpecialNode(ni)) return (false);

            // Cells
            if (ni.isCellInstance()) return (true);

			NodeProto np = ni.getProto();
            if (!np.getTechnology().isLayout()) return (false); // only layout nodes

            GeometryHandlerLayerBucket bucket = cellsMap.get(info.getCell());
			FixpTransform trans = ni.rotateOut();
            PrimitiveNode pNp = (PrimitiveNode)np;
            boolean notFound = !nodesList.contains(pNp);
            if (notFound) return false; // pNp doesn't have the layer

            Technology tech = pNp.getTechnology();

            // Don't get electric layers in case of transistors otherwise it is hard to detect ports
            Poly [] nodeInstPolyList = tech.getShapeOfNode(ni, false, true, thisLayerFunction);
			int tot = nodeInstPolyList.length;

            for(int i=0; i<tot; i++)
			{
				Poly poly = nodeInstPolyList[i];

                poly.roundPoints(); // Trying to avoid mismatches while joining areas.
                poly.transform(trans);
                bucket.addElementLocal(poly, theLayer);
            }

            return true;
        }

        private int checkMinAreaLayerWithLoops(GeometryHandler merge, Cell cell, Layer layer)
        {
            // Layer doesn't have min area
            if (minAreaRule == null && enclosedAreaRule == null && spacingRule == null) return 0;
            PolySweepMerge m = (PolySweepMerge)merge; // easier to implement for now.
            List<Area> areas = m.getAreas(layer);
            MutableInteger errorFound = new MutableInteger(0);

            // It could be multi-threaded per area
            for (Area area : areas)
            {
                List<PolyBase> list = PolyBase.getLoopsFromArea(area, layer);
                boolean minPass = true;

                for (PolyBase p : list)
                {
                    double a = p.getArea();
                    minPass = (minAreaEnclosedRule == null) ? true : a >= minAreaEnclosedRule.getValue(0);

                    if (!minPass) break; // go for full checking
                    if (spacingRule != null)
                    {
                        Rectangle2D bnd = p.getBounds2D();
                        minPass = bnd.getWidth() >= spacingRule.getValue(0);
                        if (minPass)
                           minPass = bnd.getHeight() >= spacingRule.getValue(1);
                    }
                    if (!minPass) break;  // go for full checking
                }

                // Must run the real checking
                if (!minPass)
                {
                    for (PolyBase.PolyBaseTree obj : PolyBase.getTreesFromLoops(list))
                    {
                        traversePolyTree(layer, obj, 0, cell, errorFound);
                    }
                }
            }

            return errorFound.intValue();
        }

//        private int checkMinAreaLayerWithTree(GeometryHandler merge, Cell cell, Layer layer)
//        {
//            // Layer doesn't have min area
//            if (minAreaRule == null && enclosedAreaRule == null && spacingRule == null) return 0;
//
//            Collection<PolyBase.PolyBaseTree> trees = merge.getTreeObjects(layer);
//            GenMath.MutableInteger errorFound = new GenMath.MutableInteger(0);
//
//            for (PolyBase.PolyBaseTree obj : trees)
//            {
//                traversePolyTree(layer, obj, 0, cell, errorFound);
//            }
//            return errorFound.intValue();
//        }

        private void traversePolyTree(Layer layer, PolyBase.PolyBaseTree obj, int level,
                                      Cell cell, MutableInteger count)
        {
            for (PolyBase.PolyBaseTree son : obj.getSons())
            {
                traversePolyTree(layer, son, level+1, cell, count);
            }
            boolean minAreaCheck = level%2 == 0;
            boolean checkMin = false, checkNotch = false;
            DRC.DRCErrorType errorType = DRC.DRCErrorType.MINAREAERROR;
            double minVal = 0;
            String ruleName = "";

            if (minAreaCheck)
            {
                if (minAreaRule == null) return; // no rule
                minVal = minAreaRule.getValue(0);
                ruleName = minAreaRule.ruleName;
                checkMin = true;
            }
            else
            {
                // odd level checks enclose area and holes (spacing rule)
                errorType = DRC.DRCErrorType.ENCLOSEDAREAERROR;
                if (enclosedAreaRule != null)
                {
                    minVal = enclosedAreaRule.getValue(0);
                    ruleName = enclosedAreaRule.ruleName;
                    checkMin = true;
                }
                checkNotch = (spacingRule != null);
            }
            PolyBase poly = obj.getPoly();

            if (checkMin)
            {
                double area = poly.getArea();
                // isGreaterThan doesn't consider equals condition therefore negate condition is used
                if (!DBMath.isGreaterThan(minVal, area)) return; // larger than the min value
                count.increment();
                DRC.createDRCErrorLogger(reportInfo,
                        errorType, null, cell, minVal, area, ruleName,
                        poly, null, layer, null, null, null);
            }
            if (checkNotch)
            {
                // Notches are calculated using the bounding box of the polygon -> this is an approximation
                Rectangle2D bnd = poly.getBounds2D();
                if (bnd.getWidth() < spacingRule.getValue(0))
                {
                    count.increment();
                    DRC.createDRCErrorLogger(reportInfo,
                            DRC.DRCErrorType.NOTCHERROR, "(X axis)", cell, spacingRule.getValue(0), bnd.getWidth(),
                            spacingRule.ruleName, poly, null, layer, null, null, layer);
                }
                if (bnd.getHeight() < spacingRule.getValue(1))
                {
                    count.increment();
                    DRC.createDRCErrorLogger(reportInfo,
                            DRC.DRCErrorType.NOTCHERROR, "(Y axis)", cell, spacingRule.getValue(1), bnd.getHeight(),
                            spacingRule.ruleName, poly, null, layer, null, null, layer);
                }
            }
        }
    }
}
