package nxm.ucl.prim;

import java.util.Arrays;
import java.util.Vector;

import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Time;
import nxm.ucl.inc.PipeCutsIF;

/** This primitive take an input pipe and split its data into multiple files and
    permits the playback of previously-saved data.

  <pre>
    <u><b>Typical Execution Cycle</b></u>

                   START
                     |
                     V
               +-----------+
               | open()    |
               +-----------+
                     |
           +------+  |
           |      |  |
           |      V  V
           |   +-----------+<i>      +-----------------------+</i>
           |   |           |<i>-----&gt;| processPipeSplitter() |----+</i>
           |   | process() |<i>      +-----------------------+    |</i>
           |   |           |<i>                                   |</i>
           |   |           |<i>      +-----------------------+    |</i>
           |   |           |<i>&lt;-----| processPipeJoiner()   |&lt;---+</i>
           |   +-----------+<i>      +-----------------------+</i>
           |         |
           | [!done] |
           +---------+
                     | [done]
                     V
               +-----------+
               | close()   |
               +-----------+
                     |
                     V
                  FINISH
  </pre>

  @author J. Czechowski IV / J. Hallam
*/
public class pipecuts extends Primitive implements PipeCutsIF {
  /** The allowable data type formats that can be used. <br> Value: <code>{@value}</code> */
  private static final String DATA_FORMATS = "SB,SI,SL,SF,SD,CB,CI,CL,CF,CD";

  /** The allowable data file types that can be used. <br> Value: <code>{@value}</code> */
  private static final String FILE_TYPES = "1000,2000";

  /** The minimum number of elements per cut file, prevents 0-length files. */
  private static final int MIN_ELE_COUNT = 100;

  /** The transfer length.      */ private int      xferLength;
  /** The current cut list.     */ private Vector<CutFileInfo> cutList = new Vector<>();
  /** The file name prefix.     */ private String   cutPrefix;
  /** This is the input pipe.   */ private DataFile inputPipe;
  /** This is the output pipe.  */ private DataFile outputPipe;
  /** The current output cut.   */ private DataFile currentCut;
  /** The current input cut.    */ private DataFile inputCutFile;
  /** The input buffer.         */ private Data     inData;
  /** The output buffer.        */ private Data     outData;
  /** The cut duration (MJS).   */ private double   duration     = 60;
  /** The next cut end time.    */ private double   cutFileEnd;
  /** The written element count.*/ private int      eleCount;
//  /** Read file delay.          */ private double   tdelta;
//  /** Read file start time.     */ private double   tstart;
  /** Last read time (file).    */ private double   fTime;
  /** Last read time (clock).   */ private double   cTime;

  /** Indicates if in "single file playback" mode. */
  private boolean singleFileMode = false;

  /** The output stats of the data. Should be {@link #PROCESS}, {@link #DONE}, or {@link #SUSPEND}. */
  private int outputState = PROCESS;

  /** default constructor */
  public pipecuts () {}

  public int open () {
    // Determine if running in playback-only mode.
    final String playback = MA.getCS("/JOIN", null);

    // Initialize the cut file list.
    if(playback == null) {
      playFromFile(null);
    } else {
      setCutsList(playback.split(","));
      playFromIndex(0);
    }

    // The input file/pipe
    //   If joining files and IN is the empty string then don't input anything.
    if((playback == null) || (MA.getS("IN").length() > 0)) {
      inputPipe = MA.getDataFile("IN", FILE_TYPES, DATA_FORMATS, 0);
      inputPipe.open();
    } else {
      inputPipe = null;
    }

    // The output file/pipe
    //    If inputPipe is null then just join the "playback files".
    //    If OUT is the empty string then don't output anything.
    if(inputPipe == null) {                                   /* Join Only    */
      outputPipe = MA.getDataFile("OUT", inputCutFile, 0);
      outputPipe.open();

    } else if(MA.getS("OUT").length() > 0) {                  /* Split & Join */
      outputPipe = MA.getDataFile("OUT", inputPipe, 0);
      outputPipe.open();

    } else {                                                  /* Split Only   */
      outputPipe = null;
    }

    setPrefix(MA.getCS("PRE","pipecut"));          // The cut files' prefix
    setReadRate(MA.getD("/RATE", -1));             // Set the output rate
    setCutFileInterval(MA.getD("/INT", duration)); // Set the cut file interval
    xferLength = Math.max(1, MA.getL("/TL",xfer)); // Set the transfer length

    return NORMAL;
  }

  public synchronized int process () {
    int status = NOOP;

    status = processPipeSplitter(status);
    status = processPipeJoiner(status);

    if(verbose && (status == FINISH)) {
      System.out.println("FINISHED");
      System.out.println("Cut File List = {"
                       + StringUtil.join(cutList.toArray(), ",")
                       + "}");
    }

    return status;
  }

  public int close () {
    if(inputPipe    != null) { inputPipe.close();    }
    if(outputPipe   != null) { outputPipe.close();   }
    if(inputCutFile != null) { inputCutFile.close(); }
    if(currentCut   != null) { currentCut.close();   }

    return NORMAL;
  }

  /** This will process the pipe splitter portion of the work.
      @param status The current status.
      @return NORMAL, FINISH, or NOOP
   */
  public synchronized int processPipeSplitter (int status) {
    if(inputPipe == null) {
      status = FINISH;

    } else if(inputPipe.avail() > 0) {
      final double time = inputPipe.getTimeAt();

      if((currentCut == null)
         || ((eleCount >= MIN_ELE_COUNT) && (time >= cutFileEnd))) {
        startNextCutFile(time);
      }

      final int numEle = inputPipe.read(inData, 1);

      if(numEle > 0) {
        eleCount += numEle;

        /* Write to output file */
        currentCut.setTimeAt(time);       /* Time Stamp */
        currentCut.write(inData, numEle); /* Data */

        /* Write to output pipe */
        if((inputCutFile == null) && (outputPipe != null)
                                  && (outputState == PROCESS)) {
          outputPipe.write(inData, numEle);
        }

        status = NORMAL;
      }
    } else if(!inputPipe.isStream()) {
      // A file (non-pipe/non-stream) will always have elements available until the end of file is reached.
      closeCutFile();
      status = FINISH;

    } else {
      status = NOOP;
    }

    return status;
  }

  /** This will process the pipe splitter portion of the work.
      @param status The current status.
      @return NORMAL, FINISH, or NOOP
   */
  public synchronized int processPipeJoiner (int status) {
    if(outputState == SUSPEND) {
      /* Output is paused - Wait for resume. */
      status = (status == NORMAL)? NORMAL: NOOP;

    } else if(outputState == DONE) {
      /* Output is stopped - Do Nothing */

    } else if((inputCutFile != null) && (outputPipe != null)) {
//      final double time  = inputCutFile.getTimeAt();
//      final double timer = time + Time.current();

//      if(tstart > timer) {
      if((cTime > 0)
	     && ((Time.current() - cTime) < (inputCutFile.getTimeAt() - fTime))) {
        status = (status == NORMAL)? NORMAL: NOOP;

      } else {
//        tstart = timer + tdelta;
	    fTime = inputCutFile.getTimeAt(); /* "File"  time */
        cTime = Time.current();           /* "Clock" time */

//        int numEle = inputCutFile.read(outData,1);
        int numEle = inputCutFile.read(outData,1024);

        if(numEle > 0) {
          outputPipe.write(outData, numEle);
          status = NORMAL;

        } else if(numEle == 0) {
          status = (status == NORMAL)? NORMAL: NOOP;

        } else if(singleFileMode) {
          // done
          outputState = DONE;

        } else {
          // Find the index of the current cut file.
          int cutIndex;

          final String fileName = inputCutFile.getFileName().toString();
          final String fileURL  = inputCutFile.getURL();

          for (cutIndex = 0; cutIndex < cutList.size(); cutIndex++) {
            final CutFileInfo file = cutList.get(cutIndex);

            if (fileName.equals(file.getName()) || fileURL.equals(file.getName())) {
              break;
            }
          }

          // Open the next file.
          playFromIndex(cutIndex + 1);
          status = NORMAL;
        }
      }
    }

    return status;
  }

  //////////////////////////////////////////////////////////////////////////////
  // GENERAL INTERFACE METHODS
  //////////////////////////////////////////////////////////////////////////////
  public void clearCuts() { cutList.clear(); }

  public String getActiveCut ()  { return currentCut.getURL(); }
  public String getSourcePipe () { return inputPipe.getURL(); }

  public double getCutFileInterval () { return duration;  }
  public String getPrefix ()          { return cutPrefix; }

  public CutFileInfo[] getCutsList () {
    return cutList.toArray(new CutFileInfo[0]);
  }

  public void setCutsList (String[] files) {
    cutList.clear();

    for (String file : files) {
      final DataFile df = new DataFile();

      df.init(M, new FileName(file));
      df.open(DataFile.INPUT);

      cutList.add(new CutFile(df));

      df.close();
    }
  }

  public void setCutsList (CutFileInfo[] files) {
    cutList.clear();
    cutList.addAll(Arrays.asList(files));
  }

  public void setCutFileInterval (double duration) {
    if (duration <= 0) {
      M.error("Invalid duration (" + duration + ")");
    } else {
      this.duration = duration;
    }
  }

  public void setPrefix (String prefix) {
    cutPrefix = prefix;
  }

  public void setSourcePipe (String name) {
    inputPipe = (DataFile)DataFile.getInstanceFor(this, name);
  }

  public void setReadRate (double value) {
    System.err.println("pipecuts.setReadRate(double) is not implemented.");
  }

  //////////////////////////////////////////////////////////////////////////////
  // PLAYBACK CONTROL
  //////////////////////////////////////////////////////////////////////////////
  public synchronized void pause ()  {outputState=SUSPEND;}
  public synchronized void resume () {outputState=PROCESS;}

  public synchronized void stop () {
    if(inputCutFile != null) { inputCutFile.close(); inputCutFile = null; }

    outputState = DONE;
  }

  public void playSingleFile (String filename) { playFile(filename, true);  }
  public void playFromFile (String filename)   { playFile(filename, false); }

  public void playFromIndex (int index) {
    if((index >= 0) && (index < cutList.size())) {
      playFromFile(cutList.get(index).getName());

    } else {
      playFromFile(null);
    }
  }

  public void playFromTime (double time) {
    if (time < 0) {
      playFromFile(null);
    } else {
      for (CutFileInfo file : cutList) {
        if (file.getEndTime() > time) {
          playFromFile(file.getName());
          return;
        }
      }

      playFromFile(null);
    }
  }

  /** Plays a file.
      @param filename     The name of the file.
      @param isSingleFile Indicates if in "play single file only" mode.
   */
  private synchronized void playFile (String filename, boolean isSingleFile) {
    outputState    = PROCESS;
    singleFileMode = isSingleFile;

    if (filename != null) {
      final DataFile df = new DataFile();

      df.init(M, new FileName(filename));
      df.open(DataFile.INPUT);

//      tstart  = 0;
//      tdelta  = df.getDelta();
      fTime = 0.0;
      cTime = -1.0;
      outData = df.getDataBuffer(xferLength);

      inputCutFile = df;
System.out.println("reading file '" + inputCutFile.getURL() + "'");
    } else {
      inputCutFile = null;
System.out.println("reading from input pipe");
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  // MISC. METHODS
  //////////////////////////////////////////////////////////////////////////////
  /** Open the next cut file for writing. */
  private void startNextCutFile (final double time) {
    closeCutFile(); /* Close the current cut file first. */

    final String filename = getOutputFileName(time);

    cutFileEnd = time + duration;
    eleCount   = 0;

    currentCut = new DataFile(M, filename, inputPipe, 0);
    currentCut.open(DataFile.OUTPUT);
    currentCut.setXStart(time);  //wgd // Add the START time to the header
    inData = currentCut.getDataBuffer(xferLength);

    System.out.println("writing cut file '" + currentCut.getURL() + "'");
    MQ.put("NEWCUTFILE", 0, filename, "MAIN"); //wgd // Also send the macro a message of new file...
  }

  /** Closes the current cut file. */
  private void closeCutFile () {
    if(currentCut != null) {
      final double end   = currentCut.getTimeAt(currentCut.seek());
      final double start = currentCut.getTimeAt(0);

      cutList.add(new CutFile(currentCut, start, end));
      currentCut.close();
    }
  }

  /** Get the current output file name. */
  private String getOutputFileName (double timeMJS) {
    return cutPrefix + "_" + ((long)timeMJS)
                          + "_" + ((long)getCutFileInterval());
  }

  /** The implementation of PipeCutsIF.CutFileInfo to use. */
  private class CutFile implements CutFileInfo {
    /** The cut file's name.       */ private String name;
    /** The cut file's start time. */ private double start;
    /** The cut file's end time.   */ private double end;

    /** Creates a new instance.
        @param file The file from which to determine the name, start and end parameters.
     */
    public CutFile (final DataFile file) {
      this(file,
           file.getTimeAt(0.0),
           file.getTimeAt(file.size - 1.0));
    }

    /** Creates a new instance.
        @param file  The file from which to determine the name.
        @param start The cut file's start time.
        @param end   The cut file's end time.
     */
    public CutFile (final DataFile file, final double start, final double end) {
      this(file.getFileName().getRoot(), start, end);
    }

    /** Creates a new instance.
        @param name  The cut file's name.
        @param start The cut file's start time.
        @param end   The cut file's end time.
     */
    public CutFile (final String name, final double start, final double end) {
      this.name  = name;
      this.start = start;
      this.end   = end;
    }

    public double getEndTime()   { return end;   }
    public String getName()      { return name;  }
    public double getStartTime() { return start; }

    /** Gives the name of the cut file.
        @return The name of the cut file.
     */
    public String toString() { return getName(); }
  }
}
