/**
 * 
 * Copyright (C) 2015 UHU.ES/OSHW
 * 
 * This program 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.
 * 
 * This program 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/>.
 * 
 * 
 * See http://rxtx.qbang.org/wiki/index.php/Installation_for_Windows to install
 * DLL needed for appropriate function of Arduino.jar library in Windows
 *
 * Arduino.jar library was released by Antony Garcia Gonzalez (PanamaHitek)
 * under Creative Commons Attribution - Non Commertial 4.0 International License
 * (CC BY-NC 4.0)
 *
 * JChart2D.jar library was released by Achim Westermann under
 * GNU Lesser General Public License (GNU - LGPL)
 *
 *
 */
package ISEmeter;

import Arduino.Arduino;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;

import info.monitorenter.gui.chart.Chart2D;
import info.monitorenter.gui.chart.IAxis;
import info.monitorenter.gui.chart.IAxis.AxisTitle;
import info.monitorenter.gui.chart.ITrace2D;
import info.monitorenter.gui.chart.axis.scalepolicy.AxisScalePolicyManualTicks;
import info.monitorenter.gui.chart.rangepolicies.RangePolicyFixedViewport;
import info.monitorenter.gui.chart.traces.Trace2DSimple;
import info.monitorenter.util.Range;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DecimalFormat;

import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 *
 * @author JDaniel Mozo for the Open Source Hardware Group of the 
 * University of Huelva(UHU.ES/OSHW)
 * 
 */
public abstract class ISEmeter implements ChangeListener{
  static Chart2D chart;
  static ITrace2D trace;
  static ITrace2D traceNF;
  static Timer timer = new Timer(true);
  static TimerTask task1;
  static boolean copied = true;
  static Clipboard clipboard = (Clipboard) Toolkit.getDefaultToolkit().getSystemClipboard();
  static ArrayList<Double> dataX = new ArrayList<>();
  static ArrayList<Double> dataY = new ArrayList<>();
  static ArrayList<Double> dataF = new ArrayList<>();
  static DecimalFormat formatter = new DecimalFormat("#####0.00");
  static Arduino arduino = new Arduino();
  static boolean demo = false;
  static double m_starttime;
  static int index = 0;
  static int gain;
  static int flt;
  static JLabel lblTime = new JLabel("t (s): ");
  static JLabel lblPot = new JLabel("E (mV): ");
  static float LowXMax;
  static float HighXMax;
  static final int PointNr = 181;       // poits to be showed in chart
  static final int Interval = 333;      // time in msec between data acq

  @SuppressWarnings("static-access")

  public static void main(String[]args){
    // Create a chart:
    chart = new Chart2D();
    // Create an ITrace:
    // Note that dynamic charts need limited amount of values!!!
    trace = new Trace2DSimple();    //Ltd(PointNr);
    trace.setColor(Color.RED);
    traceNF = new Trace2DSimple();
    traceNF.setColor(Color.LIGHT_GRAY);
    // Add the trace to the chart. This has to be done before adding points (deadlock prevention):
    chart.addTrace(traceNF);
    chart.addTrace(trace);
    // Configure Chart:
    chart.setPaintLabels(false);
    chart.setUseAntialiasing(true);
    IAxis axisX = chart.getAxisX();
    IAxis axisY = chart.getAxisY();
    // Set titles
    axisX.setAxisTitle(new AxisTitle("Time (s)"));
    axisY.setAxisTitle(new AxisTitle("Potential (mV)"));

    // Make it visible:
    // Create a frame.
    JFrame frame = new JFrame("Open ESImeter");
    frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    // Place frame in the middle of desktop
    Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
    int ancho = dim.width/2;
    int alto = dim.height/2;
    frame.setBounds(ancho/2,alto/2,ancho,alto);

    // Sets personalized icon frame
    Image icon = Toolkit.getDefaultToolkit().getImage("resources/UHU-OSHW.jpg");
    frame.setIconImage(icon);

    // Create layout on frame to distribute panels
    frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(),BoxLayout.X_AXIS));
    // Create panels
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
    JPanel panelR = new JPanel();
    panelR.setLayout(new BoxLayout(panelR, BoxLayout.Y_AXIS));

    // Add panels left to right
    frame.getContentPane().add(panel);
    frame.getContentPane().add(panelR);

    // Enable the termination button [cross on the upper right edge]:
    frame.addWindowListener(
        new WindowAdapter(){
            @Override
            public void windowClosing(WindowEvent e){
                // check if data was copied before exit
                if (copied) {
                    System.exit(1);         // Terminates
                }else{
                    int n = JOptionPane.showConfirmDialog(frame,"Data was not copied, do you continue?","Exit confirmation",JOptionPane.YES_NO_OPTION);
                    // if answer is YES:
                    if (n == JOptionPane.YES_OPTION) {
                        copied = true;  // set data as copied and continue
                        System.exit(0);     // Abort exit
                    }
                }
            }
        }
    );
    frame.setVisible(true);

    // Configure control panel:
    panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
    // Create controls and add them to panel:
    JButton btnStart = new JButton("Start");
    btnStart.setPreferredSize(new Dimension(100,40));
    btnStart.setMaximumSize(new Dimension(150,40));
    btnStart.setAlignmentX(panel.LEFT_ALIGNMENT);

    JButton btnCopy = new JButton("Copy");
    btnCopy.setPreferredSize(new Dimension(100,20));
    btnCopy.setMaximumSize(new Dimension(150,20));
    btnCopy.setAlignmentX(panel.LEFT_ALIGNMENT);

    JLabel label1 = new JLabel("Range(FE) (mV):");
    label1.setMinimumSize(new Dimension(100,0));
    label1.setAlignmentX(panel.LEFT_ALIGNMENT);
    JComboBox cmbSens = new JComboBox();
    cmbSens.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "+1000", "+500", "+250","+100","+50" }));
    cmbSens.setPreferredSize(new Dimension(100,20));
    cmbSens.setMaximumSize(new Dimension(100,20));
    cmbSens.setAlignmentX(panel.LEFT_ALIGNMENT);

    JLabel label2 = new JLabel("Filter:");
    label2.setMinimumSize(new Dimension(100,0));
    label2.setAlignmentX(panel.LEFT_ALIGNMENT);
    JComboBox cmbFlt = new JComboBox();
    cmbFlt.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "NF", "D1", "D2","D3","D4","D5", "LPF" }));
    cmbFlt.setPreferredSize(new Dimension(100,20));
    cmbFlt.setMaximumSize(new Dimension(100,20));
    cmbFlt.setAlignmentX(panel.LEFT_ALIGNMENT);

    lblTime.setBorder(BorderFactory.createEtchedBorder());
    lblTime.setPreferredSize(new Dimension(100,20));
    lblTime.setMaximumSize(new Dimension(100,20));
    lblTime.setAlignmentX(panel.LEFT_ALIGNMENT);
    lblPot.setBorder(BorderFactory.createEtchedBorder());
    lblPot.setPreferredSize(new Dimension(100,20));
    lblPot.setMaximumSize(new Dimension(100,20));
    lblPot.setAlignmentX(panel.LEFT_ALIGNMENT);

    JLabel label3 = new JLabel("Comm Port:");
    label3.setMinimumSize(new Dimension(100,0));
    label3.setAlignmentX(panel.LEFT_ALIGNMENT);
    JComboBox cmbPortComm = new javax.swing.JComboBox();
    cmbPortComm.setModel(new javax.swing.DefaultComboBoxModel(new String[] {}));
    cmbPortComm.setPreferredSize(new Dimension(100,20));
    cmbPortComm.setMaximumSize(new Dimension(100,20));
    cmbPortComm.setAlignmentX(panel.LEFT_ALIGNMENT);
    arduino.ShowMessageDialogs(false);
    for (int i = 1; i <= (arduino.SerialPortsAvailable()); i++){
        // read available comm-port names and add to combo
        String PortName = arduino.NameSerialPortAt(i);
        // if there are 2 ports or plus NameSerialPortAt gives all ports concatenated
        // to isolate each port name:
        if (i > 1) {
            PortName = (String) PortName.subSequence(arduino.NameSerialPortAt(i-1).length()+1,arduino.NameSerialPortAt(i).length());
        }
        cmbPortComm.addItem(PortName);
    }
    // Add a demo mode comm-port
    cmbPortComm.addItem("DEMO");
    cmbPortComm.setSelectedItem("DEMO");

    // Set graphical scales
    demo = true;
    ScaleGraf (axisY, 0, 1000, true);
    ScaleGraf (axisX, 0, 60, false);

    // Load panel controls
    panel.add(btnStart);
    panel.add(Box.createRigidArea(new Dimension(0,5)));
    panel.add(btnCopy);
    panel.add(label1);
    panel.add(cmbSens);
    panel.add(label2);
    panel.add(cmbFlt);
    panel.add(Box.createRigidArea(new Dimension(0,5)));
    panel.add(lblTime);
    panel.add(lblPot);
    panel.add(Box.createVerticalGlue());
    panel.add(Box.createRigidArea(new Dimension(0,5)));
    panel.add(label3);
    panel.add(cmbPortComm);

    //Load panelR content (graph and slider)
    // add chart to panelR:
    panelR.add(chart);
    // create and set an horizontal slider
    LowXMax = 0;
    HighXMax = PointNr * Interval/1000;
    JSlider slider = new JSlider((int) LowXMax,(int) HighXMax,(int) HighXMax);

    ChangeListener ch = new ChangeListener() {
        // Define listener action for slider change value
        @Override
        public void stateChanged(ChangeEvent e) {
            // set axisX range from slider value
            if (slider.getValueIsAdjusting()){      // while slider knob drags
                int XMax = slider.getValue();
                double XMin = XMax - PointNr * Interval/1000;
                ScaleGraf(axisX, XMin, (double)XMax, false);
            }
            //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        }
    };

    slider.addChangeListener(ch);
    panelR.add(slider);
    slider.setEnabled(false);

    frame.setVisible(true);

    btnStart.addActionListener(new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            String Text = btnStart.getText();
            // check if data was copied before acquire
            if (!copied) {
                int n = JOptionPane.showConfirmDialog(frame,"Data was not copied, do you continue?","Warning",JOptionPane.YES_NO_OPTION);
                // if answer is YES:
                if (n == JOptionPane.YES_OPTION) {
                    copied = true;  // set data as copied and continue
                }
            }
            if ("Start".equals(Text) & copied){     // start acquisition
                btnStart.setText("Stop");
                // clear chart
                trace.removeAllPoints();
                traceNF.removeAllPoints();
                // clear data arraylist
                dataX.clear();
                dataY.clear();
                dataF.clear();
                //initialize rangepolicy for Xaxis
                ScaleGraf (axisX, 0, Interval * PointNr/1000, false);
                //disable slider
                slider.setEnabled(false);

                if (!demo) {
                    index = 0;
                    m_starttime = System.currentTimeMillis();

                    // Send start acquisition order to Serial comm
                    // (Data acquisition, store and graph is in SerialPortEventListener)
                    // (Pacer is Arduino, not Java)
                    try {
                        arduino.SendData("r");
                    } catch (Exception ex) {
                        Logger.getLogger(ISEmeter.class.getName()).log(Level.SEVERE, null, ex);
                    }
                } else {
                    // Now the dynamic adding of points. This is just a demo!
                    task1 = new TimerTask(){

                        private double m_y = 100;
                        private double m_starttime = System.currentTimeMillis();
                        private int index = 0;

                        @Override
                        public void run() {
                          // This is just computation of some nice looking value.
                          double rand = Math.random();
                          boolean add = (rand >= 0.5);
                          this.m_y = (add) ? this.m_y + Math.random()*10 : this.m_y - Math.random()*10;

                          // This is the important thing: Point is added from separate Thread.
                          dataX.add((double)(System.currentTimeMillis() - this.m_starttime)/1000);
                          dataY.add(this.m_y);
                          dataF.add(Promedia(index));

                          // Displays new data on labels and graph
                          traceNF.addPoint(dataX.get(index), dataY.get(index));
                          trace.addPoint(dataX.get(index), dataF.get(index));
                          lblTime.setText("t (s): " + formatter.format(dataX.get(index)));
                          lblPot.setText("E (mV): " + formatter.format(dataY.get(index)));

                          // check scale X and drag if necessary
                          if (dataX.get(index) > Interval * PointNr /1000 ) {
                              ScaleGraf (axisX, dataX.get(index) - PointNr * Interval/1000, dataX.get(index), false);
                          }

                          index++;
                        }
                    };

                    // Every "Interval" milliseconds a new value is collected.
                    timer.schedule(task1, 0, Interval);
                }
            } else {        //stop adquisition
                btnStart.setText("Start");
                copied = false;         // set flag not copied data

                //setup the slider and enable
                HighXMax = (float) axisX.getMax();
                LowXMax = (PointNr * Interval / 1000);
                if (HighXMax < LowXMax) {
                    LowXMax = 0;
                }
                slider.setMinimum ((int) LowXMax);
                slider.setMaximum ((int) HighXMax);
                slider.setValue ((int) HighXMax);
                slider.setEnabled(HighXMax > PointNr * Interval / 1000);

                // Halts the data acquisition
                if (!demo){
                    try {
                        // Send stop acquisition order to Serial comm
                        arduino.SendData("s");
                    } catch (Exception ex) {
                        Logger.getLogger(ISEmeter.class.getName()).log(Level.SEVERE, null, ex);
                    }
                } else { // if demo mode
                    // stop new value collection.
                    task1.cancel();         //demo mode
                }
            }
        }
    });

    btnCopy.addActionListener(new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            // copy data buffer to clipboard
            // build text string to be copied
            int i = 0;
            String textBuild = new String();
            String dataXString, dataYString;
            do {
                dataXString = formatter.format(dataX.get(i));
                dataYString = formatter.format(dataY.get(i));
                textBuild += dataXString + "\t" + dataYString + "\n";
                i++;
            } while (i < dataX.size());
            // put text string to clipboard
            StringSelection data2copy = new StringSelection(textBuild);
            clipboard.setContents(data2copy,data2copy);

            copied = true;      //set copied flag
        }
    });

    cmbSens.addActionListener (new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            // Send to Arduino the Gain setting and set graph

            // Set YGraphScale depending combo selection
            switch (cmbSens.getSelectedIndex()){
            case 0: // 1000 mV
                ScaleGraf (axisY, 0, 1000, true);
                break;
            case 1: // 500 mV
                ScaleGraf (axisY, 0, 500, true);
                break;
            case 2: // 250 mV
                ScaleGraf (axisY, 0, 250, true);
                break;
            case 3: // 100 mV
                ScaleGraf (axisY, 0, 100, true);
                break;
            case 4: // 50 mV
                ScaleGraf (axisY, 0, 50, true);
                break;
            }
        }
    });

    cmbFlt.addActionListener (new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {

            String msg = new String();
            if (cmbFlt.getSelectedIndex()== 0){
                flt = 0; msg = "f0";  // LPF deactivated
            } else if (cmbFlt.getSelectedIndex() == cmbFlt.getItemCount() - 1) {
                flt = -1; msg = "f1"; // LPF activated
            } else {
                // Set the number of points promediated (numerical filter)
                flt = 2 ^ (3 + cmbFlt.getSelectedIndex()); msg = "f0";
            }

            if (!demo) {
                // Set the analog channel used to read data
                try {
                    // Send filter setting to Serial comm
                    arduino.SendData(msg);
                } catch (Exception ex) {
                    Logger.getLogger(ISEmeter.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    });


    cmbPortComm.addActionListener (new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
          // Set DEMO flag
          demo = cmbPortComm.getSelectedItem().equals("DEMO");

          if (!demo) {
            // Initialize Serial Comm Port for Arduino
            try {
                arduino.ShowMessageDialogs(false);
                arduino.KillArduinoConnection();
                //arduino.ArduinoRXTX((String) cmbPortComm.getSelectedItem(),2000,9600,evento);
            } catch (Exception ex) {
                Logger.getLogger(ISEmeter.class.getName()).log(Level.SEVERE, null, ex);
            }
            try {
                arduino.ShowMessageDialogs(true);
                arduino.ArduinoRXTX((String) cmbPortComm.getSelectedItem(),2000,9600,evento);
            } catch (Exception ex) {
                Logger.getLogger(ISEmeter.class.getName()).log(Level.SEVERE, null, ex);
            }
          }
        }
    });

    chart.addMouseMotionListener(new MouseMotionListener () {

        @Override
        public void mouseMoved(MouseEvent e) {
            if (!trace.isEmpty()){
                //System.out.println("X: " + x + "\t" + axisX.getMin() + "\t" + axisX.getMax() + "\t" + axisX.translatePxToValue(x)  );
                double scaledX = axisX.translatePxToValue(e.getX());
                double scaledY = axisY.translatePxToValue(e.getY());

                lblTime.setText("t (s): " + formatter.format(scaledX));
                lblPot.setText("E (mV): " + formatter.format(scaledY));
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        }
    });

  }

  public static void ScaleGraf (IAxis axis,double sMin,double sMax, boolean g) {
    axis.setRangePolicy(new RangePolicyFixedViewport(new Range(sMin,sMax)));
    axis.setAxisScalePolicy(new AxisScalePolicyManualTicks());
    axis.setMinorTickSpacing((sMax-sMin)/10);

    // Calculate gain from selected FE
    if (g) {
        gain = 5000/(int) sMax;

        if (!demo){         // If not DEMO send gain to Arduino
          String msg = new String();
          switch ((int) sMax) {
              case 1000: {
                  msg = "g255";
                  break; }
              case 500: {
                  msg = "g112";
                  break; }
              case 250: {
                  msg = "g52";
                  break; }
              case 100: {
                  msg = "g20";
                  break; }
              case 50: {
                  msg = "g9";
                  break; }
          }
          try {
              // Send gain setting to Serial comm
              arduino.SendData(msg);
          } catch (Exception ex) {
              Logger.getLogger(ISEmeter.class.getName()).log(Level.SEVERE, null, ex);
          }
        }
    }
  }

  static SerialPortEventListener evento = new SerialPortEventListener(){

        @Override
        public void serialEvent(SerialPortEvent spe) {
            if (arduino.MessageAvailable() == true){
                String msg = arduino.PrintMessage();    // read data
                if (msg.startsWith("v") == true){       // If message is a data:
                    // stores time in X
                    dataX.add((double)(System.currentTimeMillis() - m_starttime)/1000);
                    
                    // extract data from msg
                    int comma = msg.indexOf(",");
                    // calculate voltage from data and gain
                    String dato = msg.substring(1, comma);
                    dataY.add(Double.parseDouble(dato)*5000/1024/gain); // crude data
                    if (flt == - 1 || flt == 0) {
                        // If LPF is activated read data from msg
                        dato = msg.substring(comma + 1);
                        dataF.add(Double.parseDouble(dato)*5000/1024/gain);
                    } else {
                        // In LPF isn't activated 
                        dataF.add(Promedia(index));            // filtered data (numerically)
                    }
                    
                    // Add point to trace
                    traceNF.addPoint(dataX.get(index), dataY.get(index));
                    trace.addPoint(dataX.get(index), dataF.get(index));
                    // Print point to labels
                    lblTime.setText("t (s): " + formatter.format(dataX.get(index)));
                    lblPot.setText("E (mV): " + formatter.format(dataF.get(index)));

                    // check scale X
                    if (dataX.get(index) > Interval * PointNr /1000 ) {
                        ScaleGraf (chart.getAxisX(), dataX.get(index) - PointNr * Interval/1000, dataX.get(index), false);
                    }

                    index++;
                } else {
                    // If message isn't a data:
                    System.out.println(msg);
                }
            }
        }
  };

  private static double Promedia(int i) {
    if (i < flt) {
        return dataY.get(i);
    } else {
        double fltData = 0;
        for (int j = i - flt; j < i; j++) {
             fltData += dataY.get(j);
        }
        return fltData / flt;
    }
  }

}
