/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: RawSpiceOut.java
 * Input/output tool: reader for Raw Spice output (.raw)
 * Written by Steven M. Rubin.
 *
 * Copyright (c) 2020, 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.input;

import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.tool.simulation.MutableSignal;
import com.sun.electric.tool.simulation.ScalarSample;
import com.sun.electric.tool.simulation.Signal;
import com.sun.electric.tool.simulation.SignalCollection;
import com.sun.electric.tool.simulation.SimulationTool;
import com.sun.electric.tool.simulation.Stimuli;
import com.sun.electric.tool.simulation.SweptSample;
import com.sun.electric.util.TextUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

/**
 * Class for reading and displaying waveforms from Raw Spice output
 * (including LTSpice).  These are contained in .raw files.
 */
public class RawSpiceOut extends Input<Stimuli>
{
	RawSpiceOut() {
        super(null);
    }

	private static final boolean DEBUG = false;

	private boolean complexValues;

	/**
	 * Method to read an LTSpice output file.
	 */
	protected Stimuli processInput(URL fileURL, Cell cell, Stimuli sd)
		throws IOException
	{
		sd.setNetDelimiter(" ");

		// open the file
		if (openBinaryInput(fileURL)) return sd;

        // see if using newer or older versions of raw LTSpice
		boolean newRawLTspice = SimulationTool.isSpiceUseNewerLTSpice();

		// show progress reading .raw file
		System.out.println("Reading Spice3/NGSpice/" + (newRawLTspice ? "LTSpiceXVII" : "LTSpiceIV") + "/SmartSpice raw output file: " + fileURL.getFile());
		startProgressDialog("LTSpice output", fileURL.getFile());

        // read the actual signal data from the .raw file
		sd = readRawSpice3File(cell, sd, newRawLTspice);

		// stop progress dialog, close the file
		stopProgressDialog();
		closeInput();
        return sd;
	}

	private Stimuli readRawSpice3File(Cell cell, Stimuli sd, boolean newRawLTspice)
		throws IOException
	{
		complexValues = false;
		boolean realValues = false;
		int signalCount = -1;
		boolean firstFieldIsTime = false;
		String[] signalNames = null;
		int rowCount = -1;
        boolean isLTSpice = false;
		boolean first = true;

		sd.setCell(cell);
		SignalCollection sc = null;

        double[] time = null;
		for(;;)
		{
			String line = getLineFromBinary();
			if (line == null) break;
			updateProgressDialog(line.length());
			if (newRawLTspice) line = new String(line.getBytes(), "UTF-16");

			// make sure this isn't an HSPICE deck (check first line)
			if (first)
			{
				first = false;
				if (line.length() >= 20)
				{
					String hsFormat = line.substring(16, 20);
					if (hsFormat.equals("9007") || hsFormat.equals("9601"))
					{
						System.out.println("This is an HSPICE file, not a RAWFILE file");
						System.out.println("Change the SPICE format (in Preferences) and reread");
						return null;
					}
				}
			}

			// find the ":" separator
			int colonPos = line.indexOf(':');
			if (colonPos < 0) continue;
			String keyWord = line.substring(0, colonPos);
			String restOfLine = line.substring(colonPos+1).trim();

            if (keyWord.equals("Command"))
            {
                isLTSpice = restOfLine.indexOf("LTspice")!=-1;
                continue;
            }

			if (keyWord.equals("Plotname"))
            {
                // see if known analysis is specified
                // terminate any previous analysis
                if (sc != null)
                {
                    signalCount = -1;
                    rowCount = -1;
                    signalNames = null;
                }

                // start reading a new analysis
                if (restOfLine.startsWith("Transient Analysis"))
                {
                    sc = Stimuli.newSignalCollection(sd, "TRANS SIGNALS");
                } else if (restOfLine.startsWith("DC "))
                {
                    sc = Stimuli.newSignalCollection(sd, "DC SIGNALS");
                } else if (restOfLine.startsWith("AC "))
                {
                    sc = Stimuli.newSignalCollection(sd, "AC SIGNALS");
                } else if (restOfLine.startsWith("Operating "))
                {
                    sc = Stimuli.newSignalCollection(sd, "OPERATING POINT");
                } else
                {
                    System.out.println("Warning: unknown analysis: " + restOfLine);
                    sc = Stimuli.newSignalCollection(sd, "TRANS SIGNALS");
                }
                continue;
            }

			if (keyWord.equals("Flags"))
            {
                // the first signal is Time
                int complex = restOfLine.indexOf("complex");
                if (complex >= 0) complexValues = true;
                int r = restOfLine.indexOf("real");
                if (r >= 0) realValues = true;
                continue;
            }

			if (keyWord.equals("No. Variables"))
            {
                // the first signal is Time
                signalCount = TextUtils.atoi(restOfLine);
                continue;
            }

			if (keyWord.equals("No. Points"))
            {
                rowCount = TextUtils.atoi(restOfLine);
                time = new double[rowCount];
                continue;
            }

            if (!isLTSpice)
            {
                if (keyWord.equals("Variables"))
                {
                    if (signalCount < 0)
                    {
                        System.out.println("Missing variable count in file");
                        return null;
                    }
                    signalNames = new String[signalCount];
                    int trueSignalCount = 0;
                    for(int i=0; i<signalCount; i++)
                    {
                        if (restOfLine.length() > 0)
                        {
                            line = restOfLine;
                            restOfLine = "";
                        } else
                        {
                            line = getLineAndUpdateProgressBinary();
                            if (line == null)
                            {
                                System.out.println("Error: end of file during signal names");
                                return null;
                            }
                        }
                        line = line.trim();
                        int numberOnLine = TextUtils.atoi(line);
                        if (numberOnLine != i)
                            System.out.println("Warning: Variable " + i + " has number " + numberOnLine);
                        int spacePos = line.indexOf(" ");   if (spacePos < 0) spacePos = line.length();
                        int tabPos = line.indexOf("\t");    if (tabPos < 0) tabPos = line.length();
                        int pos = Math.min(spacePos, tabPos);
                        String name = line.substring(pos).trim();
                        spacePos = name.indexOf(" ");   if (spacePos < 0) spacePos = name.length();
                        tabPos = name.indexOf("\t");    if (tabPos < 0) tabPos = name.length();
                        pos = Math.min(spacePos, tabPos);
                        name = name.substring(0, pos);
                        if (i == 0)
                        {
                            if (name.equals("time"))
                            {
                            	firstFieldIsTime = true;
                            	continue;
                            }
                        }
                        signalNames[trueSignalCount++] = name;
                    }
                    signalCount = trueSignalCount;
                    continue;
                }
                if (keyWord.equals("Values"))
                {
                    if (signalCount < 0)
                    {
                        System.out.println("Missing variable count in file");
                        return null;
                    }
                    if (rowCount < 0)
                    {
                        System.out.println("Missing point count in file");
                        return null;
                    }
                    if (rowCount == 0)
                    {
                        System.out.println("No points in file");
                        return null;
                    }
                    double[][] values = new double[signalCount][rowCount];
                    for(int j=0; j<rowCount; j++)
                    {
                    	int valueFill = 0;
                    	int valuesToRead = signalCount;
                        if (firstFieldIsTime) valuesToRead++; else
                        	time[j] = j;
                        for(int i = -1; i < valuesToRead; )
                        {
                            line = getLineAndUpdateProgressBinary();
                            if (line == null)
                            {
                                System.out.println("Error: end of file during data points (read " + j + " out of " + rowCount);
                                return null;
                            }
                            line = line.trim();
                            if (line.length() == 0) continue;
                            int charPos = 0;
                            while (charPos <= line.length())
                            {
                                int tabPos = line.indexOf("\t", charPos);
                                if (tabPos < 0) tabPos = line.length();
                                String field = line.substring(charPos, tabPos);
                                charPos = tabPos+1;
                                while (charPos < line.length() && line.charAt(charPos) == '\t') charPos++;
                                if (i < 0)
                                {
                                    int lineNumber = TextUtils.atoi(field);
                                    if (lineNumber != j)
                                        System.out.println("Warning: event " + j + " has wrong event number: " + lineNumber);
                                } else
                                {
                                    double val = TextUtils.atof(field);
                                    if (i == 0 && firstFieldIsTime) time[j] = val; else
                                        values[valueFill++][j] = val;
                                }
                                i++;
                                if (i >= valuesToRead) break;
                            }
                        }
                    }
                    for (int i = 0; i < signalCount; i++)
                        ScalarSample.createSignal(sc, sd, signalNames[i], null, time, values[i]);
                    continue;
                }
                if (keyWord.equals("Binary"))
                {
                    if (signalCount < 0)
                    {
                        System.out.println("Missing variable count in file");
                        return null;
                    }
                    if (rowCount < 0)
                    {
                        System.out.println("Missing point count in file");
                        return null;
                    }
                    if (rowCount == 0)
                    {
                        System.out.println("No points in file");
                        return null;
                    }

                    // read the data
                    double[][] values = new double[signalCount][rowCount];
                    for(int j=0; j<rowCount; j++)
                    {
                        time[j] = getNextDouble();
                        for(int i=0; i<signalCount; i++)
                            values[i][j] = getNextDouble();
                    }
                    for (int i = 0; i < signalCount; i++)
                        ScalarSample.createSignal(sc, sd, signalNames[i], null, time, values[i]);
                    continue;
                }
            } else
            {
boolean OLD = true;
if (OLD) firstFieldIsTime = true;
                if (keyWord.equals("Variables"))
                {
                    if (signalCount < 0)
                    {
                        System.out.println("Missing variable count in file");
                        return null;
                    }
                    signalNames = new String[signalCount];
                    int trueSignalCount = 0;
                    for(int i=0; i<signalCount; i++)
                    {
                        restOfLine = getLineFromBinary();
                        if (restOfLine == null) break;
                        updateProgressDialog(restOfLine.length());
                        if (newRawLTspice) restOfLine = new String(restOfLine.getBytes(), "UTF-16");
                        restOfLine = restOfLine.trim();
                        int indexOnLine = TextUtils.atoi(restOfLine);
                        if (indexOnLine != i)
                            System.out.println("Warning: Variable " + i + " has number " + indexOnLine);
                        int nameStart = 0;
                        while (nameStart < restOfLine.length() && !Character.isWhitespace(restOfLine.charAt(nameStart))) nameStart++;
                        while (nameStart < restOfLine.length() && Character.isWhitespace(restOfLine.charAt(nameStart))) nameStart++;
                        int nameEnd = nameStart;
                        while (nameEnd < restOfLine.length() && !Character.isWhitespace(restOfLine.charAt(nameEnd))) nameEnd++;
                        String name = restOfLine.substring(nameStart, nameEnd);
                        if (name.startsWith("V(") && name.endsWith(")"))
                        {
                            name = name.substring(2, name.length()-1);
                        }
                        if (i == 0)
                        {
if (OLD) continue;
                            if (name.equals("time"))
                            {
                            	firstFieldIsTime = true;
                            	continue;
                            }
                        }
                        signalNames[trueSignalCount++] = name;
                    }
                    signalCount = trueSignalCount;
                    continue;
                }

                if (keyWord.equals("Binary"))
                {
                    if (signalCount < 0)
                    {
                        System.out.println("Missing variable count in file");
                        return null;
                    }
                    if (rowCount < 0)
                    {
                        System.out.println("Missing point count in file");
                        return null;
                    }
                    if (rowCount == 0)
                    {
                        System.out.println("No points in file");
                        return null;
                    }
                    if (DEBUG)
                    {
                        System.out.println(signalCount+" VARIABLES, "+rowCount+" SAMPLES");
                        for(int i=0; i<signalCount; i++)
                            System.out.println("VARIABLE "+i+" IS "+signalNames[i]);
                    }

                    if (newRawLTspice)
                    {
                    	// remove a character (2 bytes) at end of header to make alignment for reading binary block
                    	int c = dataInputStream.read();
                    	updateProgressDialog(Integer.toString(c).length());
                    }

                    // read all of the data in the RAW file
                    double[][] values = new double[signalCount][rowCount];
                    time = new double[rowCount];
                    for(int j=0; j<rowCount; j++)
                    {
                        double t = j;
                        if (firstFieldIsTime) t = getNextDouble();
                        if (DEBUG) System.out.println("TIME AT "+j+" IS "+t);
                        time[j] = Math.abs(t);
                        for(int i=0; i<signalCount; i++)
                        {
                            double value = 0;
                            if (realValues) value = getNextFloat(); else
                                value = getNextDouble();
                            if (DEBUG) System.out.println("   DATA POINT "+i+" ("+signalNames[i]+") IS "+value);
                            values[i][j] = value;
                        }
                    }

                    // figure out where the sweep breaks occur
                    List<Integer> sweepLengths = new ArrayList<Integer>();
                    int sweepStart = 0;
                    int sweepCount = 0;
                    for(int j=1; j<=rowCount; j++)
                    {
                        if (j == rowCount || time[j] < time[j-1])
                        {
                            int sl = j - sweepStart;
                            sweepLengths.add(Integer.valueOf(sl));
                            sweepStart = j;
                            sweepCount++;
                        }
                    }
                    if (DEBUG) System.out.println("FOUND " + sweepCount + " SWEEPS");
                    Signal<?> [][] signals = new Signal<?>[signalCount][sweepCount];
                	for(int i=0; i<signalCount; i++)
                    {
                        String name = signalNames[i];
                        int lastDotPos = name.lastIndexOf('.');
                        String context = null;
                        if (lastDotPos >= 0)
                        {
                            context = name.substring(0, lastDotPos);
                            name = name.substring(lastDotPos + 1);
                        }
                        for(int s=0; s<sweepCount; s++)
            				signals[i][s] = ScalarSample.createSignal(sc, sd, name, context);
                    }

                    // place data into the Sample object
                    int offset = 0;
                    for(int s=0; s<sweepCount; s++)
                    {
                        int sweepLength = sweepLengths.get(s).intValue();
                        for(int i=0; i<signalCount; i++)
                        {
                        	MutableSignal<ScalarSample> ms = (MutableSignal<ScalarSample>)signals[i][s];
                            for(int j=0; j<sweepLength; j++)
                            {
                                ms.addSample(time[j + offset], new ScalarSample(values[i][j+offset]));
                            }
                        }
                        offset += sweepLength;
                    }

                    String[] sweepNames = new String[sweepCount];
                    for(int s=0; s<sweepCount; s++) sweepNames[s] = "" + (s+1);
                    for(int i=0; i<signalCount; i++)
                    {
                        String name = signalNames[i];
                        int lastDotPos = name.lastIndexOf('.');
                        String context = null;
                        if (lastDotPos >= 0)
                        {
                            context = name.substring(0, lastDotPos);
                            name = name.substring(lastDotPos + 1);
                        }
                    	SweptSample.createSignal(sc, sd, name, context, false, (Signal<ScalarSample>[])signals[i]);
                    }
                    sc.setSweepNames(sweepNames);
                    return sd;
                }
            }
        }
        return sd;
	}

	private double getNextDouble()
		throws IOException
	{
		// Java dataInputStream.readLong() will always return big-endian words. Spice3-raw is native byte order.
		long lt = dataInputStream.readLong();
		if (ByteOrder.nativeOrder() != ByteOrder.BIG_ENDIAN)
		    lt = Long.reverseBytes(lt);
		double t = Double.longBitsToDouble(lt);
		int amtRead = 8;

		// for complex plots, ignore imaginary part
		if (complexValues) { amtRead *= 2;   dataInputStream.readLong(); }

		updateProgressDialog(amtRead);
		return t;
	}

	private float getNextFloat()
		throws IOException
	{
		// Java dataInputStream.readInt() will always return big-endian integers. Spice3-raw is native byte order.
		int lt = dataInputStream.readInt();
		if (ByteOrder.nativeOrder() != ByteOrder.BIG_ENDIAN)
		    lt = Integer.reverseBytes(lt);
		float t = Float.intBitsToFloat(lt);
		int amtRead = 4;

		// for complex plots, ignore imaginary part
		if (complexValues) { amtRead *= 2;   dataInputStream.readInt(); }

		updateProgressDialog(amtRead);
		return t;
	}

}
