package nxm.ucl.prim;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.HierarchyBoundsListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SpringLayout;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Command;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Message;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.libg.GPrimitive;
import nxm.sys.libg.MWindow;
import nxm.sys.libg.MidasBean;
import nxm.sys.libg.MidasDisplay;
import nxm.sys.prim.plot;
import nxm.ucl.lib.SpringUtilities;

/** Provides a way to display multiple time rastered plots
    of Type 1000 Midas files in a Scrollable Pane.

    @author  Lisa Tursky
    @author  Jay Hallam
*/
public class multiplot extends GPrimitive implements KeyListener, MouseListener {
  /** Vertical scroll to options (value={@value}). See {@link #setScrollEnd(String)}. */
  public static final String vscrollList = "Off,Top,Bottom";

  private static final int VSCROLL_OFF    = 1; // Vertical scroll turned off
  private static final int VSCROLL_TOP    = 2; // Vertical scroll to top
  private static final int VSCROLL_BOTTOM = 3; // Vertical scroll to bottom

  private static final boolean DEBUG_ALIGN  = false;    // Used when debugging alignment issues.

  private boolean isDone     = false;       // Is the command done?
  private boolean dbg        = DEBUG_ALIGN; // Turn debug messages on?
  private boolean warn       = true;        // Turn warnings on?
  private int     vscrollEnd = VSCROLL_OFF; // Turn automatic scrolling sticky (OFF|BOTTOM|TOP)

  private String       switches;   // Switches for PLOT command.
  private String       params;     // Parameters (other than file name) for PLOT command.
  private JScrollPane  pane;       // The scroll pane to place panel in.
  private JPanel       panel;      // The top-level panel.
  private GridLayout   layout;     // The layout used by panel.

  private double      xMin;       // Minimum X-axis value.
  private double      xMax;       // Maximum X-axis value.
  private double      yMin;       // Minimum Y-axis value.
  private double      yMax;       // Maximum Y-axis value.
  private double      zMin;       // Minimum Z-axis value.
  private double      zMax;       // Maximum Z-axis value.

  private int         panWidth;   // Panel width in pixels (for alignment, may not match display).
  private int         plotHeight; // Panel height in pixels (for alignment, may not match display).
  private int         maxWidth;   // Max allowable width for the data area

  // User Contributed EXPERIMENTAL MIMIC capability in 3.9.1 for NXM-3215
  private boolean mimicMode = false;
  private int mimicCount = 0;
  // User Contributed  EXPERIMENTAL ZMATCH capability in 3.9.1 for NXM-3215 (UNTESTED)
  private boolean zMatch = false;
  private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
  private java.util.List<MidasBean> beans = new ArrayList<>();
  // Adding EXPERIMENTAL STRIP CHART SUPPORTin 3.9.1 for NXM-3401
  private boolean stripChartMode; // layout as strip chart - added in 3.9.1 for NXM-3401

  private MidasDisplay[] displays;

  private String idPrefix    = "MULTIPLOT_PLOT_"; // The prefix for the individual PLOT ids.

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

  public int open() {
    layout   = new GridLayout(1, 1);
    panel    = new JPanel(layout);
    pane     = new JScrollPane(panel);
    dbg      = MA.getState("/DEBUG", dbg);
    warn     = MA.getState("/WARN", warn);
    xMin     = MA.getD("/XMIN",   0.0);
    xMax     = MA.getD("/XMAX",  10.0);
    mimicMode = MA.getZ("/MIMIC"); // original plots should mimic each other
    stripChartMode = MA.getZ("/STRIPCHART", false); // layout should be like strip chart
    zMatch   = MA.getZ("/ZMATCH",false);

    String title = MA.getS("/TITLE", "NeXtMidas Multiplot");

    MW = new MWindow(title, this);
    MW.open();
    MW.setTheme(MA.getO("/THEME"));

    int width  = MA.getL("WIDTH", 700);
    int height = MA.getL("HEIGHT", 500);
    maxWidth   = MA.getL("DATAWIDTH", 2500);
    if (width > maxWidth) width = maxWidth;

    MW.setSize(width,height);
    pane.setSize(width,height);

    int w = pane.getVerticalScrollBar().getWidth();
    int h = pane.getHorizontalScrollBar().getHeight();
    panel.setSize(MW.pos.w-w,MW.pos.h-h);
    panWidth   = MW.pos.w-w;
    plotHeight = 150;
    MW.addTo(this);

    panel.addKeyListener(this);
    panel.setFocusable(true);
    panel.addMouseListener(this);

    idPrefix = MA.getS("/IDPREFIX", idPrefix);

    info("my id "+this.id+" inner plots id prefix "+idPrefix);

    Args.SplitArg plotFiles = MA.getSplitArg(MA.getRaw("FILES"),'|');
    switches = MA.getS("SWITCHES","");
    switches = "/EXIT=NONE" + switches;
    params   = MA.getS("PARAMS","");
    displays = new MidasDisplay[plotFiles.getFields()];
    setScrollEnd(MA.getCS("/SCROLLEND","OFF"));

    // Create Midas Displays and add them to the panel.
    panel.addHierarchyBoundsListener(new HierarchyBoundsListener() {
      public void ancestorMoved(HierarchyEvent e)   { if (panel.isShowing()) { panel.validate(); } }
      public void ancestorResized(HierarchyEvent e) { if (panel.isShowing()) { panel.validate(); } }
    });
    pane.setVisible(true);
    MW.panel.add(pane);
    MW.setDisplay("FRONT");

    if (stripChartMode) scaleXMinMax(plotFiles);
    // MIMIC support for initial plots only
    mimicCount = plotFiles.getFields();
    /* The frame and the MidasDisplay panels must be visible before creating
       the MidasBean's. If you do not do this you will experience refresh
       problems in PLOT.4.8828125E-4
       Create the beans that will occupy the given Midas Displays. */
    for (int i = 0; i < plotFiles.getFields(); i++) {
      addPlotToPanel(plotFiles.getField(i), i, i==(plotFiles.getFields()-1));
    }
    resize();

    // ZMATCH suppot
    if (zMatch) {
      executorService.scheduleWithFixedDelay(this::checkZScales,  0,  100,  TimeUnit.MILLISECONDS);
    }

    return NORMAL;
  }

  public int process() {
    if (isDone) {
      return FINISH;
    } else {
      return NOOP;
    }
  }

  public int close() {
     return NORMAL;
  }

  public int processMessage(Message msg) {
    if        (msg.getName().equals("REFRESH")) {
      MW.refresh();
    } else if (msg.getName().equals("OPENFILE")) {
      addPlotToPanel(""+msg.data,-1,false);
      resize();
    } else if (msg.getName().equals("RESIZE")) {
      resize();
    } else if (msg.getName().equals("VSCROLL")) {
      scrollVertical(""+msg.data);
    } else if (msg.name.equals("POP")) {
        MW.pop(msg.info);
    } else if (msg.getName().equals("WINDOW")) {
      if (msg.data.toString().equals("CLOSING")) {
        msg.name = "EXIT";
        MQ.put(msg);
      }
    }
    if (msg.getName().equalsIgnoreCase("EXIT")) {
      isDone=true;
      return FINISH;
    }

    return NORMAL;
  }

  /** Get the horizontal alignment information for a given file.
      @param filename The name of the file.
      @return Returns the start and width pixel values (in an array {start,width}). These do not
              take in to account the size of the plot axis.
   */
  private int[] getHorizAlign (String filename) {
    final int MIN_WIDTH = 20; // Minimum width allowed.

    if ("".contentEquals(filename) || "\"\"".equals(filename)) {
      warn("multiplot called with empty filename.");
      return new int[] {0 , MIN_WIDTH};
    }

    DataFile hcb = new DataFile(M, filename,"", "", 0);
    hcb.open(BaseFile.INPUT);
    double xstart = hcb.getXStart();
    double xend   = xstart + (hcb.getXDelta() * hcb.getSize());
    double range  = (xMax - xMin);
    double mult   = (panWidth / range);
    hcb.close();

    int start = (int)((xstart - xMin) * mult);
    int end   = (int)((xend   - xMin) * mult);
    int width = end - start;

    debug("getHorizAlign("+filename+") xstart="+xstart+" xend="+xend+" range="+range+" mult="+mult);
    debug("  start="+start+" end="+end+" width="+width+" panWidth="+panWidth);
    if (start < 0) {
      warn("Can not properly align file "+filename+" with XS="+xstart+" when /XMIN="+xMin);
      start = 0;
    }
    if (width <= MIN_WIDTH) {
      warn("Can not properly display file "+filename+" with a range of "+range+" using minimum "
         + "allowable.");
      width = MIN_WIDTH;
    }

    return new int[] { start, width };
  }

  /** Get the number of plots that are in the panel. */
  private int getNumPlots() {
    return (panel==null)? 0: panel.getComponents().length;
  }

  /** Adds a given plot to the panel.
      @param filename The name of the plot.
      @param plotIdx  index of plot (-1 means beyond original group)
      @param bNumbers in strip chart mode, have axis labels along bottom of this plot
   */
  private void addPlotToPanel(String filename, int plotIdx, boolean bNumbers) {
    final int LAXIS  = 16; // Width of left axis in pixels.
    final int RAXIS  = 22; // Width of right axis in pixels.

    // DETERMINE PLOT POSITION -------------------------------------------
    int[] align = {0,panWidth};  // Value used if in strip chart mode - since 3.9.1 for NXM-3401
    if (!stripChartMode) align = getHorizAlign(filename); // = { start, width }
    int dispWidth = align[1] + LAXIS + RAXIS;
    int leftPad   = align[0] - LAXIS;
    int rightPad  = panWidth - leftPad - dispWidth;
    if (mimicMode) {
      leftPad  = 0;
      rightPad = 0;
    }

    // UPDATE GRID COUNTS ------------------------------------------------
    layout.setRows(getNumPlots() + 1); // Increase number of rows by one.

    // ADD PLOT TO DISPLAY -----------------------------------------------
    JPanel       subp = new JPanel(new SpringLayout()); // Subset panel for alignment.
    MidasDisplay disp = new MidasDisplay();             // The plot display.
    JPanel       padL = new JPanel();                   // Pad/spacer on left side of plot.
    JPanel       padR = new JPanel();                   // Pad/spacer on right side of plot.

    padL.setPreferredSize(new Dimension(Math.max(1, leftPad),  plotHeight));
    padR.setPreferredSize(new Dimension(Math.max(1, rightPad), plotHeight));
    disp.setPreferredSize(new Dimension(Math.max(1, dispWidth),plotHeight));

    if (DEBUG_ALIGN) {
      padL.setBackground(Color.red);
      padR.setBackground(Color.blue);
    }

    if (!mimicMode) subp.add(padL);
    subp.add(disp);
    subp.add(padR);
    if (!mimicMode) {
      SpringUtilities.makeCompactGrid(subp, 1, 3, 0, 0, 0, 0);
    }
    else {
      SpringUtilities.makeCompactGrid(subp, 1, 2, 0, 0, 5, 5);
    }

    panel.setSize(MW.pos.w, plotHeight*getNumPlots());
    panel.add(subp);
    panel.validate();


    // RUN THE PLOT COMMAND ----------------------------------------------
    String    idPlot   = idPrefix + (getNumPlots() - 1);

    // Add the MIMIC and STRIP CHART options in 3.9.1
    String    command  = "plot" + switches + " " + filename;
    if (!stripChartMode) {
      command += " "+ params;
    }
    else {
      command += buildStripChartString(plotIdx, bNumbers) + " ";
    }
    if (mimicMode) command += buildMimicString(plotIdx);

    MidasBean plotBean = MidasBean.createBean(M, command, disp, idPlot);
    if (mimicMode) beans.add(plotBean);
    scrollToVerticalSticky();
  }

  /** Resize the scrollpane. */
  private void resize() {
    if (panel == null || pane == null) return;
    pane.setSize(MW.pos.w, MW.pos.h);
    MW.resize(0);
    pane.validate();
    if (panel.getWidth() > maxWidth) {
      panel.setPreferredSize(new Dimension(maxWidth, panel.getHeight()));
    }
    scrollToVerticalSticky();
    panel.repaint();
  }

  /** Get the vertical scroll end sticky mode for the multiplot scroll pane.
      @return return vertical scroll end sticky setting for the multiplot
   */
  public String getScrollEnd() {
    return Parser.get(vscrollList, vscrollEnd);
  }

  /** Set the vertical scroll end sticky mode for the multiplot scroll pane and then scroll to
      specified setting.
      @param scrollSticky vertical scroll sticky setting see {@link #vscrollList}.
   */
  private void setScrollEnd(int scrollSticky) {
    vscrollEnd = scrollSticky;
    scrollToVerticalSticky();
  }

  /** Set the vertical scroll end sticky mode for the multiplot scroll pane and then scroll to
      specified setting.
      @param scrollSticky vertical scroll sticky setting see {@link #vscrollList}.
   */
  public void setScrollEnd (String scrollSticky) {
    setScrollEnd(Parser.find(vscrollList,scrollSticky,vscrollEnd));
  }

  /** Vertical scroll to top, bottom, or none based on the current scrollEnd setting. */
  private void scrollToVerticalSticky() {
    scrollVertical(vscrollEnd);
  }

  /** Scroll to the specified vertical position in the scroll panel.
      @param vpos vertical scroll sticky position setting see {@link #vscrollList}.
   */
  private void scrollVertical(String vpos) {
    scrollVertical(Parser.find(vscrollList,vpos,vscrollEnd));
  }

  /** Scroll to the specified vertical position in the scroll panel.
      @param vpos vertical scroll sticky position setting see {@link #vscrollList}.
   */
  private void scrollVertical(int vpos) {
    if (pane.getComponentCount() < 1) {
      return; // cannot scroll pane when there are no child components, so return
    }

    int x = pane.getHorizontalScrollBar().getValue(); // save current x (horizontal) scroll position
    int h = panel.getHeight(); // get panel's height so that we scroll to bottom of the panel

    switch (vpos) {
      case VSCROLL_OFF:    /* Do Nothing */              break;
      case VSCROLL_TOP:
        pane.getVerticalScrollBar().setValue(0);
        pane.getHorizontalScrollBar().setValue(x);
        break;
      case VSCROLL_BOTTOM:
        pane.getVerticalScrollBar().setValue(h);
        pane.getHorizontalScrollBar().setValue(x);
        break;
      default: throw new MidasException("MULTIPLOT: Unknown vertical scroll position vpos="+vpos);
    }
  }

  // ****************** MIMIC SUPPORT ***** 3.9.1 ************

  private String buildMimicString (int plotId) {
    StringBuilder sb = new StringBuilder();
    sb.append(" MIMIC=");
    for (int i = 0; i < mimicCount; i++) {
      if (i == plotId) continue; // Should not mimic ourselves - skip this
      sb.append(idPrefix + i + '|');
    }
    sb.deleteCharAt(sb.length()-1); // remove trailing |
    return sb.toString();
  }

  // ****************** STRIP CHART SUPPORT ***** 3.9.1 ************

  private String buildStripChartString (int plotId, boolean bNumbers) {
    String cmdAdd = " X1="+xMin+" X2="+ xMax + " AXIS=FRAME|LLABELS|SHOW|LNUMBERS";
    if (plotId == 0) {
      cmdAdd += "|TLABEL Readout=+onAxis";
    }
    else if (bNumbers) {
      cmdAdd += "|BNUMBERS Readout=+onAxis";
    }
    else {
      cmdAdd += " Readout=+onAxis";
    }
    return cmdAdd;
  }

  /** Scale the xMin and xMax based on the full range of the files */
  private void scaleXMinMax (Args.SplitArg plotFiles) {
    xMin = Integer.MAX_VALUE;
    xMax = Integer.MIN_VALUE;

    for (int i = 0; i < plotFiles.getFields(); i++) {
      String filename = plotFiles.getField(i);
      DataFile hcb = new DataFile(M, filename,"", "", 0);
      hcb.open(BaseFile.INPUT);
      double xstart = hcb.getXStart();
      xMin = Math.min(xMin,xstart);
      xMax = Math.max(xMax,xstart + (hcb.getXDelta() * hcb.getSize()));
      hcb.close();
    }
  }

  // ****************** ZMATCH SUPPORT ***** 3.9.1 ***********
  /** Step executor service only if the plot is ready to adjust
   *  @since NeXtMidas 3.9.1
   */
  @InternalUseOnly
  public void checkZScales() {
    if (resetZScale(beans, "Z")) {
      executorService.shutdownNow();
    }
  }

  private boolean resetZScale(java.util.List<MidasBean> plots, String axis) {
    int i = 0;
    for (MidasBean mb : plots) {
      Command cmd = mb.getCommand();
      if (cmd instanceof plot) {
        plot currPlot = (plot)cmd;
        int val = "Z".compareToIgnoreCase(axis) == 0 ? (int)currPlot.MP.rz2 : (int)currPlot.MP.ry2;
        if (val == 1) {
          // this plot is not ready to adjust yet
          return false;
        }
        if (i==0) {
          zMin = currPlot.MP.orz1;
          zMax = currPlot.MP.orz2;
          yMin = currPlot.MP.ory1;
          yMax = currPlot.MP.ory2;
        }
        else {
          currPlot.MP.rz1 = zMin;
          currPlot.MP.rz2 = zMax;
          currPlot.MP.ry1 = yMin;
          currPlot.MP.ry2 = yMin;
          currPlot.MP.setZBounds(zMin, zMax);
          currPlot.MP.setYBounds(yMin, yMax);
          currPlot.MP.refresh();
        }
      }
      i++;
    }
    return true;
  }

  //********************************************************

  private void debug (String str) {
    if (dbg) {
      System.out.println("MULTIPLOT: "+str);
    }
  }
  private void info (String str) {
    if (verbose) {
      M.info("MULTIPLOT: "+str);
    }
  }
  private void warn (String str) {
    if (warn) {
      M.warning("MULTIPLOT: "+str);
    }
  }

  public void keyPressed(KeyEvent e) {
    this.MW.keyPressed(e);
  }

  public void keyReleased(KeyEvent e) {
    this.MW.keyReleased(e);
  }

  public void keyTyped(KeyEvent e) {
    this.MW.keyTyped(e);
  }

  public void mouseClicked(MouseEvent e) {
    panel.requestFocus();
    MW.mouseClicked(e);
  }

  public void mouseEntered(MouseEvent e) {
    MW.mouseEntered(e);
  }

  public void mouseExited(MouseEvent e) {
    MW.mouseExited(e);
  }

  public void mousePressed(MouseEvent e) {
    MW.mousePressed(e);
  }

  public void mouseReleased(MouseEvent e) {
    MW.mouseReleased(e);
  }

}