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

import com.sun.electric.database.CellBackup;
import com.sun.electric.database.CellRevision;
import com.sun.electric.database.Snapshot;
import com.sun.electric.database.hierarchy.View;
import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.id.CellId;
import com.sun.electric.database.id.CellUsage;
import com.sun.electric.database.id.LibId;
import com.sun.electric.database.text.Version;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
 * User: gainsley
 * Date: Mar 8, 2006
 */
public class DELIB extends JELIB {

    private HashMap<String,CellFileState> cellFileMap;
    private List<String> deletedCellFiles;
    private String headerFile;
    private boolean wroteSearchForCells;
    private boolean writeHeaderOnly;    // overwrite header if it has cvs conflicts

    DELIB(boolean writeHeaderOnly) {
        // keep list of all associated cell files
        cellFileMap = new HashMap<String,CellFileState>();
        // list of cell files deleted (renamed) because they no longer exist in the library
        deletedCellFiles = new ArrayList<String>();
        wroteSearchForCells = false;
        this.writeHeaderOnly = writeHeaderOnly;
    }

    private static class CellFileState {
        public boolean modified = false;
        public boolean appendFile = false;
    }

    // last version to use subdirs to hold cells, instead of putting them all in delib dir.
    public static final String SEARCH_FOR_CELL_FILES = "____SEARCH_FOR_CELL_FILES____";
    public static final char PLATFORM_INDEPENDENT_FILE_SEPARATOR = '/';

    protected boolean writeLib(Snapshot snapshot, LibId libId, Set<CellId> oldCells) {
        Set<String> oldCellFiles = new HashSet<String>();
        for (CellId cellId: oldCells)
            oldCellFiles.add(getCellFile(cellId));
        // sanity check: make sure we are not writing inside another delib file, this is bad for cvs
        // and just bad and confusing in general
        File delibDir = new File(filePath);
        File parent = delibDir.getParentFile();
        if (parent.getName().endsWith(".delib")) {
            System.out.println("Error: Cannot write "+snapshot.getLib(libId).d.libId.libName+" inside of another DELIB directory");
            return true;
        }

        // decide what files should be written
        for (CellBackup cellBackup : snapshot.cellBackups) {
            if (cellBackup == null) continue;
            CellRevision cellRevision = cellBackup.cellRevision;
            if (cellRevision.d.getLibId() != libId) continue;
            String cellFile = getCellFile(cellRevision.d.cellId);
            String file = filePath + File.separator + cellFile;
            // different cells are in different files, with the exception of
            // different versions of the same cell, which map to the same file name
            CellFileState state = cellFileMap.get(file);
            if (state == null) {
                state = new CellFileState();
                cellFileMap.put(file, state);
            }
            // if any versions are modified or do not exist on disk like they should,
            // mark the file to be modified
            File fd = new File(file);
            if (cellBackup.modified || !fd.exists()) state.modified = true;
        }
        // See for deleted versions (Bug #1945)
        for (CellId cellId: oldCells) {
            if (snapshot.getCell(cellId) == null) {
                String cellFile = getCellFile(cellId);
                String file = filePath + File.separator + cellFile;
                CellFileState state = cellFileMap.get(file);
                if (state != null)
                    state.modified = true;
            }
        }

        boolean b = super.writeLib(snapshot, libId, null, false);
        if (!b && !writeHeaderOnly) {
            // rename cell files that are no longer in the library
            deletedCellFiles.clear();
            for (File file : delibDir.listFiles()) {
                checkIfDeleted(file, oldCellFiles);
            }
//            if (oldCellFiles != null) {
//                oldCellFiles.clear();
//                oldCellFiles.addAll(cellFileMap.keySet());
//            }
        }
        return b;
    }

    /**
     * Rename (backup) any files for cells that have been deleted. This will prevent them
     * being found by version 8.04n and greater, which searches the delib dir for cell files.
     * @param cellFile
     */
    private void checkIfDeleted(File cellFile, Set<String> oldCellFiles) {
        if (cellFileMap.containsKey(cellFile.getAbsolutePath())) return;
        String name = cellFile.getName();
        int dot = name.lastIndexOf('.');
        if (dot == -1) return;
        //String cellName = name.substring(0, dot);
        View view = View.findView(name.substring(dot+1));
        if (view == null) return;

        if (oldCellFiles == null || !oldCellFiles.contains(cellFile.getName())) return;
//        if (oldCellFiles == null || !oldCellFiles.contains(cellFile.getAbsolutePath())) return;
//        System.out.println("Renaming unlinked (possibly deleted) cell file "+name+" to "+name+".deleted");
        deletedCellFiles.add(cellFile.getAbsolutePath());
        if (!cellFile.delete())
        {
            System.out.println("  Error: Unable to delete cell file " + name);
        }
//        File deletedFileName = new File(cellFile.getAbsolutePath()+".deleted");
//        if (!cellFile.renameTo(deletedFileName)) {
//            System.out.println("  Error: Unable to rename unlinked cell file "+name+" to "+name+".deleted!");
//        }
    }

    /**
     * Write a cell. Instead of writing it to the jelib file,
     * write a reference to an external file, and write the contents there
     * @param cellBackup
     * @return true on error.
     */
    @Override
    boolean writeCell(CellRevision cellRevision) {
        if (writeHeaderOnly) {
            if (!wroteSearchForCells) {
                printWriter.println("C"+SEARCH_FOR_CELL_FILES);
                wroteSearchForCells = true;
            }
            return false; // no erro
        }

        // create cell file in directory
        String cellFile = getCellFile(cellRevision.d.cellId);
        String cellFileAbs = filePath + File.separator + cellFile;
        // save old printWriter
        CellFileState state = cellFileMap.get(cellFileAbs);
        boolean append = state.appendFile;

        // if append is null, do not write this file.
        if (state.modified) {

            // set current print writer to cell file
            PrintWriter headerWriter = printWriter;
            try {
                printWriter = new PrintWriter(new BufferedWriter(new FileWriter(cellFileAbs, append)));
            } catch (IOException e) {
                System.out.println("Error opening "+cellFileAbs+", skipping cell: "+e.getMessage());
                printWriter = headerWriter;
                return true; // error
            }

            // write out external references for this cell
            HashSet<LibId> usedLibs = new HashSet<LibId>();
            int[] instCounts = cellRevision.getInstCounts();
            for (int i = 0; i < instCounts.length; i++) {
                int instCount = instCounts[i];
                if (instCount == 0) continue;
                CellUsage u = cellRevision.d.cellId.getUsageIn(i);
                usedLibs.add(u.protoId.libId);
            }

             // write short header information (library, version)
            LibId libId = cellRevision.d.getLibId();
            printWriter.println("H" + convertString(libId.libName) + "|" + Version.getVersion());

            super.writeExternalLibraryInfo(libId, usedLibs);

            // write out the cell into the new file
            super.writeCell(cellRevision);

            printWriter.close();
            // set the print writer back
            printWriter = headerWriter;
        }

        if (!append) state.appendFile = true;       // next time around file will be appended

        if (!wroteSearchForCells) {
            printWriter.println("C"+SEARCH_FOR_CELL_FILES);
            wroteSearchForCells = true;
        }
        return false; // no error
    }

    @Override
    void writeExternalLibraryInfo(LibId libId,  Set<LibId> usedLibs) {
    }

    /**
     * Open the printWriter for writing text files
     * @return true on error.
     */
    protected boolean openTextOutputStream(String filePath) {
        // first, create a directory for the library
        File f = new File(filePath);
        this.filePath = filePath;
        if (f.exists()) {
            if (!f.isDirectory()) {
                // not a directory, issue error
                System.out.println("Error, file "+f+" is not a directory");
                return true;
            }
        } else {
            // create a directory
            if (!f.mkdir()) {
                System.out.println("Failed to make directory: "+f);
                return true;
            }
        }
        headerFile = filePath + File.separator + getHeaderFile();
        // open new printWriter for cell
        try {
            printWriter = new PrintWriter(new BufferedWriter(new FileWriter(headerFile)));
        } catch (IOException e)
		{
            System.out.println("Error opening " + headerFile+": "+e.getMessage());
            return true;
        }
        return false;
    }

    public List<String> getDeletedCellFiles() { return deletedCellFiles; }

    public List<String> getWrittenCellFiles() {
        List<String> files = new ArrayList<String>();
        for (String s : cellFileMap.keySet())
            files.add(s);
        return files;
    }

    /**
     * Cell subdirectory name. This is the directory inside the
     * .delib directory containing the Cell files for the specified cell.
     * @param cellId
     * @return the Cell subdirectory name.
     */
    public static String getCellSubDir(CellId cellId) {
        return "";
    }

    /**
     * Cell file name.  This is the path, relative to the .delib directory
     * path, of the file for the specified cell.  Note it is a relative path,
     * not an absolute path. Ex: LEsettings/LEsettings.sch
     * @param cellId
     * @return the file with the Cellin it.
     */
    private static String getCellFile(CellId cellId) {
        // versions 8.04n and above write files to .delib dir
        String cellName = cellId.cellName.getName();
        View view = cellId.cellName.getView();
        cellName = cellName + "." + view.getAbbreviation();
        cellName = cellName.replace(File.separatorChar, ':');
        return cellName;
    }

    /**
     * Method used by other tools to find out relative path for cell.
     * This is the path, relative to the .delib directory
     * path, of the file for the specified cell.  Note it is a relative path,
     * not an absolute path. Ex: LEsettings.sch or LEsettings/LEsettings.sch
     * @param cell the Cell.
     * @return the file with the Cell in it.
     */
    public static String getCellFile(Cell cell) {
        return getCellFile(cell.getId());
    }

    /**
     * Get relative path to header file from .delib directory
     * @return the name of the header file in the .delib directory.
     */
    public static final String getHeaderFile() {
        return "header";
    }
}
