/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: NanosimModel.java
 * Written by Jonathan Gainsley.
 *
 * 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.tool.simulation.test;

import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.*;

public class NanosimModel extends SimulationModel {

    protected static final boolean DEBUG = false;

    /** list of jtag testers */
    protected final List jtagTesters;
    /** list of logic settables */
    protected final List logicSettables;

    protected double simTime;         // current simulation time in ns
    protected double vdd;
    protected double timeStep;        // simulation time step in ns
    protected final HashMap nodesToSet = new HashMap();   // key: node name, value: Double voltage
    protected boolean assuraRCXNetlist = false;
    protected boolean starRCXTNetlist = false;

    /**
     * Create a new NanosimModel to simulate the behavior of the real chip
     */
    public NanosimModel() {
        super("Nanosim", "quit", "ERROR", "");
        jtagTesters = new ArrayList();
        logicSettables = new ArrayList();
        simTime = 0;
        vdd = 0;
    }

    public JtagTester createJtagTester(String tckName, String tmsName, String trstbName, String tdiName, String tdobName) {
        if (isProcessRunning()) {
            System.out.println("Error: JtagTester test device must be created before process is started.");
            return null;
        }
        NanosimJtagTester tester = new NanosimJtagTester(this, tckName, tmsName, trstbName, tdiName, tdobName);
        jtagTesters.add(tester);
        return tester;
    }

    /**
     * Create a subchain tester based on the 8- or 9-wire jtag interface.
     * jtag[8:0] = {scan_data_return, phi2_return, phi1_return, rd, wr, phi1, phi2, sin, mc*}
     * Note that mc is not present on older designs, so they are jtag[8:1].
     * @param jtagInBus the name of the 9-bit wide input bus, i.e. "jtagIn" or "jtagIn[8:0]"
     * @param jtagOutBus the name of the 9-bit wide output bus, i.e. "jtagOut" or "jtagOut[8:0]"
     */
    public JtagTester createJtagSubchainTester(String jtagInBus, String jtagOutBus) {
        if (isProcessRunning()) {
            System.out.println("Error: JtagTester test device must be created before process is started.");
            return null;
        }
        NanosimJtagSubchainTester tester = new NanosimJtagSubchainTester(this, jtagInBus, jtagOutBus);
        jtagTesters.add(tester);
        return tester;
    }

    /**
     * Create a subchain tester based on the 5-wire jtag interface.
     * @param phi2 name of the phi2 signal
     * @param phi1 name of the phi1 signal
     * @param write name of the write signal
     * @param read name of the read signal
     * @param sin name of the scan data in signal
     * @param sout name of the scan data out signal
     */
    public JtagTester createJtagSubchainTester(String phi2, String phi1, String write, String read, String sin, String sout) {
        if (isProcessRunning()) {
            System.out.println("Error: JtagTester test device must be created before process is started.");
            return null;
        }
        NanosimJtagSubchainTester tester = new NanosimJtagSubchainTester(this, phi2, phi1, write, read, sin, sout);
        jtagTesters.add(tester);
        return tester;
    }

    public LogicSettable createLogicSettable(String portName) {
        if (isProcessRunning()) {
            System.out.println("Error: LogicSettable test device must be created before process is started.");
            return null;
        }
        NanosimLogicSettable ls = new NanosimLogicSettable(this, portName);
        logicSettables.add(ls);
        return ls;
    }

    public LogicSettable createLogicSettable(List portNames) {
        if (portNames == null || portNames.size() < 1) {
            System.out.println("Error: createLogicSettable given null or empty list of ports");
            return null;
        }
        System.out.println("Error: createLogicSettable(List) is not supported by Nanosim, only using first port "+portNames.get(0));
        return new NanosimLogicSettable(this, (String)portNames.get(0));
    }

    public void disableNode(String node) {
        issueCommand("force_node_v v=0 no="+node);
        waitNS(timeStep);
    }

    public void enableNode(String node) {
        issueCommand("rel_node_v no="+node);
        waitNS(timeStep);
    }

    public double getSimulationTime() {
        return simTime;
    }

    boolean start_(String command, String simFile, int recordSim) {
        // first, find out the nanosim version, as that determines the prompt in interactive mode
        // setup the reader which will read the output of the process
        PipedOutputStream ostream = new PipedOutputStream();
        BufferedReader reader;
        try {
            PipedInputStream istream = new PipedInputStream(ostream);
            reader = new BufferedReader(new InputStreamReader(istream));
        } catch (java.io.IOException e) {
            System.out.println("Unable to create pipe to process output: "+e.getMessage());
            return false;
        }
        String[] commands = new String[] {command, "--version"};
        ExecProcess process = new ExecProcess(commands, null, null, ostream, ostream);
        process.start();
        boolean found = false;
        String version = null;
        StringBuffer buf = new StringBuffer();
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                buf.append(line+"\n");
                if (found) continue;
                String [] args = line.split("\\s+");
                if (args.length < 4) continue;
                if (args[1].equals("Version")) {
                    version = args[3];
                    setPrompt("Ver "+version+" >");
                    found = true;
                }
            }
            reader.close();
        } catch (java.io.IOException e) {
            if (!found) {
                System.out.println("Error determining nanosim version: "+e.getMessage());
                System.out.println(buf);
                return false;
            }
        }
        if (!found) {
            System.out.println("Error determining nanosim version");
            System.out.println(buf);
            return false;
        }
        if (DEBUG)
            System.out.println("Using nanosim version "+version);

        // check if simfile is from assura RCX
        // if it is, we will have to replace hierarchy delimiter '.' with '/'
        try {
            BufferedReader freader = new BufferedReader(new FileReader(simFile));
            // PROGRAM will be in the first 10 or so lines, so search up till line 20
            for (int i=0; i<20; i++) {
                String line = freader.readLine();
                if (line == null) break;
                if (line.matches("\\*  PROGRAM .*?assura.*")) {
                    assuraRCXNetlist = true;
                    System.out.println("Info: Running on Assura extracted netlist, will replace all '.' in net names with '/'");
                    break;
                } else if (line.matches("\\*|PROGRAM .*?Star-RCXT.*")) {
                    starRCXTNetlist = true;
                    System.out.println("Info: Running on Star-RCXT extracted netlist, will replace all '.x' in net names with '/'");
                    break;
                }
            }
            freader.close();
        } catch (IOException e) {
            System.out.println(e.getMessage());
            return false;
        }

        // max sim time (-t) is 90071s
        String cmd = command+" -n "+simFile+" -i -t 90071s -o "+simFile;
        if (!startProcess(cmd, null, null, simFile+".run"))
            return false;

        // get value of vdd
        vdd = getNodeVoltage("vdd");
        if (DEBUG)
            System.out.println("Using VDD of "+vdd);
        timeStep = getSimTres();
        if (DEBUG)
            System.out.println("Using time step of "+timeStep+" ns");

        // initialize test devices
        for (Iterator it = logicSettables.iterator(); it.hasNext(); ) {
            NanosimLogicSettable ls = (NanosimLogicSettable)it.next();
            if (!ls.init()) {
                System.out.println("LogicSettable initialization failed, aborting.");
                return false;
            }
        }
        for (Iterator it = jtagTesters.iterator(); it.hasNext(); ) {
            JtagTester tester = (JtagTester)it.next();
            tester.reset();
        }
        return true;
    }

    /**
     * Get the voltage of VDD for the simulation.  The simulation must have been
     * started for this method to return a valid value.
     * @return the VDD voltage, in volts.
     */
    public double getVdd() { return vdd; }

    protected static final Pattern patSimTime = Pattern.compile("The simulation time is\\s+: ([0-9\\.]+) ns");

    public void wait(float seconds) {
        waitNS(seconds*1.0e9);
    }

    /**
     * Wait the specified number of nano-seconds.  In this case, this means
     * run the simulator until that point in time, when control will return
     * to the test software.
     * @param nanoseconds the time to wait (simulate) in nanoseconds
     */
    public void waitNS(double nanoseconds) {
        waitNS(nanoseconds, true);
    }

    // internal only method; applyVoltages calls this with the boolean false so no infinite recursion
    protected void waitNS(double nanoseconds, boolean applyVoltages) {
/*
        issueCommand("get_sim_time");
        StringBuffer buf = getLastCommandOutput();
        Matcher m = patSimTime.matcher(buf.toString());
        double cur_time = 0;
        if (m.find()) {
            try {
                cur_time = Double.parseDouble(m.group(1));
            } catch (NumberFormatException e) {
                System.out.println("Error in get_sim_time, could not parse time "+m.group(1)+" from:\n"+buf);
                return;
            }
        } else {
            System.out.println("Error determining current time, get_sim_time returned: "+buf);
            return;
        }
*/
        // first apply any voltages that have been set
        if (applyVoltages)
            applyVoltages();

        if (nanoseconds < timeStep) {
            System.out.println("Warning: cannot run simulator in increments less than time step (currently "+timeStep+" ns), setting it to "+timeStep+" ns");
            nanoseconds = timeStep;
        }
        // round wait time to the nearest time step, otherwise nanosim gets confused
        nanoseconds = (int)(nanoseconds/timeStep) * timeStep;

        // calculate stop time (time at which to set breakpoint)
        double stopTime = simTime + nanoseconds;
        // set break point
        issueCommand("set_time_break "+stopTime+"ns");
        issueCommand("cont_sim");
        simTime = stopTime;
    }

    public void waitPS(double ps) {
        waitNS(ps/1000.0);
    }

    public double getTimeNS() {
        return simTime;
/*
        issueCommand("get_sim_time");
        StringBuffer buf = getLastCommandOutput();
        Matcher m = patSimTime.matcher(buf.toString());
        double cur_time = 0;
        if (m.find()) {
            try {
                cur_time = Double.parseDouble(m.group(1));
            } catch (NumberFormatException e) {
                System.out.println("Error in get_sim_time, could not parse time "+m.group(1)+" from:\n"+buf);
                return cur_time;
            }
        } else {
            System.out.println("Error determining current time, get_sim_time returned: "+buf);
            return cur_time;
        }
        return cur_time;
*/
    }

    protected static final Pattern getSimTime_tres = Pattern.compile("The engine time resolution is\\s+: ([0-9\\.]+) ns");
    /**
     * Get the simulation's time step
     * @return the simulation time step in nanoseconds
     */
    protected double getSimTres() {
        issueCommand("get_sim_time");
        StringBuffer buf = getLastCommandOutput();
        Matcher m = getSimTime_tres.matcher(buf);
        if (m.find()) {
            try {
                double d = Double.parseDouble(m.group(1));
                return d;
            } catch (NumberFormatException e) {
                System.out.println("Error converting string to double in "+m.group(0)+": "+e.getMessage());
            }
        }
        double d = 0.01;    // default is 10ps
        System.out.println("Cannot determine time step, using default of "+d+" ns");
        return d;
    }

    // ====================================================================
    // ====================================================================

    //                      Get/Set voltages

    // ====================================================================
    // ====================================================================

    /**
     * Force node to a state.  Note that 1 is high and 0 is low.
     * The node is
     * case-insensitive, and may be a hierarchical spice name, such as 'Xtop.Xfoo.net@12'.
     * It should match the name from the spice file that nanosim is simulating.
     * @param node the hierarchical spice node name
     * @param state the state to set to, must be 1 or 0.
     */
    public void setNodeState(String node, int state) {
        node = node.toLowerCase();
        if (state != 0 && state != 1) {
            System.out.println("Illegal state passed to setNodeState: "+state+". Expected 0 or 1.");
            return;
        }
        setNodeVoltage(node, state*vdd);
    }

    /**
     * Set a node in the nanosim simulation to the specified voltage.
     * The node is
     * case-insensitive, and may be a hierarchical spice name, such as 'Xtop.Xfoo.net@12'.
     * It should match the name from the spice file that nanosim is simulating.
     * @param node the hierarchical spice node name
     * @param voltage the voltage to set the node to
     */
    public void setNodeVoltage(String node, double voltage) {
        node = node.toLowerCase();
        //issueCommand("set_node_v "+node+" "+voltage+" r=5 l=1n");
        nodesToSet.put(node, Double.valueOf(voltage));
    }

    /**
     * This is used to apply all voltages at the same time for the current time step.
     */
    protected void applyVoltages() {
        // release all forced nodes
        releaseNodes(new ArrayList(nodesToSet.keySet()));

        // all nodes are released, now apply all the values
        for (Iterator it = nodesToSet.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = (Map.Entry)it.next();
            String node = (String)entry.getKey();
            Double voltage = (Double)entry.getValue();
            if (assuraRCXNetlist) {
                node = node.replaceAll("\\.", "/");
            } else if (starRCXTNetlist) {
                if (node.startsWith("x")) node = node.substring(1);
                node = node.replaceAll("\\.x?", "/");
            }
            issueCommand("force_node_v v="+voltage.doubleValue()+" no="+node);
        }
        nodesToSet.clear();
        waitNS(timeStep, false);
    }

    // release the list of nodes (Strings)
    public void releaseNodes(List nodes) {
        // NOTE JKG 7/21/05:
        // Ajanta and I have found that release does not always work,
        // and sometimes the node remains "forced". Issuing another
        // release seems to remove the forced state, though, so the
        // following code issues releases until it is released
        // (or stops after 10 tries)
        int i=0;
        int nodesReleased = 0;
        while (nodesReleased < nodes.size() && i<10) {
            // make sure all nodes to set are not forced
            nodesReleased = 0;
            boolean releasedIssued = false;
            for (Iterator it = nodes.iterator(); it.hasNext(); ) {
                String node = (String)it.next();
                if (assuraRCXNetlist) {
                    node = node.replaceAll("\\.", "/");
                } else if (starRCXTNetlist) {
                    if (node.startsWith("x")) node = node.substring(1);
                    node = node.replaceAll("\\.x?", "/");
                }
                issueCommand("get_node_info detail=on "+node);
                StringBuffer output = getLastCommandOutput();
                if (output.indexOf("IS_FORCED: 1") != -1) {
                    // we need to release node
                    issueCommand("rel_node_v no="+node);
                    releasedIssued = true;
                } else {
                    nodesReleased++;            // this node is already released
                }
            }
            if (releasedIssued) {
                // advance time to try apply release
                waitNS(timeStep, false);
            }
            i++;
        }
    }

    List getNodeVoltages(List nodes) {
        return getNodeInfo(nodes, false);
    }

    List getNodeStates(List nodes) {
        return getNodeInfo(nodes, true);
    }

    /**
     * Get the state of a node.  Returns 1 or 0.
     * The node is
     * case-insensitive, and may be a hierarchical spice name, such as 'Xtop.Xfoo.net@12'.
     * It should match the name from the spice file that nanosim is simulating.
     * May return -1 if not a valid state.
     * @param node the hierarchical spice node name
     */
    public int getNodeState(String node) {
        List list = new ArrayList();
        list.add(node);
        List vals = getNodeInfo(list, true);
        if (vals != null && vals.size() > 0)
            return ((Number)vals.get(0)).intValue();
        return -1;
    }

    /**
     * Get the voltage on a node.
     * The node is
     * case-insensitive, and may be a hierarchical spice name, such as 'Xtop.Xfoo.net@12'.
     * It should match the name from the spice file that nanosim is simulating.
     * @param node the hierarchical spice node name
     * @return the voltage, or -1 if error
     */
    public double getNodeVoltage(String node) {
        List list = new ArrayList();
        list.add(node);
        List vals = getNodeInfo(list, false);
        if (vals != null && vals.size() > 0)
            return ((Double)vals.get(0)).doubleValue();
        return -1;
    }

    protected static final Pattern patNodeInfo = Pattern.compile("Node status of (.*?)\\((\\d+)\\): (.*?) \\(([0-9\\.\\-]+) V\\)");
    // digital only
    protected static final Pattern patNodeInfo2 = Pattern.compile("Node status of (.*?)\\((\\d+)\\): (\\d+)");

    /**
     * Gets the voltages on the nodes, unless returnState is true, in
     * which case it returns a list of 1's and 0's representing the logic state,
     * rather than the actual voltages.  Also returns -2 for undefined logic state, or -1 on error.
     * @param nodes the nodes to query
     * @param returnState true to convert voltages to 1 or 0 logic states
     * @return a list of voltages/states, or null on error
     */
    protected List getNodeInfo(List nodes, boolean returnState) {
        // apply any outstanding voltages
        if (nodesToSet.size() > 0)
            applyVoltages();
        if (assuraRCXNetlist || starRCXTNetlist) {
            List nodesfixed = new ArrayList();
            for (Iterator it = nodes.iterator(); it.hasNext(); ) {
                String node = (String)it.next();
                if (assuraRCXNetlist) {
                    node = node.replaceAll("\\.", "/");
                } else if (starRCXTNetlist) {
                    if (node.startsWith("x")) node = node.substring(1);
                    node = node.replaceAll("\\.x?", "/");
                }
                nodesfixed.add(node);
            }
            nodes = nodesfixed;
        }


        List nodeslc = new ArrayList();
        StringBuffer cmd = new StringBuffer();
        cmd.append("get_node_info ");
        for (Iterator it = nodes.iterator(); it.hasNext(); ) {
            String node = (String)it.next();
            node = node.toLowerCase();
            nodeslc.add(node);
            cmd.append(node+" ");
        }
        nodes = nodeslc;

        issueCommand(cmd.toString());
        StringBuffer result = getLastCommandOutput();
        String [] results = result.toString().trim().split("\n");

        List voltages = new ArrayList();
        List states = new ArrayList();
        int nodeIndex = 0;
        for (int i=0; i<results.length; i++) {
            if (!results[i].startsWith("Node status")) continue;        // quick check
            Matcher m = patNodeInfo.matcher(results[i]);
            Matcher m2 = patNodeInfo2.matcher(results[i]);
            String node = (String)nodes.get(nodeIndex);
            if (m.find()) {
                // don't do this check anymore, as nanosim can map the hierarchical name to the real name
                //if (!m.group(1).equals(node)) {
                //    System.out.println("Error on get_info_node: expected info for node "+node+" but got info for node "+m.group(1));
                //    return null;
                //}
                Integer state;
                // state can be one of 1, 0, or U
                if (m.group(3).equals("1"))
                    state = Integer.valueOf(1);
                else if (m.group(3).equals("0"))
                    state = Integer.valueOf(0);
                else if (m.group(3).equals("U"))
                    state = Integer.valueOf(-2);
                else {
                    System.out.println("Uknown state of "+node+": "+m.group(3)+", setting it to -1 (Undefined)");
                    state = Integer.valueOf(-1);
                }
                Double voltage;
                try {
                    voltage = Double.valueOf(m.group(4));
                } catch (NumberFormatException e) {
                    System.out.println("Error on get_info_node: NumberFormatException converting node "+node+" state/voltage ("+m.group(3)+"/"+m.group(4)+") to integer/double");
                    return null;
                }

                voltages.add(voltage);
                states.add(state);
                nodeIndex++;
                if (DEBUG) System.out.println("get_info_node "+node+": \tstate="+state+"\tvoltage="+voltage);
            } else if (m2.find() && returnState) {
                // digial only
                if (!m2.group(1).equals(node)) {
                    System.out.println("Error on get_info_node: expected info for node "+node+" but got info for node "+m.group(1));
                    return null;
                }
                Integer state;
                // state can be one of 1, 0, or U
                if (m2.group(3).equals("1"))
                    state = Integer.valueOf(1);
                else if (m2.group(3).equals("0"))
                    state = Integer.valueOf(0);
                else if (m2.group(3).equals("U"))
                    state = Integer.valueOf(-2);
                else {
                    System.out.println("Uknown state of "+node+": "+m2.group(3)+", setting it to -1 (Undefined)");
                    state = Integer.valueOf(-1);
                }
                states.add(state);
                nodeIndex++;
                if (DEBUG) System.out.println("get_info_node "+node+": \tstate="+state);
            }
        }
        if (returnState) return states;
        return voltages;
    }

    /**
     * Saves the state of all nodes at the given time to an external .ic file,
     * using the report_node_ic command.
     * @param time time in nanoseconds
     */
    public void reportNodeIC(double time) {
        issueCommand("report_node_ic all "+time+"ns");
    }

    /**
     * Saves the state of all nodes at the current time to an external .ic file,
     * using the report_node_ic command.
     */
    public void reportNodeIC() {
        issueCommand("report_node_ic all");
    }

    /** Unit Test
     * This test requires the file sim.spi in your working dir
     * */
    public static void main(String[] args) {
        NanosimModel nm = new NanosimModel();
        // the example file sim.spi needs to be in your working dir
        nm.start("nanosim", "sim.spi", 0);
        nm.issueCommand("get_sim_time");
        nm.finish();
    }
}
