/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: TableData.java
 * Written by Jonathan Gainsley, Sun Microsystems.
 *
 * Copyright (c) 2009, 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.simulation.sctiming;

import java.util.*;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.StreamTokenizer;

/**
 * TableData provides storage for the measurement results read from
 * a spice mt0 file. Data is stored as it is encountered in the
 * mt0 file, with each row of the table corresponding to a row (sweep) in the
 * mt0 file. Columns correspond to a particular measurement.
 * User: gainsley
 * Date: Nov 16, 2006
 */
public class TableData {

    List<String> headers;
    List<double[]> rows;

    /**
     * Create a new TableData for storing spice measurement results.
     * Measurement names must be specified.
     * @param headers measurement names
     */
    public TableData(List<String> headers) {
        this.headers = headers;
        this.rows = new ArrayList<double[]>();
    }

    /**
     * Add a row to the table. The number of entries in the row
     * must equal the number of headers declared for the table.
     * @param row row to add to the table
     */
    public void addRow(double [] row) {
        if (row.length != headers.size()) {
            System.out.println("Cannot add row of length "+row.length+" to table of length "+headers.size());
            return;
        }
        rows.add(row);
    }

    /**
     * Get the number of columns (number of headers) for the table.
     * @return number of columns
     */
    public int getNumCols() { return headers.size(); }

    /**
     * Get the number of rows in the table.
     * @return number of rows in the table.
     */
    public int getNumRows() { return rows.size(); }

    /**
     * Get the headers for the table.
     * @return headers for the table.
     */
    public List<String> getHeaders() { return headers; }

    /**
     * Get the data for the given row. Will throw
     * an IndexOutOfBoundsException if the row is out of bounds
     * @param row the row number
     * @return data for that row
     */
    public double[] getRow(int row) {
        return rows.get(row);
    }

    /**
     * Get the index of the given header in the headers list
     * @param headername the header name
     * @return index of the given header in the headers list,
     * or -1 if not found
     */
    public int getColumn(String headername) {
        if (headername == null) return -1;
        for (int i=0; i<headers.size(); i++) {
            if (headers.get(i).equalsIgnoreCase(headername)) return i;
        }
        return -1;
    }

    /**
     * Get the value in a given row and column (header)
     * @param row the row
     * @param header the column header
     * @return the value, or Double.NaN if either row or column is out of bounds
     */
    public double getValue(int row, String header) {
        int col = getColumn(header);
        if (col < 0) return Double.NaN;
        if (row < 0 || row >= rows.size()) return Double.NaN;
        return (rows.get(row))[col];
    }

    /**
     * Print the data in this TableData as a pretty ASCII table.
     */
    public void printData() {
        int colwidth = 12;
        for (String s : headers) {
            if (s.length() > colwidth) colwidth = s.length() + 1;
        }
        StringBuffer line = new StringBuffer();
        for (String s : headers) {
            line.append(s);
            for (int i=s.length(); i<colwidth; i++) line.append(" ");
        }
        System.out.println(line);
        for (double [] row : rows) {
            line = new StringBuffer();
            for (int i=0; i<row.length; i++) {
                String s = Double.toString(row[i]);
                line.append(s);
                for (int j=s.length(); j<colwidth; j++) line.append(" ");
            }
            System.out.println(line);
        }
    }

    /**
     * Get the a Table2D object containing the data in this TableData.
     * Most data for characterization varies with respect to
     * one or two parameters being swept. Thus, it is more
     * intuitive to store that data where the rows and cols
     * are two of the parameters from this table.
     * @param index1 the name of the parameter/measure to be the row index
     * @param index2 the name of the parameter/measure to be the column index
     * @param index3 a third index that can be used to mask out averaging (see index3MaskValues)
     * @param index3ExcludeValues if the size of the table is greater than the unique values
     * of index1 * index2, getTable2D normally averages the extra values. To selectively
     * choose which values to exclude from this averaging, put those values in this array.
     * If null, all values will be averaged.
     * @return a Table2D object with the same data
     */
    public Table2D getTable2D(String index1, String index2, String index3, String index3ExcludeValues) {
        int i1 = getColumn(index1);
        int i2 = getColumn(index2);
        int i3 = getColumn(index3);
        double [] excludeValues = getDoubleValues(index3ExcludeValues);
        if (i1 < 0 || i2 < 0) return null;
        List<Double> values1 = new ArrayList<Double>();
        List<Double> values2 = new ArrayList<Double>();
        for (double [] row : rows) {
            Double d1 = new Double(row[i1]);
            Double d2 = new Double(row[i2]);
            if (!values1.contains(d1)) values1.add(d1);
            if (!values2.contains(d2)) values2.add(d2);
        }
        double [] di1 = new double[values1.size()];
        double [] di2 = new double[values2.size()];
        int i=0;
        for (Iterator<Double> it = values1.iterator(); it.hasNext(); ) {
            di1[i] = it.next().doubleValue();
            i++;
        }
        i=0;
        for (Iterator<Double> it = values2.iterator(); it.hasNext(); ) {
            di2[i] = it.next().doubleValue();
            i++;
        }
        Table2D table = new Table2D(di1, index1, di2, index2);
        boolean getStdDev = false;
        if (di1.length * di2.length < getNumRows()) {
            System.out.println("Warning: Conversion of TableData to Table2D will average" +
                    " some values because the number of unique values of "+index1+" times "+index2+
                    " is smaller than the dimensions of the TableData");
            getStdDev = true;
        }

        for (int j=0; j<headers.size(); j++) {
            String key = headers.get(j);
            if (key.equals(index1) || key.equals(index2)) continue;

            double [][] data = new double[di1.length][di2.length];
            int [][] count = new int[di1.length][di2.length];
            for (int x=0; x<di1.length; x++) {
                Arrays.fill(data[x], 0);
                Arrays.fill(count[x], 0);
            }
            for (double [] row : rows) {
                // get index
                double ii1 = row[i1];
                double ii2 = row[i2];

                // check if this value should be excluded
                boolean skipThisVal = false;
                if (i3 >= 0) {
                    double ii3 = row[i3];
                    for (double excludeval : excludeValues) {
                        if (excludeval == ii3) {
                            skipThisVal = true;
                            //System.out.println("Excluding value "+index3+"="+ii3+" from standard deviation of "+index3+" in table of "+index1+" vs "+index2);
                            break;
                        }
                    }
                }
                if (skipThisVal) continue;

                int r=0, c=0;
                for (r=0; r<di1.length; r++) {
                    if (di1[r] == ii1) break;
                }
                for (c=0; c<di2.length; c++) {
                    if (di2[c] == ii2) break;
                }
                data[r][c] += row[j];
                count[r][c]++;
            }
            for (int r=0; r<di1.length; r++) {
                for (int c=0; c<di2.length; c++) {
                    if (count[r][c] > 1) {
                        data[r][c] /= count[r][c];
                    }
                }
            }
            table.setData(key, data);
            if (getStdDev) {
                // we averaged the values, let's get the std dev
                double [][] stddev = new double[di1.length][di2.length];
                for (int x=0; x<di1.length; x++) {
                    Arrays.fill(stddev[x], 0);
                }
                for (double [] row : rows) {
                    // get index
                    double ii1 = row[i1];
                    double ii2 = row[i2];

                    // check if this value should be excluded
                    boolean skipThisVal = false;
                    if (i3 >= 0) {
                        double ii3 = row[i3];
                        for (double excludeval : excludeValues) {
                            if (excludeval == ii3) {
                                skipThisVal = true;
                                //System.out.println("Excluding value "+index3+"="+ii3+" from standard deviation of "+index3+" in table of "+index1+" vs "+index2);
                                break;
                            }
                        }
                    }
                    if (skipThisVal) continue;

                    int r=0, c=0;
                    for (r=0; r<di1.length; r++) {
                        if (di1[r] == ii1) break;
                    }
                    for (c=0; c<di2.length; c++) {
                        if (di2[c] == ii2) break;
                    }
                    double diff = data[r][c] - row[j]; // avg - value
                    stddev[r][c] += diff*diff; // running total of diff^2
                }
                int thirdD = getNumRows() / di1.length / di2.length;
                for (int r=0; r<di1.length; r++) {
                    for (int c=0; c<di2.length; c++) {
                        stddev[r][c] = Math.sqrt(stddev[r][c]/thirdD); // sqrt(sum of diffs / num diffs)
                    }
                }
                table.setData(key+"_stddev", stddev);
            }
        }
        return table;
    }

    /**
     * Get the average (mean) value of the data in a given column (header)
     * @param column the column
     * @return the mean
     */
    public double getAverage(int column) {
        if (column < 0 || column >= headers.size()) return 0;
        double total = 0;
        for (double[] row : rows) {
            total += row[column];
        }
        return (total/rows.size());
    }

    private double [] getDoubleValues(String list) {
        if (list == null) return new double [] {};
        String [] svals = list.split("\\s+");
        double [] dvals = new double[svals.length];
        Arrays.fill(dvals, -1);
        for (int i=0; i<svals.length; i++) {
            try {
                dvals[i] = Double.valueOf(svals[i]);
            } catch (NumberFormatException e) {
                return new double [] {};
            }
        }
        return dvals;
    }

    /**
     * Parse spice measurement results from the given mt0 file
     * and put them in a TableData object.
     * @param filename the spice mt0 file (path and extension)
     * @return the measurements in a TableData object
     * @throws SCTimingException
     */
    public static TableData readSpiceMeasResults(String filename) throws SCTimingException {
        BufferedReader reader;
        try {
            reader = new BufferedReader(new FileReader(filename));
        } catch (java.io.FileNotFoundException e) {
            throw new SCTimingException(e.getMessage());
        }
        try {
            int ttype;
            List<String> headers = new ArrayList<String>();
            StreamTokenizer s = new StreamTokenizer(reader);
            s.resetSyntax();
            s.wordChars(0, 255);
            s.whitespaceChars(' ', ' ');
            s.whitespaceChars('\t', '\t');
            s.whitespaceChars('\r', '\r');
            s.whitespaceChars('\n', '\n');
            s.whitespaceChars('\f', '\f');
            s.quoteChar('\'');
            s.quoteChar('"');
            s.eolIsSignificant(true);
            boolean title = false;
            // find start of header. it is after title line
            while ((ttype = s.nextToken()) != StreamTokenizer.TT_EOF) {
                if (!title && ttype == StreamTokenizer.TT_WORD && s.sval.equalsIgnoreCase(".TITLE")) {
                    title = true; continue;
                }
                if (title && ttype == StreamTokenizer.TT_EOL)
                    break;
            }
            // this is start of indices
            s.eolIsSignificant(false);
            while ((ttype = s.nextToken()) != StreamTokenizer.TT_EOF) {
                if (ttype != StreamTokenizer.TT_WORD)
                    throw new SCTimingException("Expected string, but got "+s.toString()+" while parsing index data of "+filename);
                headers.add(s.sval);
                if (s.sval.equals("alter#")) break;
            }
            TableData data = new TableData(headers);
            int i=0;
            double [] row = new double[headers.size()];
            while ((ttype = s.nextToken()) != StreamTokenizer.TT_EOF) {
                if (i == 0) {
                    row = new double[headers.size()];
                }
                if (ttype != StreamTokenizer.TT_WORD)
                    throw new SCTimingException("Expected value, but got '"+s.toString()+"' while parsing index data of "+filename);
                try {
                    row[i] = Double.parseDouble(s.sval);
                } catch (NumberFormatException e) {
                    if (s.sval.equalsIgnoreCase("failed")) {
                        row[i] = Double.NaN;
                    } else {
                        throw new SCTimingException("Expected numeric value, but got '"+s.toString()+"' while parsing spice measurement file "+filename);
                    }
                }
                i++;
                if (i == headers.size()) {
                    data.addRow(row);
                    i = 0;
                }
            }
            reader.close();
            return data;
        } catch (java.io.IOException e) {
            throw new SCTimingException(e.getMessage());
        }
    }

}
