package nxm.sys.libg;

import java.awt.Color;

import nxm.sys.inc.Drawable;
import nxm.sys.inc.PlotFile;
import nxm.sys.lib.Args;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Table;

/** A class to hold and plot a multi-track data layer. Supports plotting Midas
    Type 3000 files with multiple Y traces, in addition to other file types. <br>
    <br>
    In NeXtMidas 3.3.2 this class was updated to use double-precision rather than
    single-precision computations (see Bug 2721 - LayerMulti / LayerScatter:
    Incorrect plotting of time values). This update necessitated API changes for
    the following <code>protected</code> variables/functions and may have limited
    backwards-compatibility for some subclasses of {@link LayerMulti}:
    <pre>
      NeXtMidas 3.3.1      | NeXtMidas 3.3.2+      | Notes
      ---------------------+-----------------------+----------------------------
      float[][] recFbuf    | double[][] recData    | changed name, type
      float     minY       | double     minY       | changed type
      float     maxY       | double     maxY       | changed type
      ---------------------+-----------------------+----------------------------
      updateRecFbuf(..)    | updateRecData(..)     | changed name, input type
      ---------------------+-----------------------+----------------------------
    </pre>

    @author  Neon Ngo / J. Czechowski IV
 */
public class LayerMulti extends Layer {
  protected static final int OVERLAP = 1; // Buffers have 1-element overlap so that line can start at end of previous transfer length.

  // size  = Line-segments per Buffer (e.g. elements/buffer); adjustable via /LPB=
  // frame = Line-segments Per Screen (e.g number of rows (elements) in file); adjustable via /LPS=
  //         For static data file: LPB <= LPS
  //         For pipe data file:   LPB  = LPS

  protected double[][] recData;         // Data read in from file (converted to float) [# y traces (cols)][size (elements)+1]
                                        // Columns (traces) that are marked to be shown will be allocated and initialized (non null)
  protected String[]   recNames;        // columns (y traces) name mapping in file. colNames.length = recFbuf.length
  protected String     showColumns="*"; // Column names (e.g. "ONE|TWO|TEN", "*" = show all).
  protected boolean    showColsChanged; // flag to signal if we need to update recFbuf b/c showColumns was updated
  protected Table[]    rowBuf;          // Buffer to read Table of rows into. rowBuf[transfer length (elements)]
  protected int        lastRow;         // The last row from previous frame of data (lines connect between frames)
  protected int        recIndex;        // Index of NEXT insertion into recFbuf, for realtime data
  protected int        recSize;         // Number of valid elements in recFbuf, for realtime data and paged mode
  protected boolean    frameReady;      // Is the frame ready to be drawn?
  protected boolean    frameDrawn;      // Has the frame been drawn?
  protected double     minY = 0;        // Minimum Y value read in.
  protected double     maxY = 1;        // Maximum Y value read in.
  protected boolean    rereadFile;      // Reread file starting at 0?

  /** Constructors */
  public LayerMulti () {
    this("Multi");
  }

  /** Constructors */
  protected LayerMulti (String name) {
    super(name);
  }

  /** private Since NeXtMidas 3.7.0, was deprecated in 2.5.4: This constructor is not used and will be removed from future releases. */
  private LayerMulti (int ncolors) { // TODO: remove in 3.9+ or 3.7.1+
    this();
    setColorMap(MColor.MULTI, ncolors);
  }

  @Override
  public boolean setFile (PlotFile pf, Table tab, Args MA) {
    setFile(pf);

    if (pfnh == null) {
      warning("LayerMulti requires file to implement the PlotFileNH interface");
      return false;
    }

    int nels   = (int)pf.getSize();        // number of rows in the file
    int numRec = pfnh.getRecordDefCount(); // number of columns in the file (i.e. max number of traces)

    setXAxis(pf.getXStart(), pf.getXDelta());
    setColorMap(MColor.MULTI, numRec);

    recData  = new double[numRec][]; // allocate double[] for each column (y trace) and column name mapping
    recIndex = 0;

    String[] names = new String[numRec];  // allocate sub-records (columns) name mapping in file
    for (int i=0; i<numRec; i++) {
      Table recDefTab = pfnh.getRecordDef(i);
      names[i] = recDefTab.getS("NAME");
    }
    setRecNames(names);
    showColsChanged = true; // Force a reset of the columns shown.

    if (pfnh instanceof DataFile) {
      DataFile df = (DataFile)pf;
      if (df.isPipe()) {
        int ps = (int)(df.getPipeSize() / df.getBPE()); // number of elements that can fit in the pipe size
        // Case 1: If nels is greater than ps OR
        // Case 2: If the header size is zero then use ps (for BUG 954)
        if (nels>ps || nels==0) nels = ps;
      }
    }

    int lps   = MA.getL("/LPS", nels); // Line-segments Per Screen (defaults to the number of elements in data file)
    int lpb   = MA.getL("/LPB", -1);   // Line-segments Per Buffer (recFBuf[]), -1 to use default buffer size
    if (realtime) {                    // In realtime mode, LPB has to be the same as LPS
      lpb = lps;                       //    otherwise a refresh draw will not have all data points
    } else if (lpb<0) {                // In non-realtime mode, if LPB is not specified (e.g negative)
      lpb = Math.min(lps, 1024);       //    then set LPB so that if LPS is greater than 1K, layer will go into paging mode
    }
    int tl   = lpb;                    // transfer length for Table buffer (rowBuf) (currently this should be same as lpb)
    rowBuf   = new Table[tl+OVERLAP];  // allocate row buffer of Tables for a single transfer length (+1 to join to previous frame)
    setSize(lpb);                      // set Number of elements (rows) in the buffer
    setFrame(lps);                     // set frame size (DEF=number of rows (elements) in file)

    isPaged    = !realtime && (lpb < lps);
    frameReady = false;
    frameDrawn = false;
    lastRow    = 0;

    return true;
  }

  /** Does the frame need to be drawn? */
  protected boolean getDrawFrame () {
    return (frameReady && !frameDrawn);
  }

  @Override
  public boolean getNeedRefresh () {
    return super.getNeedRefresh();// || getDrawFrame();
  }

  /** Sets the color of the traces using their column (record) name. The keys in the table
      corresponds to the column names and their value are the colors to use for that trace. Only the
      colors of keys that are specified in the table will be overridden, unless there is a color
      value set in the "_GLOBAL_" key. In that case, the color for those traces that are not
      specified will be the _GLOBAL_'s (i.e. default) color. Note that only named columns are
      considered (the "_N_" syntax is not supported in the color table). <br>
      <br>
      Example usage:
      <pre>
        Override the ORD (column) to red and ORD2 column to use green, other columns
        that are plotted will not have their colors changed.
          setColorTable("{ORD=red,ORD2=green}")

        Override the ABCS (column) to #FF0000 (red) and ORD2 column to use yellow,
        All other columns will be white.
          setColorTable("{ABCS=#FF0000,ORD2=yellow,_GLOBAL_=WHITE}")

      </pre>
      @param colorTab Table of colors for specified columns.
      @since NeXtMidas 2.5.5
      @see MColor#getColor(Object) on how to specify the color
   */
  public void setColorTable (Table colorTab) {
    if (colorTab==null || colorTab.isEmpty()) return;

      Color defaultColor   = null;
      Object globalDefault = colorTab.getKey("_GLOBAL_"); // global default color
      if (globalDefault!=null) {
        defaultColor = MColor.getColor(globalDefault);
      }

      for (int i=0; i<recNames.length && i<colors.length+1; i++) { // make sure we are in the bounds of colors[]
        String colname = recNames[i];
        Color  color   = defaultColor;
        if (!colname.equals("_"+i+"_") && colorTab.containsKey(colname)) {
          color = MColor.getColor(colorTab.get(colname));
        }
        if (color!=null) {      // only update color if it is specified
          colors[i+1] = color;  // color[0] is reserved for bgcolor, hence i+1
        }
      }
      refresh();
  }

  /** Get all the current color settings for each column as a table of Strings. Note that only named
      columns are considered. The "_N_" syntax is not supported in the color table.
      @return Table of colors for specified columns.
      @since NeXtMidas 2.5.5
   */
  public Table getColorTable () {
    Table tbl = new Table();
    for (int i=0; i<recNames.length && i<colors.length+1; i++) { // make sure we are in the bounds of colors[]
      String colname = recNames[i];
      if (!colname.equals("_"+i+"_")) {
        tbl.setKey(colname, MColor.toString(colors[i+1]));
      }
    }
    return tbl;
  }

  /** Sets the columns to show. This is the same as {@link #setTraces(String)}.
      @param columns The traces to show, multiple columns can be shown separated by a "|". If
                     this is the <code>null</code> or <code>"*"</code> all traces will be shown.
      @since NeXtMidas 2.1.0
   */
  public final void setColumns (String columns) {
    setTraces(columns);
  }

  /** Gets the columns to show. This is the same as {@link #getTraces()}.
      @return The list of columns displayed with multiple columns can be shown separated by a "|".
              If all columns are displayed, "*" is returned.
      @since NeXtMidas 2.1.0
   */
  public final String getColumns () {
    return getTraces();
  }

  /** Sets the line traces to show. The traces will be shown in the same order as in the file
      (regardless) of how they are specified when passed to this method; this is done so
      that the colors of the individual lines shown will not change if one of them is
      turned on/off.
      @param traces The traces to show, multiple lines can be shown separated by a "|". If
                    this is the <code>null</code> or <code>"*"</code> all traces will be shown. If this
                    is anything other than <code>"*"</code>, the leading and trailing "|" are optional.
      @since NeXtMidas 2.7.0
   */
  public void setTraces (String traces) {
    if (traces == null) traces = "*";

    if (!traces.equals("*")) {
      if (!traces.startsWith("|")) traces = "|" + traces;
      if (!traces.endsWith("|"))   traces = traces + "|";
    }

    if (!traces.equals(showColumns)) {
      showColumns = traces;
      showColsChanged = true;
      refresh();
    }
  }

  /** Gets the line traces to show.
      @return The list of columns displayed with multiple lines can be shown separated by a "|".
              If all columns are displayed, "*" is returned. The traces listing typically displays
              with leading and trailing "|" to match the syntax used with other mask values (this
              does NOT apply to <code>"*"</code>).
      @since NeXtMidas 2.7.0
   */
  public String getTraces () {
    return showColumns;
  }

  /** Sets the record names. This does some reformatting of the names to speed up show(int). */
  protected void setRecNames (String[] names) {
    if (names == null) names = new String[0];

    recNames = new String[names.length];
    for (int i = 0; i < names.length; i++) {
           if (names[i] == null)       recNames[i] = "_"+i+"_";
      else if (names[i].length() == 0) recNames[i] = "_"+i+"_";
      else                             recNames[i] = names[i];
    }
  }

  /** Should the given column be shown? */
  protected boolean show (int col) {
    if (showColumns.equals("*")) return true; // show all

    if (col < recNames.length) {
      if (showColumns.indexOf("|"+recNames[col]+"|") >= 0) return true;
    }
    return (showColumns.indexOf("|_"+col+"_|") >= 0);
  }

  /** Checks which sub-records (Y traces/columns) should be shown and allocate
      array buffer for them if they have not already been allocated. Also,
      null out columns that are no longer shown.
     @param fbuf buffer array of array to allocate float array to hold data to be plotted
     @param size number of elements (in file to allocate memory for. This is either
                 the total file size or the transfer length (for paged draws)
   */
  protected void updateRecData (double[][] buf, int size) {
    // This is identical to the float version, except for the use of "double"
    for (int col = 0; col < buf.length; col++) {
      if (!show(col)) {
        buf[col] = null; // clear out buf for records that are not selected to be shown
      } else if (buf[col] == null) {
        buf[col] = new double[size];
      } else if (buf[col].length != size) {
        // JC4: We might consider keeping the data when possible
        buf[col] = new double[size];
      }
    }
  }

  @Override
  public int getData (int mode) {
    int stat = Drawable.FULL;
    if (pf==null || !pf.isOpen() || rowBuf==null || isDone || getDrawFrame()) return stat;

    if (!lock()) return stat;
    try {
      frameReady = false;
      frameDrawn = false;

      if (rereadFile && !realtime) {
        pf.seek(0.0);
        ndata      = 0;
        rereadFile = false;
      }
      double avail = pf.avail();
      int ndo = (int)Math.min(avail, rowBuf.length-OVERLAP);

      fseek = pf.seek();

      if (ndo>0) {                 // process new data
        int count = 0;
        if ((fseek > 0.0) && (rowBuf[lastRow] != null) && !realtime) {
          rowBuf[count++].merge(rowBuf[lastRow]);
          fseek--;
        }
        for (int i = 0; i < ndo; i++) {
          if (rowBuf[count] == null) rowBuf[count] = new Table(Table.HT, 0);

          rowBuf[count].clear();
          pfnh.readDataTable(rowBuf[count]);
          count++;
        }
        lastRow = count-1; // to account for overlap with count++ in for loop above
        process(count);

        if (getNeedRefresh()) stat = Drawable.DATA;  // draw full data if a refresh is needed
        else if (isReset)     stat = Drawable.DATA;
        else if (isPaged)     stat = Drawable.DATAX;
        else                  stat = defmode;

        frameReady = true;
      }
      else if (isPaged) {  // stop page reads
        pf.seek(pf.getSize());
        setXStart(pf.getStart());
        ndata=0;
      }
      else if (!realtime) {
        stat=Drawable.FULL;
      }
      else if (pf.isStream() && !pf.isStreaming()) {
        stat = Drawable.DONE;
      }
    } finally {
      unlock();
    }
    return stat;
  }

  private volatile boolean lock = false;
  protected synchronized boolean lock () {
    if (lock) return false;
    lock = true;
    return lock;
  }
  protected synchronized void unlock () {
    lock = false;
  }

  /** process the plot data for specified number of frame elements */
  @Override
  public void process (int elem) {
    if (showColsChanged) {
      showColsChanged = false;
      updateRecData(recData, size+OVERLAP);
      if (!realtime) ndata = 0;
    }

    // For a pipe, we want to save any old values that will still fit in the buffer.
    if (realtime) {
      recIndex = table2da(rowBuf, recIndex, elem, recData);
      recSize  = Math.min(size + OVERLAP, recSize+elem);
      ndata    = recSize;
    }
    else {
      table2da(rowBuf, 0, elem, recData);
      recSize = ndata = elem;
    }
    frameReady = true;
    // if (debug) System.out.println("process() recIndex="+recIndex+" ndata="+ndata+" recFbuf[][]="+
    //   java.util.Arrays.deepToString(recFbuf) ); // requires: Java 5+
  }

  /** Convert rows array of Table (tabBuf) to an array of array (fbuf or dbuf).
      The first row in each buffer is always reserved for use in paging mode to
      store the last row from the previous transfer length.
      @param tabBuf input Table array of rows to convert into the float buffer
      @param start  start index in the buffer arrays (0 if not a pipe).
      @param nrows  number of rows to process
      @param buffer the array of array buffer where data is converted into
   */
  private int table2da (Table[] tabBuf, int start, int nrows, double[][] buffer) {
    // This is identical to table2fa(..) except for the use of "double" and the
    // "buf[num++] = dval;" line.
    boolean isFirst = (fseek == 0.0);
    int     end     = start;

    for (int col=0; col < buffer.length; col++) {
      double[] buf = buffer[col];
      if (buf != null) {
        int num = start;
        int len = buf.length;
        if (realtime) len -= OVERLAP; // LPB==LPS in real-time, so don't need overlap buffer slot
        for (int row=0; row<nrows; row++) {
          double val = convertData(tabBuf[row], recNames[col], MP.mode);
          buf[num++] = val;

          if (isFirst) {
            minY  =  maxY = val;
            isFirst = false;
          }

          if (val < minY) minY = val; // save min Y value
          if (val > maxY) maxY = val; // save max Y value

          if (num >= len) { num = 0; } // wrap to beginning of buffer when at end
        }
        end = num;
      }
    }
    return end;
  }

  /** Convert data according to mode. The logic for this mimics {@link Layer#data2fa(Data, int, float[], int, int)}.
      @param data Data object containing the scalar or complex data
      @param mode The mode (usually {@link MPlot#mode} )
      @param fval Initial value
      @return The value converted according to the mode if the mode is supported.
              If the Data object is null or the mode is not supported, returns the input float unconverted.
      @since NeXtMidas 3.1.1
   */
  private double convertData (Table tbl, String key, int mode) {
    Data   dat = tbl.getData(key);
    int    spa;
    double val0;

    if (dat == null) {
      val0 = tbl.getD(key);
      spa  = 1;
    }
    else {
      Number num0 = dat.getNumber(0);
      if (num0 == null) { val0 = tbl.getD(key);      spa = 1; }
      else              { val0 = num0.doubleValue(); spa = dat.spa; }
    }
    double scale = getScaleFactor(spa, mode);

    // Note that modes M_RNI and M_RVI are not supported yet, so they just hit
    // the "default" case and return val0. At one point we issued a warning in
    // this case, but ultimately commented it out since RNI is default for
    // some file types.
    if (spa==1) {
      switch (mode) {
        case MPlot.M_10LOG: return scale * Math.log(Math.max(10.e-11,Math.abs(val0)));
        case MPlot.M_20LOG: return scale * Math.log(Math.max(10.e-11,Math.abs(val0)));
        default:            return val0;
      }
    }
    if (spa==2) {
      double val1 = dat.getNumber(1).doubleValue();

      switch (mode) {
        case MPlot.M_MAG:   return Math.sqrt(val0*val0+val1*val1);
        case MPlot.M_PHASE: return scale * Math.atan2(val1,val0);
        case MPlot.M_10LOG: return scale * Math.log(Math.max(1.e-20,(val0*val0+val1*val1)));
        case MPlot.M_20LOG: return scale * Math.log(Math.max(1.e-20,(val0*val0+val1*val1)));
        case MPlot.M_REAL:  return val0;
        case MPlot.M_VX:    return val0;
        case MPlot.M_IMAG:  return val1;
        case MPlot.M_VY:    return val1;
        default:            return val0;
      }
    }
    if (spa==3) {
      double val1 = dat.getNumber(1).doubleValue();
      double val2 = dat.getNumber(2).doubleValue();

      switch (mode) {
        case MPlot.M_MAG:   return Math.sqrt(val0*val0+val1*val1+val2*val2);
        case MPlot.M_REAL:  return val0;
        case MPlot.M_VX:    return val0;
        case MPlot.M_IMAG:  return val1;
        case MPlot.M_VY:    return val1;
        case MPlot.M_VZ:    return val2;
        default:            return val0;
      }
    }
    return val0;
  }

  @Override
  public void findRange () {
    super.findRange();
    double xs = xstart;

// JC4: We may want this to plot from beginning till end...
//    if (realtime) {
//      xs = xstart + fseek * xdelta;
//    }

    if (xdelta < 0) {
      x1 = xs + (frame-1)*xdelta;
      x2 = xs;
    } else {
      x1 = xs;
      x2 = xs + (frame-1)*xdelta;
    }
    y1 = minY;
    y2 = maxY;

    if (x1 == x2) { x2 = x1 + xdelta;   } // Special case, only 0 or 1 rows in the file/TL
    if (y1 == y2) { y1-=0.5f; y2+=0.5f; } // Special case, flat-line plot

    // a1 and a2 are used to set the y-axis range (so set them to y1 and y2, respectively)
    a1 = y1;
    a2 = y2;
    // System.out.println("findRange() x1="+x1+" x2="+x2+" y1="+y1+" y2="+y2+" a1="+a1+" a2="+a2);
  }

  @Override
  public void draw (int flag) {
    if (!lock()) return;
    try {
      super.draw(flag);
      if (flag!=Drawable.FULL && !getDrawFrame()) return;

      Line   lin = line.copy();
      double xs;  // xstart to draw line-segments in buffer
      int npts = (ndata>size && realtime)? size: ndata; // adjust number of points for realtime mode

      if (realtime) {
        xs  = xstart;
        MP.setColor(MP.theme.cfg);
        if (MP.xvy) {
          double yd  = (maxY-minY)/size;
          double ys1 = ystart + yd*recIndex ;
          if (recIndex>0) ys1 = ys1 - yd;
          MP.drawHorizontal(maxY-ys1-minY);  // in xvy mode, draw horizontal scan line
        } else {
          double xs1 = xs + xdelta*recIndex; // x position at end index of line-segments draw buffer
          if (recIndex>0) xs1 -= xdelta;
          MP.drawVertical(xs1);   // draw vertical scan line at end of Line-segments in buffer
        }
      } else {
        if (fseek>=frame) {         // When the PIPE is PAUSED, we need to adjust xs and potentially npts
          if (isPaged && npts<=0) { // when in paging mode (only have last transfer length of data in buffer)
            npts = recSize;         // adjust npts and xs to use last number of valid elements in buffer
            xs   = xstart + xdelta*(fseek-npts);
          } else {
            xs = xstart;            // set xs to the start of the file (fseek is at end of the file)
          }
        } else {
          xs = xstart + xdelta*fseek; // for more data file, set xs based on current fseek in file
        }
      }

      // loop through each Y-Trace and draw it's line-segments
      for (int irec=0; irec < recData.length; irec++) {
        if (recData[irec] != null) {
          lin.setColor(colors[irec%(colors.length-1)+1]);
          MP.drawLine(recData[irec], 0, 1, npts, lin, xs, xdelta);
        }
      }
      frameDrawn = true;
    } finally {
      unlock();
    }
  }

  @Override
  public boolean reset () {
    rereadFile = true;
    if (!realtime) {
      minY = 0;
      maxY = 1;
    }
    return super.reset();
  }

  @Override
  public void refresh () {
    rereadFile = true;
    super.refresh();
  }

  /** Utility method to return a new Layer.
   *  @return new {@link LayerMulti}
   *  @since NeXtMidas 3.9.2
   */
  public static Layer getLayer() {
    return new LayerMulti();
  }
}
