/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: Output.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.Main;
import com.sun.electric.database.EditingPreferences;
import com.sun.electric.database.IdMapper;
import com.sun.electric.database.LibraryBackup;
import com.sun.electric.database.Snapshot;
import com.sun.electric.database.constraint.Constraints;
import com.sun.electric.database.geometry.ERectangle;
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.Library;
import com.sun.electric.database.id.CellId;
import com.sun.electric.database.id.LibId;
import com.sun.electric.database.text.Version;
import com.sun.electric.database.topology.PortInst;
import com.sun.electric.database.variable.EditWindow_;
import com.sun.electric.database.variable.ElectricObject;
import com.sun.electric.database.variable.VarContext;
import com.sun.electric.database.variable.Variable;
import com.sun.electric.tool.Job;
import com.sun.electric.tool.JobException;
import com.sun.electric.tool.Listener;
import com.sun.electric.tool.Tool;
import com.sun.electric.tool.io.FileType;
import com.sun.electric.tool.io.IOTool;
import com.sun.electric.tool.io.output.bookshelf.BookshelfOutput;
import com.sun.electric.tool.user.ErrorLogger;
import com.sun.electric.tool.user.User;
import com.sun.electric.util.TextUtils;

import java.awt.geom.Rectangle2D;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;

import javax.swing.SwingUtilities;

/**
 * This class manages writing files in different formats.
 * The class is subclassed by the different file writers.
 */
public class Output
{
    /**
     * Used in ECAD
     */
	static class NetNames
	{
		String nodeName;
		String netName;
		String portName;
	}

	static class NetNamesSort implements Comparator<NetNames>
    {
        public int compare(NetNames nn1, NetNames nn2)
        {
            String name1 = nn1.netName;
            String name2 = nn2.netName;
            return name1.compareToIgnoreCase(name2);
        }
    }

    /**
     * This is the non-interactive version of exportCellCommand
     * @param cell the Cell to be written.
     * @param context the VarContext of the Cell (its position in the hierarchy above it).
     * @param fileP the path to the disk file to be written.
     * @param type the format of the output file.
     * @param override a list of Polys to draw instead of the cell contents.
    */
    public static void exportCellCommand(Cell cell, VarContext context, String fileP, FileType type, List<PolyBase> override)
    {
    	new OutputCellInfo(cell, context, fileP, type, override);
    }

    /**
     * Return OutputPreferences for a specified FileType.
     * This includes current default values of IO ProjectSettings
     * and either factory default or current default values of Prefs
     * Current default value of Prefs can be obtained only from client thread
     * @param type specified file type.
     * @param factory get factory default values of Prefs
     * @param override the list of Polys to write instead of cell contents.
     * @return an OutputPreferences object for the given file type.
     * @throws InvalidStateException on attempt to get current default values of Prefs from server thread
     */
    public static OutputPreferences getOutputPreferences(FileType type, Cell cell, boolean factory, List<PolyBase> override)
    {
        if (!factory && !SwingUtilities.isEventDispatchThread() && !Main.isBatch())
                throw new IllegalStateException("Current default Prefs can be accessed only from client thread");

		if (type == FileType.CDL) return new Spice.SpicePreferences(factory, true);
		if (type == FileType.CIF) return new CIF.CIFPreferences(factory, cell);
		if (type == FileType.COSMOS || type == FileType.ESIM || type == FileType.RSIM) return new Sim.SimPreferences(factory, type);
		if (type == FileType.DEF) return new DEF.DEFPreferences(factory);
		if (type == FileType.DFTM) return new DFTM.DFTMPreferences(factory);
		if (type == FileType.DXF) return new DXF.DXFPreferences(factory);
		if (type == FileType.STL) return new STL.STLPreferences(factory);
		if (type == FileType.EAGLE) return new Eagle.EaglePreferences(factory);
		if (type == FileType.ECAD) return new ECAD.ECADPreferences(factory);
		if (type == FileType.EDIF) return new EDIF.EDIFPreferences(factory);
		if (type == FileType.FASTHENRY) return new FastHenry.FastHenryPreferences(factory);
		if (type == FileType.FLATRECT) return new FlatRects.FlatRectPreferences(factory);
		if (type == FileType.GERBER) return new Gerber.GerberPreferences(factory);
		if (type == FileType.HPGL) return new HPGL.HPGLPreferences(factory, cell);
		if (type == FileType.GDS) return new GDS.GDSPreferences(factory, cell);
		if (type == FileType.IRSIM) return new IRSIM.IRSIMPreferences(factory);
		if (type == FileType.L) return new L.LPreferences(factory);
		if (type == FileType.LEF) return new LEF.LEFPreferences(factory);
        if (type == FileType.LISP) return new DualEval.DualEvalPreferences(factory);
		if (type == FileType.MAXWELL) return new Maxwell.MaxwellPreferences(factory);
		if (type == FileType.MOSSIM) return new MOSSIM.MOSSIMPreferences(factory);
		if (type == FileType.PADS) return new Pads.PadsPreferences(factory);
		if (type == FileType.PAL) return new PAL.PALPreferences(factory);
		if (type == FileType.POSTSCRIPT || type == FileType.EPS) return new PostScript.PostScriptPreferences(factory, override, cell);
		if (type == FileType.SILOS) return new Silos.SilosPreferences(factory);
		if (type == FileType.SKILL) return new IOTool.SkillPreferences(factory, false, cell);
        if (type == FileType.SKILLEXPORTSONLY) return new IOTool.SkillPreferences(factory, true, cell);
		if (type == FileType.SPICE || type == FileType.CIR) return new Spice.SpicePreferences(factory, false);
		if (type == FileType.SVG) return new SVG.SVGPreferences(factory, cell);
		if (type == FileType.TEGAS) return new Tegas.TegasPreferences(factory);
		if (type == FileType.VERILOG) return new Verilog.VerilogPreferences(factory, false);
		if (type == FileType.VERILOGA) return new Verilog.VerilogPreferences(factory, true);
		if (type == FileType.BOOKSHELF) return new BookshelfOutput.BookshelfOutputPreferences(factory);
		if (type == FileType.TELESIS) return new Telesis.TelesisPreferences(factory);
        return null;
    }

    /**
     * Class to define cell information during output.
     */
    private static class OutputCellInfo extends Job
    {
        private Cell cell;
        private VarContext context;
        private String filePath;
        private OutputPreferences prefs;

        /**
         * @param cell the Cell to be written.
         * @param context the VarContext of the Cell (its position in the hierarchy above it).
         * @param filePath the path to the disk file to be written.
         * @param override the list of Polys to write instead of cell contents.
         */
        public OutputCellInfo(Cell cell, VarContext context, String filePath, FileType type, List<PolyBase> override)
        {
            super("Export "+cell+" ("+type+")", IOTool.getIOTool(), Job.Type.SERVER_EXAMINE, null, null, Priority.USER);
            this.cell = cell;
            this.context = context;
            this.filePath = filePath;
            prefs = getOutputPreferences(type, cell, false, override);
            if (prefs != null)
            	startJob();
        }

        @Override
        public boolean doIt() throws JobException
        {
        	prefs.doOutput(cell, context, filePath, getEditingPreferences());
            return true;
        }
    }

    public abstract static class OutputPreferences implements java.io.Serializable
	{
        // IO Settings
        public boolean useCopyrightMessage = IOTool.isUseCopyrightMessage();
        public boolean includeDateAndVersionInOutput = User.isIncludeDateAndVersionInOutput();
        private int unitsPrecision = EditingPreferences.getInstance().getUnitsPrecision();
        private double unitsScale = 0;
        private boolean disablePopups = false;

        protected OutputPreferences() { this(false); }

        protected OutputPreferences(boolean factory) {
            if (!factory && !SwingUtilities.isEventDispatchThread() && !Main.isBatch())
                throw new IllegalStateException("Current default Prefs can be accessed only from client thread");
        }

        public Output doOutput(Cell cell, VarContext context, String filePath, EditingPreferences ep) {
            return doOutput(cell, context, filePath);
        }

        public int getUnitsOfPrecision() { return unitsPrecision; }

        public abstract Output doOutput(Cell cell, VarContext context, String filePath);

        public String formatDouble(double v) {
        	if (unitsScale == 0)
        	{
        		unitsScale = 10;
        		for(int i=1; i<unitsPrecision; i++) unitsScale *= 10;
        	}
        	double rounded = Math.round(v * unitsScale) / unitsScale;
        	return TextUtils.formatDouble(rounded, unitsPrecision);
        }

        public String formatDoublePostFix(double v) {
            return TextUtils.formatDoublePostFix(v, unitsPrecision);
        }

        public boolean isDisablePopups() { return disablePopups; }

        public void disablePopups() { disablePopups = true; }
	}

	/** file path */									protected String filePath;
	/** for writing text files */						protected PrintWriter printWriter;
	/** for writing text arrays */						protected StringWriter stringWriter;
	/** for writing binary files */						protected DataOutputStream dataOutputStream;
	/** True to write with less information displayed */protected boolean quiet;
	/** for storing generated errors */					protected ErrorLogger errorLogger;

	public Output()
	{
		String classPath = getClass().getName();
		int lastDot = classPath.lastIndexOf('.');
		if (lastDot >= 0) classPath = classPath.substring(lastDot+1);
        errorLogger = ErrorLogger.newInst(classPath + " Output");
    }

    /**
     * Method to retrieve number of errors during the writing process
     */
    public int getNumErrors() { return errorLogger.getNumErrors(); }

    /**
     * Method to retrieve number of warnings during the writing process
     */
    public int getNumWarnings() { return errorLogger.getNumWarnings(); }

    /**
     * Method to write a cell.
     * This method is never called.
     * Instead, it is always overridden by the appropriate write subclass.
     * @param cell the Cell to be written.
     * @param context the VarContext of the cell (for parameter evaluation)
     * @return true on error.
     */
    protected boolean writeCell(Cell cell, VarContext context) { return true; }

	/**
	 * Method to write all Libraries in Snapshot into a panic directory.
     * @param panicSnapshot Snapshot to save.
     * @param panicDir panic directory to save.
     * @return true on error.
	 */
	public static boolean writePanicSnapshot(Snapshot panicSnapshot, File panicDir, boolean oldRevision) {
        FileType type = FileType.JELIB;
        HashMap<LibId,URL> libFiles = new HashMap<LibId,URL>();
        TreeMap<String,LibId> sortedLibs = new TreeMap<String,LibId>(TextUtils.STRING_NUMBER_ORDER);
        for (LibraryBackup libBackup: panicSnapshot.libBackups) {
            if (libBackup == null) continue;
            if ((libBackup.d.flags & Library.HIDDENLIBRARY) != 0) continue;
            LibId libId = libBackup.d.libId;
            String libName = libBackup.d.libId.libName;
            URL libURL = libBackup.d.libFile;
            File newLibFile;
            if (libURL == null || libURL.getPath() == null) {
                newLibFile = new File(panicDir.getAbsolutePath(), libName + "." + type.getFirstExtension());
            } else {
                File libFile = new File(libURL.getPath());
                String fileName = libFile.getName();
                if (fileName == null) fileName = libName + "." + type.getFirstExtension();
                newLibFile = new File(panicDir.getAbsolutePath(), fileName);
            }
            URL newLibURL = TextUtils.makeURLToFile(newLibFile.getAbsolutePath());
            libFiles.put(libId, newLibURL);
            sortedLibs.put(libName, libId);
        }
        boolean error = false;
        if (oldRevision) {
            IdMapper idMapper = IdMapper.consolidateExportIds(panicSnapshot);
            panicSnapshot = panicSnapshot.withRenamedIds(idMapper, null, null);
        }
        for (LibId libId: sortedLibs.values()) {
            System.out.print("."); System.out.flush();
            URL libURL = libFiles.get(libId);
            JELIB jelib = new JELIB();
    		String properOutputName = TextUtils.getFilePath(libURL) + TextUtils.getFileNameWithoutExtension(libURL) + ".jelib";
            if (jelib.openTextOutputStream(properOutputName) ||
                    jelib.writeLib(panicSnapshot, libId, libFiles, oldRevision) ||
                    jelib.closeTextOutputStream()) {
                System.out.println("Error saving "+panicSnapshot.getLib(libId).d.libId.libName);
                error = true;
            }
        }
        System.out.println(" Libraries saved");
        return error;
    }

    /**
     * Method to write an entire Library in JELIB format.
     * This method doesn't modify library, so it can be run out of Job.
     * @param newName name which is used to prepare file name of the library.
     * @param lib library to save
     * @return true if there were errors while writing the file
     */
    public static boolean saveJelib(String newName, Library lib) {
        HashMap<LibId,URL> libFiles = new HashMap<LibId,URL>();
        // rename the library if requested
        URL libURL = lib.getLibFile();
        if (newName != null) {
            libURL = TextUtils.makeURLToFile(newName);
            libFiles.put(lib.getId(), libURL);
        }
        boolean error = false;
        String properOutputName = TextUtils.getFilePath(libURL) + TextUtils.getFileNameWithoutExtension(libURL) + ".jelib";
        JELIB jelib = new JELIB();
        if (jelib.openTextOutputStream(properOutputName) ||
                jelib.writeLib(lib.getDatabase().backup(), lib.getId(), libFiles, false) ||
                jelib.closeTextOutputStream()) {
            System.out.println("Error saving "+lib.getName());
            error = true;
        }
        return error;
    }

	/**
	 * Method to write an entire Library with a particular format.
	 * This is used for output formats that capture the entire library
	 * (only the ELIB and Readable Dump formats).
	 * The alternative to writing the entire library is writing a single
	 * cell and the hierarchy below it (use "writeCell").
	 * @param lib the Library to be written.
	 * @param type the format of the output file.
	 * @param compatibleWith6 true to write a library that is compatible with version 6 Electric.
	 * @param thisQuiet true to save with less information displayed.
     * @param delibHeaderOnly true to write only the header for a DELIB type library
     * @param backupScheme controls how older files are backed-up.
	 */
	public static void writeLibrary(Library lib, FileType type, boolean compatibleWith6,
		boolean thisQuiet, boolean delibHeaderOnly, int backupScheme) throws JobException {
        writeLibrary(lib, type, compatibleWith6, thisQuiet, delibHeaderOnly, backupScheme, null, null);
    }

    /**
     * Method to write an entire Library with a particular format.
     * This is used for output formats that capture the entire library
     * (only the ELIB and Readable Dump formats).
     * The alternative to writing the entire library is writing a single
     * cell and the hierarchy below it (use "writeCell").
     * @param lib the Library to be written.
     * @param type the format of the output file.
     * @param compatibleWith6 true to write a library that is compatible with version 6 Electric.
     * @param thisQuiet true to save with less information displayed.
     * @param delibHeaderOnly true to write only the header for a DELIB type library
     * @param backupScheme controls how older files are backed-up.
     * @param deletedCellFiles output list of deleted cell files of DELIB library
     * @param writtenCellFiles output list of written cell files of DELIB library
     */
    public static void writeLibrary(Library lib, FileType type, boolean compatibleWith6,
        boolean thisQuiet, boolean delibHeaderOnly, int backupScheme,
        List<String> deletedCellFiles, List<String> writtenCellFiles) throws JobException
    {
        // make sure that all "meaning" options are attached to the database
//		Pref.installMeaningVariables();

        // make sure that this library save is announced
        for(Iterator<Listener> it = Tool.getListeners(); it.hasNext(); )
        {
            Listener listener = it.next();
            listener.writeLibrary(lib);
        }
        Snapshot snapshot = lib.getDatabase().backup();
        LibId libId = lib.getId();

//		// make sure all technologies with irrelevant scale information have the same scale value
//		double largestScale = 0;
//		for(Iterator<Technology> it = Technology.getTechnologies(); it.hasNext(); )
//		{
//			Technology tech = it.next();
//			if (tech.isScaleRelevant()) continue;
//			if (tech == Generic.tech) continue;
//			if (tech.getScale() > largestScale) largestScale = tech.getScale();
//		}
//        Setting.SettingChangeBatch changeBatch = new Setting.SettingChangeBatch();
//		for(Iterator<Technology> it = Technology.getTechnologies(); it.hasNext(); )
//		{
//			Technology tech = it.next();
//			if (tech.isScaleRelevant()) continue;
//			if (tech == Generic.tech) continue;
//            changeBatch.add(tech.getScaleSetting(), Double.valueOf(largestScale));
////			tech.setScale(largestScale);
//		}
//        Setting.implementSettingChanges(changeBatch);

        // handle different file types
        URL libFile = lib.getLibFile();
        if (libFile == null)
            libFile = TextUtils.makeURLToFile(lib.getName());

        // make the proper output file name
        File f = new File(libFile.getPath());
        String fullOutputName = TextUtils.decodeString(f.getAbsolutePath()); // the original path is decoded to eliminate encoded spaces.
        String properOutputNameWithoutExtension = TextUtils.getFilePath(libFile) + TextUtils.getFileNameWithoutExtension(libFile);
        String properOutputName = fullOutputName;
        if (properOutputNameWithoutExtension.equals(fullOutputName))
        {
            if (type == FileType.ELIB) properOutputName = properOutputNameWithoutExtension + ".elib";
            if (type == FileType.JELIB) properOutputName = properOutputNameWithoutExtension + ".jelib";
            if (type == FileType.DELIB) properOutputName = properOutputNameWithoutExtension + ".delib";
            if (type == FileType.READABLEDUMP) properOutputName = properOutputNameWithoutExtension + ".txt";
        }

//		String properOutputNameWithoutExtension = TextUtils.getFilePath(libFile) + TextUtils.getFileNameWithoutExtension(libFile);
//		String properOutputName = properOutputNameWithoutExtension;
//		if (type == FileType.ELIB) properOutputName += ".elib";
//		if (type == FileType.JELIB) properOutputName += ".jelib";
//		if (type == FileType.DELIB) properOutputName += ".delib";
//		if (type == FileType.READABLEDUMP) properOutputName += ".txt";
        if (type == FileType.ELIB || type == FileType.JELIB || type == FileType.DELIB)
        {
            // backup previous files if requested
            if (backupScheme == 1)
            {
                // one-level backup
                File newFile = new File(properOutputName);
                if (newFile.exists())
                {
                    String backupFileName = properOutputName + "~";
                    File oldFile = new File(backupFileName);
                    boolean canRename = true;
                    if (oldFile.exists())
                    {
                        if (!oldFile.delete())
                        {
                            System.out.println("Unable to delete former library file " + oldFile);
                            canRename = false;
                        }
                    }
                    if (canRename)
                    {
                        if (!newFile.renameTo(oldFile))
                        {
                            System.out.println("Unable to rename " + newFile + " to " + oldFile);
                        }
                    }
                }
            } else if (backupScheme == 2)
            {
                // full-history backup
                File newFile = new File(properOutputName);
                if (newFile.exists())
                {
                    long modified = newFile.lastModified();
                    Date modifiedDate = new Date(modified);
                    SimpleDateFormat sdf = new SimpleDateFormat("-yyyy-MM-dd");
                    for(int i=0; i<1000; i++)
                    {
                        String backupFileName = properOutputNameWithoutExtension + sdf.format(modifiedDate);
                        if (i != 0)
                            backupFileName += "--" + i;
                        backupFileName += "." + type.getFirstExtension();
                        File oldFile = new File(backupFileName);
                        if (oldFile.exists()) continue;
                        if (!newFile.renameTo(oldFile))
                        {
                            System.out.println("Unable to rename " + newFile + " to " + oldFile);
                        }
                        break;
                    }
                }
            }
        }
        if (type == FileType.ELIB || type == FileType.JELIB)
        {
            if (type == FileType.ELIB)
            {
                ELIB elib = new ELIB();
                elib.quiet = thisQuiet;
                if (compatibleWith6) elib.write6Compatible();
                if (elib.openBinaryOutputStream(properOutputName))
                    throw new JobException("elib.openBinaryOutputStream() failed");
//                if (CVS.isEnabled()) {
//                    CVSLibrary.savingLibrary(lib);
//                }
                if (elib.writeLib(snapshot, libId))
                    throw new JobException("elib.writeLib() failed");
                if (elib.closeBinaryOutputStream())
                    throw new JobException("elib.closeBinaryOutputStream() failed");
//                if (CVS.isEnabled()) {
//                    CVSLibrary.savedLibrary(lib);
//                }
            } else
            {
                JELIB jelib = new JELIB();
                jelib.quiet = thisQuiet;
                if (jelib.openTextOutputStream(properOutputName))
                    throw new JobException("jelib.openTextOutputStream() failed");
//                if (CVS.isEnabled()) {
//                    CVSLibrary.savingLibrary(lib);
//                }
                if (jelib.writeLib(snapshot, libId, null, false))
                    throw new JobException("jelib.writeLib() failed");
                if (jelib.closeTextOutputStream())
                    throw new JobException("jelib.closeTextOutputStream() failed");
//                if (CVS.isEnabled()) {
//                    CVSLibrary.savedLibrary(lib);
//                }
            }
        } else if (type == FileType.READABLEDUMP)
        {
            ReadableDump readableDump = new ReadableDump();
            readableDump.quiet = thisQuiet;
            if (readableDump.openTextOutputStream(properOutputName))
                throw new JobException("readableDump.openTextOutputStream() failed");
            if (readableDump.writeLib(snapshot, libId))
                throw new JobException("readableDump.writeLib() failed");
            if (readableDump.closeTextOutputStream())
                throw new JobException("readableDump.closeTextOutputStream() failed");
        } else if (type == FileType.DELIB)
        {
            DELIB delib = new DELIB(delibHeaderOnly);
            delib.quiet = thisQuiet;
            if (delib.openTextOutputStream(properOutputName))
                throw new JobException("delib.openTextOutputStream() failed");
//            if (CVS.isEnabled() && !delibHeaderOnly) {
//                CVSLibrary.savingLibrary(lib);
//            }
            Set<CellId> oldCells = lib.getDelibCells();
            if (delib.writeLib(snapshot, libId, oldCells))
                throw new JobException("delib.writeLib() failed");
            lib.setDelibCells();
//            HashSet<String> cellFiles = new HashSet<String>(lib.getDelibCellFiles());
//            if (delib.writeLib(snapshot, libId, cellFiles)) return true;
//            lib.setDelibCellFiles(cellFiles);
            if (delib.closeTextOutputStream())
                throw new JobException("delib.closeTextOutputStream() failed");
            if (deletedCellFiles != null)
                deletedCellFiles.addAll(delib.getDeletedCellFiles());
            if (writtenCellFiles != null)
                writtenCellFiles.addAll(delib.getWrittenCellFiles());
//            if (CVS.isEnabled() && !delibHeaderOnly) {
//                CVSLibrary.savedLibrary(lib, delib.getDeletedCellFiles(), delib.getWrittenCellFiles());
//            }
        } else
        {
            throw new JobException("Unknown export type: " + type);
        }
		// clean up and return
        lib.setFromDisk();
        if (!thisQuiet) System.out.println(properOutputName + " written");
        // Update the version in library read in memory
        lib.setVersion(Version.getVersion());
        // if using CVS, update state
/*
        if (CVS.isEnabled()) {
            CVSLibrary.updateState(lib);
        }
*/
        lib.clearChanged();
        Constraints.getCurrent().writeLibrary(lib);
    }

    /**
     * Returns variable disk name. Usually it is variable name.
     * Disk name of PortInst variables is key ATTRP_portName_varName.
     * @param owner owner of the Variable.
     * @param var Variable.
     * @return disk name of variable.
     */
    String diskName(ElectricObject owner, Variable var) {
        String portName = null;
        if (owner instanceof PortInst) {
            PortInst pi = (PortInst)owner;
            portName = pi.getPortProto().getName();
        }
        return diskName(portName, var);
    }

	/**
	 * Returns variable disk name. Usually it is variable name.
	 * Disk name of PortInst variables is key ATTRP_portName_varName.
     * @param portName port name or null.
	 * @param var Variable.
	 * @return disk name of variable.
	 */
    String diskName(String portName, Variable var) {
        if (portName == null) return var.getKey().getName();
        StringBuffer sb = new StringBuffer("ATTRP_");
        for (int i = 0; i < portName.length(); i++) {
            char ch = portName.charAt(i);
            if (ch == '\\' || ch == '_') sb.append('\\');
            sb.append(ch);
        }
        sb.append('_');
        sb.append(var.getKey().getName());
        return sb.toString();
    }

    /**
     * Opens output for writing binary files.
     * @param filePath the name of the file.
     * @return true on error.
     */
    protected boolean openBinaryOutputStream(String filePath)
    {
		this.filePath = filePath;
        FileOutputStream fileOutputStream;
		try
		{
			fileOutputStream = new FileOutputStream(filePath);
		} catch (FileNotFoundException e)
		{
			System.out.println("Could not write file " + filePath);
			System.out.println("Reason: " + e.getMessage());
			return true;
		}
		BufferedOutputStream bufStrm = new BufferedOutputStream(fileOutputStream);
		dataOutputStream = new DataOutputStream(bufStrm);
        return false;
    }

    /**
     * Close output for writing binary to a file.
     * @return true on error.
     */
    protected boolean closeBinaryOutputStream()
    {
		try
		{
			dataOutputStream.close();
		} catch (IOException e)
		{
			System.out.println("Error closing " + filePath);
			return true;
		}
        return false;
    }

	/**
	 * Open output for writing text to a file.
	 * @param fileP the name of the file.
	 * @return true on error.
	 */
	protected boolean openTextOutputStream(String fileP)
	{
		this.filePath = fileP;
		try
		{
			// Extra step with URL is needed for MacOSX paths.
			URL fileURL = TextUtils.makeURLToFile(fileP);
			printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(TextUtils.getFile(fileURL)), "UTF-8")));
		} catch (IOException e)
		{
			reportError("Error opening " + fileP+": "+e.getMessage());
			return true;
		}
		return false;
	}

	/**
     * Close output for writing text to a file.
     * @return true on error.
     */
    protected boolean closeTextOutputStream()
    {
        printWriter.close();
        return false;
    }

    /**
     * Open output for collecting a list of strings.
     */
    protected void openStringsOutputStream()
    {
        stringWriter = new StringWriter();
    }

	/**
     * Close output for collecting a list of strings.
     * @return the list of strings.
     */
    protected List<String> closeStringsOutputStream()
    {
    	StringBuffer sb = stringWriter.getBuffer();
    	String [] lines = sb.toString().split("\n");
    	List<String> strings = new ArrayList<String>();
    	for(int i=0; i<lines.length; i++)
    		strings.add(lines[i]);
        return strings;
    }

    /**
     * Method to terminate the logging process in the ErrorLogger
     */
    protected Output finishWrite()
    {
        errorLogger.termLogging(true);
        return this;
    }

    /**
     * Method to report errors during the output process
     * @param msg
     */
    protected void reportWarning(String msg)
    {
        errorLogger.logWarning(msg, null, -1);
        System.out.println(msg);
    }

    /**
     * Method to report errors during the output process. It must be public
     * because it is used in parent package
     * @param msg
     */
    public void reportError(String msg)
    {
        errorLogger.logError(msg, -1);
        System.out.println(msg);
    }

    /**
     * Method to write copyright header information to the output.
     * @param prefix the characters that start a line of commented output.
     * @param postfix the characters that end a line of commented output.
     */
	protected void emitCopyright(String prefix, String postfix)
	{
		if (!IOTool.isUseCopyrightMessage()) return;
		String str = IOTool.getCopyrightMessage();
		int start = 0;
		while (start < str.length())
		{
			int endPos = str.indexOf('\n', start);
			if (endPos < 0) endPos = str.length();
			String oneLine = str.substring(start, endPos);
			writeChunk(prefix + oneLine + postfix + "\n", null);
			start = endPos+1;
		}
	}

	/** number of characters written on a line */	private int lineCharCount = 0;
	private int maxWidth = 80;
	private boolean strictWidthLimit = false;
	private String continuationString = "";

	/**
	 * Method to set the size of a line of output.
	 * @param width the maximum number of characters on a line of output (default is 80).
	 * @param strict true to strictly enforce the line-width limit, even if it means breaking
	 * a symbol in the middle.  When false, very long names may exceed the width limit.
	 */
	protected void setOutputWidth(int width, boolean strict) { maxWidth = width;   strictWidthLimit = strict; }

	/**
	 * Method to set the string that will be emitted at the start of a "continuation line".
	 * The continuation line is the line that follows a line which is broken up because
	 * of width limits.
	 * @param str the string that will be emitted at the start of a "continuation line".
	 */
	protected void setContinuationString(String str) { continuationString = str; }

	private void writeChunk(String str, StringBuffer buffer)
	{
		int len = str.length();
		if (len <= 0) return;
		if (buffer != null)
			buffer.append(str);
		else
		{
			if (printWriter != null) printWriter.print(str); else
				if (stringWriter != null) stringWriter.write(str);
		}
		lineCharCount += len;
		if (str.charAt(len-1) == '\n') lineCharCount = 0;
	}


	/**
	 * Method to add a string to the output, limited by the maximum
	 * width of an output line.
	 * @param str the string to add to the output.
	 */
	protected void writeWidthLimited(String str) { writeWidthLimited(str, null); }

	/**
	 * Method to add a string to the output, limited by the maximum
	 * width of an output line.
	 * @param str the string to add to the output.
	 * @param buffer StringBuffer to write data too in case of not using printWriter
	 */
	protected void writeWidthLimited(String str, StringBuffer buffer)
	{
		for(;;)
		{
			int len = str.length();
			if (len <= 0) break;
			int i = str.indexOf('\n');
			if (i < 0) i = len; else i++;
			if (lineCharCount + i < maxWidth)
			{
				// the next chunk fits: write it
				String chunk = str;
				if (i < len) chunk = str.substring(0, i);
				writeChunk(chunk, buffer);

				str = str.substring(i);
				if (str.length() == 0) break;
				continue;
			}

			// find place to break the line
			String exact = str.substring(0, maxWidth - lineCharCount);
			int splitPos = exact.lastIndexOf(' ');
			if (splitPos < 0)
			{
				int commaPos = exact.lastIndexOf(',');
				if (commaPos > splitPos) splitPos = commaPos;
				int openPos = exact.lastIndexOf('(');
				if (openPos > splitPos) splitPos = openPos;
				int closePos = exact.lastIndexOf(')');
				if (closePos > splitPos) splitPos = closePos;
				int semiPos = exact.lastIndexOf(';');
				if (semiPos > splitPos) splitPos = semiPos;

				if (splitPos < 0)
				{
					splitPos = exact.length()-1;
					if (!strictWidthLimit)
					{
						splitPos = str.length()-1;
						int spacePos = str.indexOf(' ');
						if (spacePos >= 0 && spacePos < splitPos) splitPos = spacePos;
						commaPos = str.indexOf(',');
						if (commaPos >= 0 && commaPos < splitPos) splitPos = commaPos;
						openPos = str.indexOf('(');
						if (openPos >= 0 && openPos < splitPos) splitPos = openPos;
						closePos = str.indexOf(')');
						if (closePos >= 0 && closePos < splitPos) splitPos = closePos;
						semiPos = str.indexOf(';');
						if (semiPos >= 0 && semiPos < splitPos) splitPos = semiPos;
					}
				}
			}
			while (splitPos+1 < str.length() && str.charAt(splitPos+1) == '\n') splitPos++;
			exact = str.substring(0, splitPos+1);

			writeChunk(exact, buffer);
			if (!exact.endsWith("\n"))
			{
				writeChunk("\n", buffer);
				if (continuationString.length() > 0) writeChunk(continuationString, buffer);
			}
			str = str.substring(exact.length());
		}
	}

	/**
	 * Method to determine the area of a cell that is to be printed.
	 * Returns null if the area cannot be determined.
	 */
	public static Rectangle2D getAreaToPrint(Cell cell, boolean reduce, EditWindow_ wnd)
	{
		ERectangle cb = cell.getBounds();
		Rectangle2D bounds = new Rectangle2D.Double(cb.getMinX(), cb.getMinY(), cb.getWidth(), cb.getHeight());
		if (wnd != null) bounds = wnd.getBoundsInWindow();

		// extend it and make it square
		if (reduce)
		{
			double wid = bounds.getWidth() * 0.75;
			double hei = bounds.getHeight() * 0.75;
//			us_squarescreen(el_curwindowpart, NOWINDOWPART, FALSE, lx, hx, ly, hy, 0);
			bounds.setRect(bounds.getCenterX(), bounds.getCenterY(), wid, hei);
		}

		if (IOTool.getPlotArea() != 0)
		{
			if (wnd == null)
			{
				System.out.println("No current window: printing entire cell");
			} else
			{
				if (IOTool.getPlotArea() == 2)
				{
					bounds = wnd.getDisplayedBounds();
				} else
				{
					Rectangle2D hBounds = wnd.getHighlightedArea();
					if (hBounds == null || hBounds.getWidth() == 0 || hBounds.getHeight() == 0)
					{
						System.out.println("Warning: no highlighted area; printing entire cell");
					} else
					{
						bounds = hBounds;
					}
				}
			}
		}
		return bounds;
	}

	/**
	 * Class to write a library in a CHANGE Job.
	 * Used by regressions that need to queue the output after existing change jobs.
	 */
	public static class WriteJELIB extends Job
    {
        private Library lib;
        private String newName;
        private IdMapper idMapper;
        private int backupScheme;

        public WriteJELIB(Library lib, String newName)
        {
            super("Write "+lib, User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
            this.lib = lib;
            this.newName = newName;
            backupScheme = IOTool.getBackupRedundancy();
            startJob();
        }

        public boolean doIt() throws JobException
        {
            try
            {
                if (newName != null) {
                    URL libURL = TextUtils.makeURLToFile(newName);
                    lib.setLibFile(libURL);
                    idMapper = lib.setName(TextUtils.getFileNameWithoutExtension(libURL));
                    if (idMapper != null)
                        lib = EDatabase.serverDatabase().getLib(idMapper.get(lib.getId()));
                }
                fieldVariableChanged("idMapper");
            } catch (Exception e)
            {
            	throw new JobException("Exception caught when saving library: " + e.getMessage());
            }
            Output.writeLibrary(lib, FileType.JELIB, false, false, false, backupScheme);
            return false;
        }

        public void terminateOK() {
            User.fixStaleCellReferences(idMapper);
        }
    }
}

