/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: SpiceNetlistReader.java
 *
 * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
 *
 * Electric(tm) is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.tool.io.input.spicenetlist;

import java.io.*;
import java.util.*;

/**
 * Parse a spice netlist. Ignores comments, and
 * coalesces split lines into single lines.
 * User: gainsley
 * Date: Aug 3, 2006
 */
public class SpiceNetlistReader {

    private File file;
    BufferedReader reader;
    private StringBuffer lines;
    private int lineno;

    private HashMap<String,String> options = new LinkedHashMap<String,String>();
    private HashMap<String,String> globalParams = new LinkedHashMap<String,String>();
    private List<SpiceInstance> topLevelInstances = new ArrayList<SpiceInstance>();
    private HashMap<String,SpiceSubckt> subckts = new LinkedHashMap<String,SpiceSubckt>();
    private List<String> globalNets = new ArrayList<String>();
    private SpiceSubckt currentSubckt;

    public SpiceNetlistReader() {
        reader = null;
        lines = null;
    }

    public HashMap<String,String> getOptions() { return options; }
    public HashMap<String,String> getGlobalParams() { return globalParams; }
    public List<SpiceInstance> getTopLevelInstances() { return topLevelInstances; }
    public Collection<SpiceSubckt> getSubckts() { return subckts.values(); }
    public List<String> getGlobalNets() { return globalNets; }
    public SpiceSubckt getSubckt(String name) {
        return subckts.get(name.toLowerCase());
    }

//    private static final boolean DEBUG = true;

    // ============================== Parsing ==================================

//    enum TType { PAR, PARVAL, WORD }

    public void readFile(String fileName, boolean verbose) throws FileNotFoundException {
        file = new File(fileName);
        reader = new BufferedReader(new FileReader(fileName));
        lines = new StringBuffer();
        currentSubckt = null;

        String line;
        lineno = 0;
        try {
            while ((line = readLine()) != null) {
                line = line.trim();
                String [] tokens = getTokens(line);
                if (tokens.length == 0) continue;
                String keyword = tokens[0].toLowerCase();

                if (keyword.equals(".include")) {
                    if (tokens.length < 2) {
                        prErr("No file specified for .include");
                        continue;
                    }
                    String ifile = tokens[1];
                    if (!ifile.startsWith("/") && !ifile.startsWith("\\")) {
                        // relative path, add to current path
                        File newFile = new File(file.getParent(), ifile);
                        ifile = newFile.getPath();
                    }
                    File saveFile = file;
                    BufferedReader saveReader = reader;
                    StringBuffer saveLines = lines;
                    int saveLine = lineno;

                    try {
                        if (verbose)
                            System.out.println("Reading include file "+ifile);
                        readFile(ifile, verbose);
                    } catch (FileNotFoundException e) {
                        file = saveFile;
                        lineno = saveLine;
                        prErr("Include file does not exist: "+ifile);
                    }
                    file = saveFile;
                    reader = saveReader;
                    lines = saveLines;
                    lineno = saveLine;
                }
                else if (keyword.startsWith(".opt")) {
                    parseOptions(options, 1, tokens);
                }
                else if (keyword.equals(".param")) {
                    parseParams(globalParams, 1, tokens);
                }
                else if (keyword.equals(".subckt")) {
                    currentSubckt = parseSubckt(tokens);
                    if (currentSubckt != null && subckts.containsKey(currentSubckt.getName().toLowerCase())) {
                        prErr("Subckt "+currentSubckt.getName()+
                                " already defined");
                        continue;
                    }
                    subckts.put(currentSubckt.getName().toLowerCase(), currentSubckt);
                }
                else if (keyword.equals(".global")) {
                    for (int i=1; i<tokens.length; i++) {
                        if (!globalNets.contains(tokens[i]))
                            globalNets.add(tokens[i]);
                    }
                }
                else if (keyword.startsWith(".ends")) {
                    currentSubckt = null;
                }
                else if (keyword.startsWith(".end")) {
                    // end of file
                }
                else if (keyword.startsWith("x")) {
                    SpiceInstance inst = parseSubcktInstance(tokens);
                    addInstance(inst);
                }
                else if (keyword.startsWith("r")) {
                    SpiceInstance inst = parseResistor(tokens);
                    addInstance(inst);
                }
                else if (keyword.startsWith("c")) {
                    SpiceInstance inst = parseCapacitor(tokens);
                    addInstance(inst);
                }
                else if (keyword.startsWith("m")) {
                    SpiceInstance inst = parseMosfet(tokens);
                    addInstance(inst);
                }
                else if (keyword.equals(".protect") || keyword.equals(".unprotect")) {
                    // ignore
                }
                else {
                    prWarn("Parser does not recognize: "+line);
                }
            }
            reader.close();
        } catch (IOException e) {
            System.out.println("Error reading file "+file.getPath()+": "+e.getMessage());
        }
    }

    private void prErr(String msg) {
        System.out.println("Error ("+getLocation()+"): "+msg);
    }
    private void prWarn(String msg) {
        System.out.println("Warning ("+getLocation()+"): "+msg);
    }
    private String getLocation() {
        return file.getName()+":"+lineno;
    }

    /**
     * Get the tokens in a line.  Tokens are separated by whitespace,
     * unless that whitespace is surrounded by single quotes, or parentheses.
     * When quotes are used, those quotes are removed from the string literal.
     * The construct <code>name=value</code> is returned as three tokens,
     * the second being the char '='.
     * @param line the line to parse
     * @return an array of tokens
     */
    private String [] getTokens(String line) {
        List<String> tokens = new ArrayList<String>();
        int start = 0;
        boolean inquotes = false;
        int inparens = 0;
        int i;
        for (i=0; i<line.length(); i++) {
            char c = line.charAt(i);
            if (inquotes) {
                if (c == '\'') {
                    if (inparens > 0) continue;
                    // end string literal
                    tokens.add(line.substring(start, i));
                    start = i+1;
                    inquotes = false;
                }
            }
            else if (c == '\'') {
                if (inparens > 0) continue;
                inquotes = true;
                if (start != i) {
                    prErr("Improper use of open quote '");
                    break;
                }
                start = i+1;
            }
            // else !inquotes:
            else if (Character.isWhitespace(c) && inparens == 0) {
                // end of token (unless just more whitespace)
                if (start < i)
                    tokens.add(line.substring(start, i));
                start = i+1;
            }
            else if (c == '(') {
                inparens++;
            }
            else if (c == ')') {
                if (inparens == 0) {
                    prErr("Too many ')'s");
                    break;
                }
                inparens--;
            }
            else if (c == '=') {
                if (start < i)
                    tokens.add(line.substring(start, i));
                tokens.add("=");
                start = i+1;
            }
            else if (c == '*') {
                break; // rest of line is comment
            }
        }
        if (start < i) {
            tokens.add(line.substring(start, i));
        }

        if (inparens != 0)
            prErr("Unmatched parentheses");

        // join {par, =, val} to {par=val}
        List<String> joined = new ArrayList<String>();
        for (int j=0; j<tokens.size(); j++) {
            if (tokens.get(j).equals("=")) {
                if (j == 0) {
                    prErr("No right hand side to assignment");
                } else if (j == tokens.size()-1) {
                    prErr("No left hand side to assignment");
                } else {
                    int last = joined.size() - 1;
                    joined.set(last, joined.get(last)+"="+tokens.get(++j));
                }
            } else {
                joined.add(tokens.get(j));
            }
        }
        String ret [] = new String[joined.size()];
        for (int k=0; k<joined.size(); k++)
            ret[k] = joined.get(k);
        return ret;
    }

    private void parseOptions(HashMap<String,String> map, int start, String [] tokens) {
        for (int i=start; i<tokens.length; i++) {
            int e = tokens[i].indexOf('=');
            String pname = tokens[i];
            String value = "true";
            if (e > 0) {
                pname = tokens[i].substring(0, e);
                value = tokens[i].substring(e+1);
            }
            if (pname == null || value == null) {
                prErr("Bad option value: "+tokens[i]);
                continue;
            }
            map.put(pname.toLowerCase(), value);
        }
    }

    private void parseParams(HashMap<String,String> map, int start, String [] tokens) {
        for (int i=start; i<tokens.length; i++) {
            parseParam(map, tokens[i], null);
        }
    }

    private void parseParam(HashMap<String,String> map, String parval, String defaultParName) {
        int e = parval.indexOf('=');
        String pname = defaultParName;
        String value = parval;
        if (e > 0) {
            pname = parval.substring(0, e);
            value = parval.substring(e+1);
            if (defaultParName != null && !defaultParName.equalsIgnoreCase(pname)) {
                prWarn("Expected param "+defaultParName+", but got "+pname);
            }
        }
        if (pname == null || value == null) {
            prErr("Badly formatted param=val: "+parval);
            return;
        }
        map.put(pname.toLowerCase(), value);
    }

    private SpiceSubckt parseSubckt(String [] parts) {
        SpiceSubckt subckt = new SpiceSubckt(parts[1]);
        int i=2;
        for (; i<parts.length; i++) {
            if (parts[i].indexOf('=') > 0) break;  // parameter
            subckt.addPort(parts[i]);
        }
        parseParams(subckt.getParams(), i, parts);
        return subckt;
    }

    private SpiceInstance parseSubcktInstance(String [] parts) {
        String name = parts[0].substring(1);
        List<String> nets = new ArrayList<String>();
        int i=1;
        for (; i<parts.length; i++) {
            if (parts[i].contains("=")) break;  // parameter
            nets.add(parts[i]);
        }
        String subcktName = nets.remove(nets.size()-1); // last one is subckt reference
        SpiceSubckt subckt = subckts.get(subcktName.toLowerCase());
        if (subckt == null) {
            prErr("Cannot find subckt for "+subcktName);
            return null;
        }
        SpiceInstance inst = new SpiceInstance(subckt, name);
        for (String net : nets)
            inst.addNet(net);
        parseParams(inst.getParams(), i, parts);
        // consistency check
        if (inst.getNets().size() != subckt.getPorts().size()) {
            prErr("Number of ports do not match: "+inst.getNets().size()+
                  " (instance "+name+") vs "+subckt.getPorts().size()+
                  " (subckt "+subckt.getName()+")");
        }
        return inst;
    }

    private void addInstance(SpiceInstance inst) {
        if (inst == null) return;
        if (currentSubckt != null)
            currentSubckt.addInstance(inst);
        else
            topLevelInstances.add(inst);
    }

    private SpiceInstance parseResistor(String [] parts) {
        if (parts.length < 4) {
            prErr("Not enough arguments for resistor");
            return null;
        }
        SpiceInstance inst = new SpiceInstance(parts[0]);
        for (int i=1; i<3; i++) {
            inst.addNet(parts[i]);
        }
        parseParam(inst.getParams(), parts[3], "r");
        return inst;
    }

    private SpiceInstance parseCapacitor(String [] parts) {
        if (parts.length < 4) {
            prErr("Not enough arguments for capacitor");
            return null;
        }
        SpiceInstance inst = new SpiceInstance(parts[0]);
        for (int i=1; i<3; i++) {
            inst.addNet(parts[i]);
        }
        parseParam(inst.getParams(), parts[3], "c");
        return inst;
    }

    private SpiceInstance parseMosfet(String [] parts) {
        if (parts.length < 8) {
            prErr("Not enough arguments for mosfet");
            return null;
        }
        SpiceInstance inst = new SpiceInstance(parts[0]);
        int i=1;
        for (; i<5; i++) {
            inst.addNet(parts[i]);
        }
        String model = parts[i];
        inst.getParams().put("model", model);
        i++;
        parseParams(inst.getParams(), i, parts);
        return inst;
    }

    private void parseComment(String line) {
        if (currentSubckt == null) return;
        String [] parts = line.split("\\s+");
        for (int i=0; i<parts.length; i++) {
            if (parts[i].equalsIgnoreCase("PORT") && i+2 < parts.length) {
                String dir = parts[i+1];
                String port = parts[i+2];
                SpiceSubckt.PortType type = SpiceSubckt.PortType.valueOf(dir);
                if (type != null)
                    currentSubckt.setPortType(port, type);
                return;
            }
        }
    }

    /**
     * Read one line of the spice file. This concatenates continuation lines
     * that start with '+' to the first line, replacing '+' with ' '.  It
     * also ignores comment lines (lines that start with '*').
     * @return one spice line
     * @throws IOException
     */
    private String readLine() throws IOException {
        while (true) {
            lineno++;
            String line = reader.readLine();
            if (line == null) {
                // EOF
                if (lines.length() == 0) return null;
                return removeString();
            }
            line = line.trim();
            if (line.startsWith("*")) {
                // comment line
                parseComment(line);
                continue;
            }
            if (line.startsWith("+")) {
                // continuation line
                lines.append(" ");
                lines.append(line.substring(1));
                continue;
            }
            // normal line
            if (lines.length() == 0) {
                // this is the first line read, read next line to see if continued
                lines.append(line);
            } else {
                // beginning of next line, save it and return completed line
                String ret = removeString();
                lines.append(line);
                return ret;
            }
        }
    }

    private String removeString() {
        String ret = lines.toString();
        lines.delete(0, lines.length());
        return ret;
    }

    // ================================= Writing ====================================

    public void writeFile(String fileName) {
        if (fileName == null) {
            write(System.out);
        } else {
            try {
                PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream(fileName)));
                write(out);
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    public void write(PrintStream out) {
        out.println("* Spice netlist");
        for (String key : options.keySet()) {
            String value = options.get(key);
            if (value == null) {
                out.println(".option "+key);
            } else {
                out.println(".option "+key+"="+options.get(key));
            }
        }
        out.println();
        for (String key : globalParams.keySet()) {
            out.println(".param "+key+"="+globalParams.get(key));
        }
        out.println();
        for (String net : globalNets) {
            out.println(".global "+net);
        }
        out.println();
        for (String subcktName : subckts.keySet()) {
            SpiceSubckt subckt = subckts.get(subcktName);
            subckt.write(out);
            out.println();
        }
        for (SpiceInstance inst : topLevelInstances) {
            inst.write(out);
        }
        out.println();
        out.println(".end");
        if (out != System.out) out.close();
    }

    static void multiLinePrint(PrintStream out, boolean isComment, String str)
    {
        // put in line continuations, if over 78 chars long
        char contChar = '+';
        if (isComment) contChar = '*';
        int lastSpace = -1;
        int count = 0;
        boolean insideQuotes = false;
        int lineStart = 0;
        for (int pt = 0; pt < str.length(); pt++)
        {
            char chr = str.charAt(pt);
//			if (sim_spice_machine == SPICE2)
//			{
//				if (islower(*pt)) *pt = toupper(*pt);
//			}
            if (chr == '\n')
            {
                out.print(str.substring(lineStart, pt+1));
                count = 0;
                lastSpace = -1;
                lineStart = pt+1;
            } else
            {
                if (chr == ' ' && !insideQuotes) lastSpace = pt;
                if (chr == '\'') insideQuotes = !insideQuotes;
                count++;
                if (count >= 78 && !insideQuotes && lastSpace > -1)
                {
                    String partial = str.substring(lineStart, lastSpace+1);
                    out.print(partial + "\n" + contChar);
                    count = count - partial.length();
                    lineStart = lastSpace+1;
                    lastSpace = -1;
                }
            }
        }
        if (lineStart < str.length())
        {
            String partial = str.substring(lineStart);
            out.print(partial);
        }
    }

    // ======================== Spice Netlist Information ============================

    // =================================== test ================================

    public static void main(String [] args) {
        SpiceNetlistReader reader = new SpiceNetlistReader();

        try {
            reader.readFile("/import/async/cad/2006/bic/jkg/bic/testSims/test_clk_regen.spi", true);
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        }

        reader.writeFile("/tmp/output.spi");
    }

//    private static void testLineParserTests() {
//        SpiceNetlistReader reader = new SpiceNetlistReader();
//        reader = new SpiceNetlistReader();
//        reader.file = new File("/none");
//        reader.lineno = 1;
//        testLineParser(reader, ".measure tran vmin min v( data) from=0ns to=1.25ns  ");
//        testLineParser(reader, ".param poly_res_corner='1.0 * p' * 0.8 corner");
//        testLineParser(reader, ".param poly_res_corner   =    '1.0 * p' * 0.8 corner");
//        testLineParser(reader, ".param AVT0N = AGAUSS(0.0,  '0.01 / 0.1' , 1)");
//    }
//
//    private static void testLineParser(SpiceNetlistReader reader, String line) {
//        System.out.println("Parsing: "+line);
//        String [] tokens = reader.getTokens(line);
//        for (int i=0; i<tokens.length; i++) {
//            System.out.println(i+": "+tokens[i]);
//        }
//    }
}
