/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: Snapshot.java
 * Written by: Dmitry Nadezhin.
 *
 * Copyright (c) 2005, 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.database;

import com.sun.electric.database.geometry.ERectangle;
import com.sun.electric.database.id.CellId;
import com.sun.electric.database.id.CellUsage;
import com.sun.electric.database.id.IdManager;
import com.sun.electric.database.id.IdReader;
import com.sun.electric.database.id.IdWriter;
import com.sun.electric.database.id.LibId;
import com.sun.electric.database.id.TechId;
import com.sun.electric.database.text.CellName;
import com.sun.electric.database.text.Setting;
import com.sun.electric.technology.TechPool;
import com.sun.electric.technology.Technology;
import com.sun.electric.technology.technologies.Artwork;
import com.sun.electric.technology.technologies.Generic;
import com.sun.electric.technology.technologies.Schematics;
import com.sun.electric.tool.Tool;
import com.sun.electric.util.TextUtils;
import com.sun.electric.util.collections.ImmutableArrayList;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

/**
 *
 */
public class Snapshot {
    /**
     * Flag to disable cells whose names differ by case.
     */
    public static final boolean CELLNAMES_IGNORE_CASE = false;
    
    public final IdManager idManager;
    public final int snapshotId;
    public final Tool tool;
    public final ImmutableArrayList<CellTree> cellTrees;
    public final ImmutableArrayList<CellBackup> cellBackups;
    private final int[] cellGroups;
    private final CellId[] groupMainSchematics;
    public final ImmutableArrayList<LibraryBackup> libBackups;
    public final Environment environment;
    public final TechPool techPool;
    final EquivalentSchematicExports[] equivSchemExports;

    /**
     * Creates a new instance of Snapshot
     */
    private Snapshot(IdManager idManager, int snapshotId, Tool tool,
            ImmutableArrayList<CellTree> cellTrees,
            ImmutableArrayList<CellBackup> cellBackups,
            int[] cellGroups, CellId[] groupMainSchematics,
            ImmutableArrayList<LibraryBackup> libBackups,
            Environment environment) {
        this.idManager = idManager;
        this.snapshotId = snapshotId;
        this.tool = tool;
        this.cellTrees = cellTrees;
        this.cellBackups = cellBackups;
        this.cellGroups = cellGroups;
        this.groupMainSchematics = groupMainSchematics;
        this.libBackups = libBackups;
        this.environment = environment;
        techPool = environment.techPool;
        equivSchemExports = new EquivalentSchematicExports[cellTrees.size()];
    }

    /**
     * Creates empty snapshot.
     */
    public Snapshot(IdManager idManager) {
        this(idManager, 0, null, CellTree.EMPTY_LIST, CellBackup.EMPTY_LIST,
                new int[0], new CellId[0],
                LibraryBackup.EMPTY_LIST, idManager.getInitialEnvironment());
    }

    /**
     * Creates a new instance of Snapshot which differs from this Snapshot.
     * Four array parameters are supplied. Each parameter may be null if its contents is the same as in this Snapshot.
     * @param tool Tool which initiated database changes.
     * @param environment Environment of this Snapshot
     * @param cellBackupsArray array indexed by cellIndex of new CellBackups.
     * @param libBackupsArray array indexed by libIndex of LibraryBackups.
     * @return new snapshot which differs froms this Snapshot or this Snapshot.
     * @throws IllegalArgumentException on invariant violation.
     * @throws ArrayOutOfBoundsException on some invariant violations.
     */
    public Snapshot with(Tool tool, Environment environment, CellBackup[] cellBackupsArray, LibraryBackup[] libBackupsArray) {
        if (environment == null) {
            environment = this.environment;
        }
        CellTree[] cellTreesArray = null;
        if (cellBackupsArray != null) {
            cellTreesArray = computeCellTrees(ImmutableArrayList.of(cellBackupsArray), environment.techPool);
        }
        return with(tool, environment, cellTreesArray, libBackupsArray);
    }

    /**
     * Creates a new instance of Snapshot which differs from this Snapshot.
     * Four array parameters are supplied. Each parameter may be null if its contents is the same as in this Snapshot.
     * @param tool Tool which initiated database changes.
     * @param environment Environment of this Snapshot
     * @param cellTreesArray array indexed by cellIndex of new CellTrees.
     * @param libBackupsArray array indexed by libIndex of LibraryBackups.
     * @return new snapshot which differs froms this Snapshot or this Snapshot.
     * @throws IllegalArgumentException on invariant violation.
     * @throws ArrayOutOfBoundsException on some invariant violations.
     */
    public Snapshot with(Tool tool, Environment environment, CellTree[] cellTreesArray, LibraryBackup[] libBackupsArray) {
//        long startTime = System.currentTimeMillis();
        if (environment == null) {
            environment = this.environment;
        }
        ImmutableArrayList<CellTree> cellTrees = copyArray(cellTreesArray, this.cellTrees);
        ImmutableArrayList<LibraryBackup> libBackups = copyArray(libBackupsArray, this.libBackups);
        if (this.cellTrees == cellTrees && this.libBackups == libBackups && this.environment == environment) {
            return this;
        }
        TechPool techPool = environment.techPool;

        // Check usages in cells
        boolean namesChanged = this.libBackups != libBackups || this.cellTrees.size() != cellTrees.size();
        for (int cellIndex = 0; cellIndex < cellTrees.size(); cellIndex++) {
            CellTree newTree = cellTrees.get(cellIndex);
            CellTree oldTree = getCellTree(cellIndex);
            if (newTree == null) {
                if (oldTree != null) {
                    namesChanged = true;
                }
                continue;
            }
            CellBackup newBackup = newTree.top;
            ImmutableCell newCell = newBackup.cellRevision.d;
            if (!newBackup.techPool.isRestriction(techPool)) {
                throw new IllegalArgumentException();
            }
            // Check that sub trees match
            if (newCell.cellId.cellIndex != cellIndex) {
                throw new IllegalArgumentException();
            }
            if (newCell.cellId.idManager != idManager) {
                throw new IllegalArgumentException();
            }
            for (CellTree cellSubTree : newTree.subTrees) {
                if (cellSubTree == null) {
                    continue;
                }
                if (cellSubTree != cellTrees.get(cellSubTree.top.cellRevision.d.cellId.cellIndex)) {
                    throw new IllegalArgumentException();
                }
            }

            CellBackup oldBackup = getCell(cellIndex);
            if (oldBackup == null || newCell.groupName != oldBackup.cellRevision.d.groupName
                    || newCell.params != oldBackup.cellRevision.d.params) {
                namesChanged = true;
            }
        }
        ImmutableArrayList<CellBackup> cellBackups = this.cellBackups;
        if (this.cellTrees != cellTrees) {
            CellBackup[] cellBackupArray = new CellBackup[cellTrees.size()];
            for (int cellIndex = 0; cellIndex < cellTrees.size(); cellIndex++) {
                CellTree cellTree = cellTrees.get(cellIndex);
                if (cellTree != null) {
                    cellBackupArray[cellIndex] = cellTree.top;
                }
            }
            cellBackups = ImmutableArrayList.of(cellBackupArray);
        }

        // Check names
        int[] cellGroups = this.cellGroups;
        CellId[] groupMainSchematics = this.groupMainSchematics;
        if (namesChanged) {
            cellGroups = new int[cellBackups.size()];
            checkNames(idManager, cellBackups, cellGroups, libBackups);
            if (Arrays.equals(this.cellGroups, cellGroups)) {
                cellGroups = this.cellGroups;
            } else {
                int maxGroup = -1;
                for (int groupIndex : cellGroups) {
                    maxGroup = Math.max(maxGroup, groupIndex);
                }
                groupMainSchematics = new CellId[maxGroup + 1];
                for (int cellIndex = 0; cellIndex < cellBackups.size(); cellIndex++) {
                    CellBackup cellBackup = cellBackups.get(cellIndex);
                    if (cellBackup == null) {
                        continue;
                    }
                    CellId cellId = cellBackup.cellRevision.d.cellId;
                    if (!cellId.isSchematic()) {
                        continue;
                    }
                    int groupIndex = cellGroups[cellIndex];
                    CellId mainSchematics = groupMainSchematics[groupIndex];
                    if (mainSchematics == null || cellId.cellName.compareTo(mainSchematics.cellName) < 0) {
                        groupMainSchematics[groupIndex] = cellId;
                    }
                }
                if (Arrays.equals(this.groupMainSchematics, groupMainSchematics)) {
                    groupMainSchematics = this.groupMainSchematics;
                }
            }
        }

        Snapshot snapshot = new Snapshot(idManager, idManager.newSnapshotId(), tool,
                cellTrees, cellBackups, cellGroups, groupMainSchematics,
                libBackups, environment);

        // Try to reuse EquivalenSchematicExports
        for (CellTree cellTree : snapshot.cellTrees) {
            if (cellTree == null) {
                continue;
            }
            reuseSchemEq(snapshot, cellTree.top.cellRevision.d.cellId);
        }
//        long endTime = System.currentTimeMillis();
//        System.out.println("Creating snapshot took: " + (endTime - startTime) + " msec");
        return snapshot;
    }

    private EquivalentSchematicExports reuseSchemEq(Snapshot snapshot, CellId cellId) {
        EquivalentSchematicExports newSchemEq = snapshot.equivSchemExports[cellId.cellIndex];
        if (newSchemEq != null) {
            return newSchemEq;
        }
//        if (cellId.toString().equals("redFour:NMOS;1{sch}") || cellId.toString().equals("orangeST090nm:NMOSf;1{ic}"))
//            cellId = cellId;
        CellTree oldCellTree = getCellTree(cellId);
        CellTree newCellTree = snapshot.getCellTree(cellId);
        if (newCellTree != oldCellTree) {
            return null;
        }
        newSchemEq = equivSchemExports[cellId.cellIndex];
        if (newSchemEq == null) {
            return null;
        }
        assert cellId.isIcon() || cellId.isSchematic();
        CellId mainSchemId = snapshot.groupMainSchematics[snapshot.cellGroups[cellId.cellIndex]];
        if (mainSchemId != groupMainSchematics[cellGroups[cellId.cellIndex]]) {
            return null;
        }
        if (mainSchemId != cellId && mainSchemId != null) {
            EquivalentSchematicExports oldMainSchemEq = equivSchemExports[mainSchemId.cellIndex];
            if (oldMainSchemEq == null) {
                assert cellId.isSchematic();
                return null;
            }
            EquivalentSchematicExports newMainSchemEq = reuseSchemEq(snapshot, mainSchemId);
            if (newMainSchemEq == null) {
                newMainSchemEq = snapshot.getEquivExports(mainSchemId);
            }
            if (newMainSchemEq != oldMainSchemEq) {
                newSchemEq = null;
            }
        }
        for (CellTree subTree : newCellTree.subTrees) {
            if (subTree == null) {
                continue;
            }
            CellId subCellId = subTree.top.cellRevision.d.cellId;
            if (subCellId.isIcon()) {
                if (cellId.isSchematic() && snapshot.cellGroups[cellId.cellIndex] == snapshot.cellGroups[subCellId.cellIndex]) {
                    // Icon of parent
                    continue;
                }
            } else if (!subCellId.isSchematic()) {
                continue;
            }
            EquivalentSchematicExports oldSubSchemEq = equivSchemExports[subCellId.cellIndex];
            assert oldSubSchemEq != null;
            EquivalentSchematicExports newSubSchemEq = reuseSchemEq(snapshot, subCellId);
            if (newSubSchemEq == null) {
                newSubSchemEq = snapshot.getEquivExports(subCellId);
            }
            if (!newSubSchemEq.equals(oldSubSchemEq)) {
                newSchemEq = null;
            }
        }
        if (newSchemEq != null) {
            snapshot.equivSchemExports[cellId.cellIndex] = newSchemEq;
        }
        return newSchemEq;
    }

    public Snapshot with(Tool tool, Environment environment) {
        TechPool techPool = environment.techPool;
        CellBackup[] cellBackupArray = new CellBackup[cellBackups.size()];
        for (CellBackup cellBackup : cellBackups) {
            if (cellBackup == null) {
                continue;
            }
            cellBackupArray[cellBackup.cellRevision.d.cellId.cellIndex] = cellBackup.withTechPool(techPool);
        }
        return with(tool, environment, cellBackupArray, null);
    }

    private static <T> ImmutableArrayList<T> copyArray(T[] newArray, ImmutableArrayList<T> oldList) {
        if (newArray == null) {
            return oldList;
        }
        int l;
        for (l = newArray.length; l > 0 && newArray[l - 1] == null; l--);
        if (l == oldList.size()) {
            int i = 0;
            while (i < oldList.size() && newArray[i] == oldList.get(i)) {
                i++;
            }
            if (i == l) {
                return oldList;
            }
        }
        return new ImmutableArrayList<T>(newArray, 0, l);
    }

    private CellTree[] computeCellTrees(ImmutableArrayList<CellBackup> cellBackups, TechPool techPool) {
        CellTree[] result = new CellTree[cellBackups.size()];
        for (int cellIndex = 0; cellIndex < result.length; cellIndex++) {
            CellBackup cellBackup = cellBackups.get(cellIndex);
            if (cellBackup == null) {
                continue;
            }
            computeCellTrees(cellBackup.cellRevision.d.cellId, cellBackups, techPool, result);
        }
        return result;
    }

    private CellTree computeCellTrees(CellId topCellId, ImmutableArrayList<CellBackup> cellBackups, TechPool techPool, CellTree[] cellTrees) {
        int cellIndex = topCellId.cellIndex;
        CellTree cellTree = cellTrees[cellIndex];
        if (cellTree == null) {
            CellBackup top = cellBackups.get(cellIndex);
            int[] instCounts = top.cellRevision.getInstCounts();
            CellTree[] subTrees = new CellTree[instCounts.length];
            for (int i = 0; i < subTrees.length; i++) {
                if (instCounts[i] == 0) {
                    continue;
                }
                subTrees[i] = computeCellTrees(topCellId.getUsageIn(i).protoId, cellBackups, techPool, cellTrees);
            }
            cellTree = getCellTree(topCellId);
            if (cellTree == null) {
                cellTree = CellTree.newInst(top.cellRevision.d, techPool);
            }
            cellTree = cellTree.with(top, subTrees, techPool);
            cellTrees[cellIndex] = cellTree;
        }
        return cellTree;
    }

    /**
     * Returns Snapshot which differs from this Snapshot by renamed Ids.
     * @param idMapper a map from old Ids to new Ids.
     * @return Snapshot with renamed Ids.
     */
    public Snapshot withRenamedIds(IdMapper idMapper, CellId fromGroup, String toGroup) {
        int maxCellIndex = -1;
        for (CellBackup cellBackup : cellBackups) {
            if (cellBackup == null) {
                continue;
            }
            maxCellIndex = Math.max(maxCellIndex, idMapper.get(cellBackup.cellRevision.d.cellId).cellIndex);
        }
        int maxLibIndex = -1;
        for (LibraryBackup libBackup : libBackups) {
            if (libBackup == null) {
                continue;
            }
            maxLibIndex = Math.max(maxLibIndex, idMapper.get(libBackup.d.libId).libIndex);
        }

        CellBackup[] cellBackupsArray = new CellBackup[maxCellIndex + 1];
        LibraryBackup[] libBackupsArray = new LibraryBackup[maxLibIndex + 1];

        BitSet cellBackupsChangedInLib = new BitSet();
        BitSet fromGroupCellIds = new BitSet();
        BitSet toGroupCellIds = new BitSet();
        CellName fromGroupName = null;
        CellName toGroupName = null;
        if (fromGroup != null) {
            assert getCell(fromGroup) != null;
            CellId toGroupCellId = idMapper.get(fromGroup);
            assert toGroupCellId != null;
            assert getCell(toGroupCellId) == null;
            int fromGroupIndex = cellGroups[fromGroup.cellIndex];
            assert fromGroupIndex >= 0;
            int toGroupIndex1 = -1, toGroupIndex2 = -1;
            if (toGroup == null) {
                toGroupIndex2 = fromGroupIndex;
            }
            for (int cellIndex = 0; cellIndex < cellBackups.size(); cellIndex++) {
                CellBackup cellBackup = cellBackups.get(cellIndex);
                if (cellBackup == null) {
                    continue;
                }
                CellId cellId = cellBackup.cellRevision.d.cellId;
                if (cellId.libId == fromGroup.libId) {
                    if (cellId.cellName.getName().equals(toGroupCellId.cellName.getName())) {
                        toGroupIndex1 = cellGroups[cellIndex];
                    }
                    if (toGroup != null && cellId.cellName.getName().equals(toGroup)) {
                        toGroupIndex2 = cellGroups[cellIndex];
                    }
                }
            }
            ArrayList<CellName> fromCellNames = new ArrayList<CellName>();
            ArrayList<CellName> toCellNames = new ArrayList<CellName>();
            toGroupCellIds.set(toGroupCellId.cellIndex);
            toCellNames.add(toGroupCellId.cellName);
            for (int cellIndex = 0; cellIndex < cellBackups.size(); cellIndex++) {
                CellBackup cellBackup = cellBackups.get(cellIndex);
                if (cellBackup == null || cellIndex == fromGroup.cellIndex) {
                    continue;
                }
                CellId cellId = cellBackup.cellRevision.d.cellId;
                if (cellGroups[cellIndex] == fromGroupIndex) {
                    fromGroupCellIds.set(cellIndex);
                    fromCellNames.add(cellId.cellName);
                }
                if (cellGroups[cellIndex] == toGroupIndex1 || cellGroups[cellIndex] == toGroupIndex2) {
                    toGroupCellIds.set(cellIndex);
                    toCellNames.add(cellId.cellName);
                }
            }
            if (!fromCellNames.isEmpty()) {
                fromGroupName = makeCellGroupName(fromCellNames);
            }
            if (!toCellNames.isEmpty()) {
                toGroupName = makeCellGroupName(toCellNames);
            }
        }
        for (int cellIndex = 0; cellIndex < cellBackups.size(); cellIndex++) {
            CellBackup oldCellBackup = cellBackups.get(cellIndex);
            if (oldCellBackup == null) {
                continue;
            }
            CellName newGroupName = oldCellBackup.cellRevision.d.groupName;
            if (fromGroup != null) {
                if (toGroupCellIds.get(cellIndex) || cellIndex == fromGroup.cellIndex) {
                    newGroupName = toGroupName;
                } else if (fromGroupCellIds.get(cellIndex)) {
                    newGroupName = fromGroupName;
                }
            }
            CellBackup newCellBackup = oldCellBackup.withRenamedIds(idMapper, newGroupName);
            if (newCellBackup != oldCellBackup) {
                cellBackupsChangedInLib.set(newCellBackup.cellRevision.d.cellId.libId.libIndex);
            }
            int newCellIndex = newCellBackup.cellRevision.d.cellId.cellIndex;
            cellBackupsArray[newCellIndex] = newCellBackup;
        }
        if (cellBackupsChangedInLib.isEmpty()) {
            cellBackupsArray = null;
        }

        boolean libBackupsChanged = false;
        for (int libIndex = 0; libIndex < libBackups.size(); libIndex++) {
            LibraryBackup oldLibBackup = libBackups.get(libIndex);
            if (oldLibBackup == null) {
                continue;
            }
            LibraryBackup newLibBackup = oldLibBackup.withRenamedIds(idMapper);
            if (cellBackupsChangedInLib.get(libIndex)) {
                newLibBackup = newLibBackup.withModified();
            }
            if (newLibBackup != oldLibBackup) {
                libBackupsChanged = true;
            }
            libBackupsArray[newLibBackup.d.libId.libIndex] = newLibBackup;
        }
        if (!libBackupsChanged) {
            libBackupsArray = null;
        }

        if (cellBackupsArray == null && libBackups == null) {
            return this;
        }
        return with(tool, null, cellBackupsArray, libBackupsArray);
    }

    public List<LibId> getChangedLibraries(Snapshot oldSnapshot) {
        if (oldSnapshot == null) {
            oldSnapshot = idManager.getInitialSnapshot();
        }
        if (idManager != oldSnapshot.idManager) {
            throw new IllegalArgumentException();
        }
        List<LibId> changed = null;
        if (oldSnapshot.libBackups != libBackups) {
            int numLibs = Math.max(oldSnapshot.libBackups.size(), libBackups.size());
            for (int i = 0; i < numLibs; i++) {
                LibraryBackup oldBackup = oldSnapshot.getLib(i);
                LibraryBackup newBackup = getLib(i);
                if (oldBackup == newBackup) {
                    continue;
                }
                if (changed == null) {
                    changed = new ArrayList<LibId>();
                }
                changed.add(idManager.getLibId(i));
            }
        }
        if (changed == null) {
            changed = Collections.emptyList();
        }
        return changed;
    }

    public List<CellId> getChangedCells(Snapshot oldSnapshot) {
        if (oldSnapshot == null) {
            oldSnapshot = idManager.getInitialSnapshot();
        }
        List<CellId> changed = null;
        int numCells = Math.max(oldSnapshot.cellBackups.size(), cellBackups.size());
        for (int i = 0; i < numCells; i++) {
            CellBackup oldBackup = oldSnapshot.getCell(i);
            CellBackup newBackup = getCell(i);
            if (oldBackup == newBackup) {
                continue;
            }
            if (changed == null) {
                changed = new ArrayList<CellId>();
            }
            changed.add(idManager.getCellId(i));
        }
        if (changed == null) {
            changed = Collections.emptyList();
        }
        return changed;
    }

    public Collection<CellId> getCellsDownTop() {
        LinkedHashSet<CellId> order = new LinkedHashSet<CellId>();
        for (CellBackup cellBackup : cellBackups) {
            if (cellBackup == null) {
                continue;
            }
            getCellsDownTop(cellBackup.cellRevision.d.cellId, order);
        }
        return order;
    }

    private void getCellsDownTop(CellId root, LinkedHashSet<CellId> order) {
        if (order.contains(root)) {
            return;
        }
        CellBackup cellBackup = getCell(root);
        CellRevision cellRevision = cellBackup.cellRevision;
        for (int i = 0; i < cellRevision.cellUsages.length; i++) {
            if (cellRevision.cellUsages[i] == null) {
                continue;
            }
            CellUsage cu = root.getUsageIn(i);
            getCellsDownTop(cu.protoId, order);
        }
        boolean added = order.add(root);
        assert added;
    }

    public CellTree getCellTree(CellId cellId) {
        if (cellId.getIdManager() != idManager) {
            throw new IllegalArgumentException();
        }
        return getCellTree(cellId.cellIndex);
    }

    public CellBackup getCell(CellId cellId) {
        if (cellId.getIdManager() != idManager) {
            throw new IllegalArgumentException();
        }
        return getCell(cellId.cellIndex);
    }

    public CellRevision getCellRevision(CellId cellId) {
        CellBackup cellBackup = getCell(cellId);
        return cellBackup != null ? cellBackup.cellRevision : null;
    }

    public CellTree getCellTree(int cellIndex) {
        return cellIndex < cellTrees.size() ? cellTrees.get(cellIndex) : null;
    }

    public CellBackup getCell(int cellIndex) {
        return cellIndex < cellBackups.size() ? cellBackups.get(cellIndex) : null;
    }

    public CellRevision getCellRevision(int cellIndex) {
        CellBackup cellBackup = getCell(cellIndex);
        return cellBackup != null ? cellBackup.cellRevision : null;
    }

    /**
     * Returns group name of group with specified CellNames.
     * Name of cell group is a base name of main schematics cell if any.
     * Otherwise it is a shortest base name among cells in the group.
     * @param cellNames collection of CellNames in a group.
     * @return name of cell group.
     * @throws InvalidArgumentException if cellNames is empty
     */
    public static CellName makeCellGroupName(Collection<CellName> cellNames) {
        if (cellNames.isEmpty()) {
            throw new IllegalArgumentException();
        }
        String groupName = null;
        for (CellName cellName : cellNames) {
            if (!cellName.isSchematic()) {
                continue;
            }
            String name = cellName.getName();
            if (groupName == null || TextUtils.STRING_NUMBER_ORDER.compare(name, groupName) < 0) {
                groupName = name;
            }
        }
        if (groupName == null) {
            for (CellName cellName : cellNames) {
                String name = cellName.getName();
                if (groupName == null || name.length() < groupName.length()
                        || name.length() == groupName.length() && TextUtils.STRING_NUMBER_ORDER.compare(name, groupName) < 0) {
                    groupName = name;
                }
            }
        }
        assert groupName != null;
        return CellName.parseName(groupName + "{sch}");
    }

    public EquivalentSchematicExports getEquivExports(CellId top) {
        EquivalentSchematicExports eq = equivSchemExports[top.cellIndex];
        if (eq == null) {
            ImmutableNetSchem netSchem = new ImmutableNetSchem(this, top);
            eq = new EquivalentSchematicExports(netSchem);
            equivSchemExports[top.cellIndex] = eq;
        }
        return eq;
    }

    public ERectangle getCellBounds(CellId cellId) {
        return getCellBounds(cellId.cellIndex);
    }

    public ERectangle getCellBounds(int cellIndex) {
        CellTree cellTree = getCellTree(cellIndex);
        return cellTree != null ? cellTree.getBounds() : null;
    }

    public int getCellGroupIndex(CellId cellId) {
        return cellGroups[cellId.cellIndex];
    }

    public boolean cellGroupsProbablyChanged(Snapshot that) {
        return this.cellGroups != that.cellGroups;
    }

    public CellId getMainSchematics(CellId cellId) {
        return groupMainSchematics[cellGroups[cellId.cellIndex]];
    }

    /**
     * Returns TechPool of this Snapshot
     */
    public TechPool getTechPool() {
        return techPool;
    }

    /**
     * Get Technology by TechId
     * TechId must belong to same IdManager as TechPool
     * @param techId TechId to find
     * @return Technology b giben TechId or null
     * @throws IllegalArgumentException of TechId is not from this IdManager
     */
    public Technology getTech(TechId techId) {
        return techPool.getTech(techId);
    }

    /**
     * Returns Artwork technology in this database
     */
    public Artwork getArtwork() {
        return techPool.getArtwork();
    }

    /**
     * Returns Generic technology in this database
     */
    public Generic getGeneric() {
        return techPool.getGeneric();
    }

    /**
     * Returns Schematic technology in this database
     */
    public Schematics getSchematics() {
        return techPool.getSchematics();
    }

//    private Technology getTech(int techIndex) {
//        return techIndex < technologies.size() ? technologies.get(techIndex) : null;
//    }
    /**
     * Returns map from Setting to its value in this Snapshot
     */
    public Map<Setting, Object> getSettings() {
        return environment.getSettings();
    }

    public LibraryBackup getLib(LibId libId) {
        return getLib(libId.libIndex);
    }

    private LibraryBackup getLib(int libIndex) {
        return libIndex < libBackups.size() ? libBackups.get(libIndex) : null;
    }

//    private boolean equals(Snapshot that) {
//        return this.cellBackups.equals(that.cellBackups) &&
//                this.libBackups.equals(that.libBackups) &&
//                Arrays.equals(this.cellGroups, that.cellGroups) &&
//                this.cellBounds.equals(that.cellBounds);
//    }
    public void writeDiffs(IdWriter writer, Snapshot oldSnapshot) throws IOException {
        writer.writeDiffs();
        writer.writeInt(snapshotId);

        writer.writeBoolean(tool != null);
        if (tool != null) {
            writer.writeTool(tool);
        }

        environment.writeDiff(writer, oldSnapshot.environment);

        boolean libsChanged = oldSnapshot.libBackups != libBackups;
        writer.writeBoolean(libsChanged);
        if (libsChanged) {
            writer.writeInt(libBackups.size());
            for (int i = 0; i < libBackups.size(); i++) {
                LibraryBackup oldBackup = oldSnapshot.getLib(i);
                LibraryBackup newBackup = getLib(i);
                if (oldBackup == newBackup) {
                    continue;
                }
                if (oldBackup == null) {
                    writer.writeInt(i);
                    newBackup.write(writer);
                } else if (newBackup == null) {
                    writer.writeInt(~i);
                } else {
                    writer.writeInt(i);
                    newBackup.write(writer);
                }
            }
            writer.writeInt(Integer.MAX_VALUE);
        }

        writer.writeInt(cellBackups.size());
        for (int i = 0; i < cellBackups.size(); i++) {
            CellBackup oldBackup = oldSnapshot.getCell(i);
            CellBackup newBackup = getCell(i);
            if (oldBackup == newBackup) {
                continue;
            }
            if (oldBackup == null) {
                writer.writeInt(i);
                newBackup.write(writer);
            } else if (newBackup == null) {
                writer.writeInt(~i);
            } else {
                writer.writeInt(i);
                newBackup.write(writer);
            }
        }
        writer.writeInt(Integer.MAX_VALUE);

        boolean cellGroupsChanged = cellGroups != oldSnapshot.cellGroups;
        writer.writeBoolean(cellGroupsChanged);
        if (cellGroupsChanged) {
            assert cellGroups.length == cellBackups.size();
            for (int cellIndex = 0; cellIndex < cellGroups.length; cellIndex++) {
                writer.writeInt(cellGroups[cellIndex]);
            }
            for (int groupIndex = 0; groupIndex < groupMainSchematics.length; groupIndex++) {
                CellId mainSchematics = groupMainSchematics[groupIndex];
                writer.writeInt(mainSchematics != null ? mainSchematics.cellIndex : -1);
            }
        }
    }

    public static Snapshot readSnapshot(IdReader reader, Snapshot oldSnapshot) throws IOException {
        assert reader.idManager == oldSnapshot.idManager;
        reader.readDiffs();
        int snapshotId = reader.readInt();
        boolean hasTool = reader.readBoolean();
        Tool tool = hasTool ? reader.readTool() : null;

        Environment environment = Environment.readEnvironment(reader, oldSnapshot.environment);
        TechPool techPool = environment.techPool;
        boolean technologiesChanged = techPool != oldSnapshot.techPool;

        ImmutableArrayList<LibraryBackup> libBackups = oldSnapshot.libBackups;
        boolean libsChanged = reader.readBoolean();
        if (libsChanged) {
            int libLen = reader.readInt();
            LibraryBackup[] libBackupsArray = new LibraryBackup[libLen];
            for (int libIndex = 0, numLibs = Math.min(oldSnapshot.libBackups.size(), libLen); libIndex < numLibs; libIndex++) {
                libBackupsArray[libIndex] = oldSnapshot.libBackups.get(libIndex);
            }
            for (;;) {
                int libIndex = reader.readInt();
                if (libIndex == Integer.MAX_VALUE) {
                    break;
                }
                if (libIndex >= 0) {
                    LibraryBackup newBackup = LibraryBackup.read(reader);
                    libBackupsArray[libIndex] = newBackup;
                } else {
                    libIndex = ~libIndex;
                    assert libBackupsArray[libIndex] != null;
                    libBackupsArray[libIndex] = null;
                }
            }
            libBackups = ImmutableArrayList.of(libBackupsArray);
        }

        int cellLen = reader.readInt();
        int cellMax = Math.min(oldSnapshot.cellBackups.size(), cellLen);
        CellBackup[] cellBackupsArray = new CellBackup[cellLen];
        for (int cellIndex = 0; cellIndex < cellMax; cellIndex++) {
            cellBackupsArray[cellIndex] = oldSnapshot.cellBackups.get(cellIndex);
        }
        if (technologiesChanged) {
            for (int cellIndex = 0; cellIndex < cellLen; cellIndex++) {
                CellBackup cellBackup = cellBackupsArray[cellIndex];
                if (cellBackup == null) {
                    continue;
                }
                cellBackupsArray[cellIndex] = cellBackup.withTechPool(techPool);
            }
        }
        for (;;) {
            int cellIndex = reader.readInt();
            if (cellIndex == Integer.MAX_VALUE) {
                break;
            }
            if (cellIndex >= 0) {
                CellBackup newBackup = CellBackup.read(reader, techPool);
                cellBackupsArray[cellIndex] = newBackup;
            } else {
                cellIndex = ~cellIndex;
                assert cellBackupsArray[cellIndex] != null;
                cellBackupsArray[cellIndex] = null;
            }
        }
        ImmutableArrayList<CellBackup> cellBackups = ImmutableArrayList.of(cellBackupsArray);

        int[] cellGroups = oldSnapshot.cellGroups;
        CellId[] groupMainSchematics = oldSnapshot.groupMainSchematics;
        boolean cellGroupsChanged = reader.readBoolean();
        if (cellGroupsChanged) {
            cellGroups = new int[cellBackups.size()];
            int maxGroup = -1;
            for (int cellIndex = 0; cellIndex < cellGroups.length; cellIndex++) {
                int groupIndex = reader.readInt();
                maxGroup = Math.max(maxGroup, groupIndex);
                cellGroups[cellIndex] = groupIndex;
            }
            groupMainSchematics = new CellId[maxGroup + 1];
            for (int groupIndex = 0; groupIndex < groupMainSchematics.length; groupIndex++) {
                CellId mainSchematics = null;
                int cellIndex = reader.readInt();
                if (cellIndex >= 0) {
                    mainSchematics = reader.idManager.getCellId(cellIndex);
                }
                groupMainSchematics[groupIndex] = mainSchematics;
            }
        }
        for (int i = 0; i < cellBackups.size(); i++) {
            assert (cellBackups.get(i) != null) == (cellGroups[i] >= 0);
        }
        CellTree[] cellTreesArray = oldSnapshot.computeCellTrees(cellBackups, environment.techPool);
        ImmutableArrayList<CellTree> cellTrees = ImmutableArrayList.of(cellTreesArray);
        return new Snapshot(oldSnapshot.idManager, snapshotId, tool, cellTrees, cellBackups,
                cellGroups, groupMainSchematics,
                libBackups, environment);
    }

    /**
     * Checks invariant of this Snapshot.
     * @throws IllegalArgumentException on invariant violation.
     * @throws ArrayOutOfBoundsException on some invariant violations.
     * @throws AssertionError if invariant is broken.
     * @throws AssertionError if invariant is broken.
     */
    public void check() {
//        long startTime = System.currentTimeMillis();
        techPool.check();
        environment.check();
        assert environment.techPool == techPool;
        assert techPool.idManager == idManager;
        for (LibraryBackup libBackup : libBackups) {
            if (libBackup == null) {
                continue;
            }
            libBackup.check();
        }
        assert cellTrees.size() == cellBackups.size();
        for (int cellIndex = 0; cellIndex < cellTrees.size(); cellIndex++) {
            CellTree cellTree = cellTrees.get(cellIndex);
            CellBackup cellBackup = cellBackups.get(cellIndex);
            if (cellTree == null) {
                assert cellBackup == null;
                continue;
            }
            assert cellBackup == cellTree.top;
            for (CellTree subCellTree : cellTree.subTrees) {
                if (subCellTree == null) {
                    continue;
                }
                CellId subCellId = subCellTree.top.cellRevision.d.cellId;
                assert subCellTree == cellTrees.get(subCellId.cellIndex);
            }
            cellTree.check();
            assert cellTree.techPool.isRestriction(techPool);
        }
        if (libBackups.size() > 0) {
            assert libBackups.get(libBackups.size() - 1) != null;
        }
        if (cellTrees.size() > 0) {
            assert cellTrees.get(cellTrees.size() - 1) != null;
        }
        checkRecursion(cellBackups);
        int[] cellGroups = new int[cellBackups.size()];
        checkNames(idManager, cellBackups, cellGroups, libBackups);
        assert Arrays.equals(this.cellGroups, cellGroups);
        int maxGroup = -1;
        for (int groupIndex : cellGroups) {
            maxGroup = Math.max(maxGroup, groupIndex);
        }
        assert groupMainSchematics.length == maxGroup + 1;
        BitSet foundMainSchematics = new BitSet();
        for (CellBackup cellBackup : cellBackups) {
            if (cellBackup == null) {
                continue;
            }
            CellId cellId = cellBackup.cellRevision.d.cellId;
            if (!cellId.isSchematic()) {
                continue;
            }
            int groupIndex = cellGroups[cellId.cellIndex];
            int cmp = cellId.cellName.compareTo(groupMainSchematics[groupIndex].cellName);
            assert cmp >= 0;
            if (cmp == 0) {
                assert groupMainSchematics[groupIndex] == cellId;
                foundMainSchematics.set(groupIndex);
            }
        }
        for (int groupIndex = 0; groupIndex < groupMainSchematics.length; groupIndex++) {
            assert foundMainSchematics.get(groupIndex) == (groupMainSchematics[groupIndex] != null);
        }
//        long endTime = System.currentTimeMillis();
//        System.out.println("Checking snapshot invariants took: " + (endTime - startTime) + " msec");
    }

    /*
     * Checks name consistency of arrays intended for Sbapshot construction.
     * @throws IllegalArgumentException on invariant violation.
     * @throws ArrayOutOfBoundsException on some invariant violations.
     */
    private static void checkNames(IdManager idManager, ImmutableArrayList<CellBackup> cellBackups, int[] cellGroups, ImmutableArrayList<LibraryBackup> libBackups) {
        HashSet<String> libNames = new HashSet<String>();
        ArrayList<HashMap<String, CellName>> protoNameToGroupName = new ArrayList<HashMap<String, CellName>>();
        ArrayList<HashSet<CellName>> cellNames = new ArrayList<HashSet<CellName>>();
        ArrayList<HashMap<String,String>> canonicCellNames = new ArrayList<HashMap<String,String>>();
        ArrayList<HashMap<CellName, Integer>> groupNameToGroupIndex = new ArrayList<HashMap<CellName, Integer>>();
        for (int libIndex = 0; libIndex < libBackups.size(); libIndex++) {
            LibraryBackup libBackup = libBackups.get(libIndex);
            if (libBackup == null) {
                protoNameToGroupName.add(null);
                cellNames.add(null);
                canonicCellNames.add(null);
                groupNameToGroupIndex.add(null);
                continue;
            }
            protoNameToGroupName.add(new HashMap<String, CellName>());
            cellNames.add(new HashSet<CellName>());
            canonicCellNames.add(new HashMap<String,String>());
            groupNameToGroupIndex.add(new HashMap<CellName, Integer>());
            if (libBackup.d.libId != idManager.getLibId(libIndex)) {
                throw new IllegalArgumentException("LibId");
            }
            String libName = libBackup.d.libId.libName;
            if (!libNames.add(libName)) {
                throw new IllegalArgumentException("duplicate libName");
            }
            for (LibId libId : libBackup.referencedLibs) {
                if (libId != libBackups.get(libId.libIndex).d.libId) {
                    throw new IllegalArgumentException("LibId in referencedLibs");
                }
            }
        }
        assert protoNameToGroupName.size() == libBackups.size() && cellNames.size() == libBackups.size() && groupNameToGroupIndex.size() == libBackups.size();

        assert cellBackups.size() == cellGroups.length;
        Arrays.fill(cellGroups, -1);
        ArrayList<CellId> groupMainSchematics = new ArrayList<CellId>();
        ArrayList<ImmutableCell> groupParamOwners = new ArrayList<ImmutableCell>();
        for (int cellIndex = 0; cellIndex < cellBackups.size(); cellIndex++) {
            CellBackup cellBackup = cellBackups.get(cellIndex);
            if (cellBackup == null) {
                continue;
            }
            ImmutableCell d = cellBackup.cellRevision.d;
            CellId cellId = d.cellId;
            if (cellId != idManager.getCellId(cellIndex)) {
                throw new IllegalArgumentException("CellId");
            }
            LibId libId = d.getLibId();
            int libIndex = libId.libIndex;
            if (libId != libBackups.get(libIndex).d.libId) {
                throw new IllegalArgumentException("LibId in ImmutableCell");
            }
            HashMap<String, CellName> cellNameToGroupNameInLibrary = protoNameToGroupName.get(libIndex);
            HashSet<CellName> cellNamesInLibrary = cellNames.get(libIndex);
            HashMap<String,String> canonicCellNamesInLibrary = canonicCellNames.get(libIndex);
            HashMap<CellName, Integer> groupNameToGroupIndexInLibrary = groupNameToGroupIndex.get(libIndex);
            String protoName = cellId.cellName.getName();
            CellName groupName = cellNameToGroupNameInLibrary.get(protoName);
            if (groupName == null) {
                groupName = d.groupName;
                cellNameToGroupNameInLibrary.put(protoName, groupName);
            } else if (!d.groupName.equals(groupName)) {
                throw new IllegalArgumentException("cells with same proto name in different groups");
            }
            Integer gn = groupNameToGroupIndexInLibrary.get(groupName);
            if (gn == null) {
                gn = Integer.valueOf(groupParamOwners.size());
                groupMainSchematics.add(null);
                groupParamOwners.add(null);
                groupNameToGroupIndexInLibrary.put(groupName, gn);
            }
            if (cellId.isSchematic()) {
                CellId mainSchematics = groupMainSchematics.get(gn);
                if (mainSchematics == null || cellId.cellName.compareTo(mainSchematics.cellName) < 0) {
                    groupMainSchematics.set(gn, cellId);
                    groupParamOwners.set(gn, d);
                }
            }
            cellGroups[cellIndex] = gn.intValue();
            if (!cellNamesInLibrary.add(cellId.cellName)) {
                throw new IllegalArgumentException("duplicate CellName in library");
            }
            if (CELLNAMES_IGNORE_CASE)
            {
                String canonicProtoName = TextUtils.canonicString(protoName);
                String oldProtoName = canonicCellNamesInLibrary.get(canonicProtoName);
                if (oldProtoName == null)
                {
                    canonicCellNamesInLibrary.put(canonicProtoName, protoName);
                } else if (!oldProtoName.equals(protoName))
                {
                    throw new IllegalArgumentException("duplicate CellName in library " + libId + ": " + oldProtoName + " and " + protoName);

                }
            }
        }
        for (int cellIndex = 0; cellIndex < cellBackups.size(); cellIndex++) {
            CellBackup cellBackup = cellBackups.get(cellIndex);
            if (cellBackup == null) {
                continue;
            }
            ImmutableCell d = cellBackup.cellRevision.d;
            if (d.paramsAllowed()) {
                int gn = cellGroups[d.cellId.cellIndex];
                ImmutableCell paramOwner = groupParamOwners.get(gn);
                if (paramOwner != null) {
                    d.checkSimilarParams(paramOwner);
                } else {
                    groupParamOwners.set(gn, d);
                }
            }
        }
    }

    private static void checkRecursion(ImmutableArrayList<CellBackup> cellBackups) {
        BitSet visited = new BitSet();
        BitSet checked = new BitSet();
        for (CellBackup cellBackup : cellBackups) {
            if (cellBackup == null) {
                continue;
            }
            checkRecursion(cellBackup.cellRevision.d.cellId, cellBackups, visited, checked);
        }
        assert visited.equals(checked);
    }

    private static void checkRecursion(CellId cellId, ImmutableArrayList<CellBackup> cellBackups, BitSet visited, BitSet checked) {
        int cellIndex = cellId.cellIndex;
        if (checked.get(cellIndex)) {
            return;
        }
        assert !visited.get(cellIndex);
        visited.set(cellIndex);
        CellBackup cellBackup = cellBackups.get(cellIndex);
        CellRevision cellRevision = cellBackup.cellRevision;
        for (int i = 0; i < cellRevision.cellUsages.length; i++) {
            CellUsageInfo cui = cellRevision.cellUsages[i];
            if (cui == null) {
                continue;
            }
            CellUsage u = cellId.getUsageIn(i);
            int subCellIndex = u.protoId.cellIndex;
            if (checked.get(subCellIndex)) {
                continue;
            }
            if (visited.get(subCellIndex)) {
                throw new IllegalArgumentException("Recursive instance of " + u.protoId + " in " + u.parentId);
            }
            checkRecursion(u.protoId, cellBackups, visited, checked);
        }
        checked.set(cellIndex);
    }
}
