/*
 * PanTiltGUI.java
 *
 * Created on April 21, 2008, 11:50 AM
 */
package ch.unizh.ini.jaer.projects.labyrinth;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.logging.Logger;

import net.sf.jaer.hardwareinterface.HardwareInterfaceException;
import ch.unizh.ini.jaer.hardware.pantilt.PanTiltAimer.Message;

/**
 * Tests ball controller by displaying a GUI that allows manual control of ball position under feedback control.
 * @author  tobi
 */
public class LabyrinthTableTiltControllerGUI extends javax.swing.JFrame implements PropertyChangeListener {

    private PropertyChangeSupport support = new PropertyChangeSupport(this);
    // property change messages
    public final String POSITION = "position";
    Logger log = Logger.getLogger("LabyrinthBallController");
    private LabyrinthBallControllerInterface controller;
    private int w = 200, h = 200, x0 = 0, y0 = 0;
    private Point2D.Float lastPanTilt = new Point2D.Float(0, 0);
    private Point lastMousePressLocation = new Point(w / 2, h / 2);
    private boolean recordingEnabled = false;
    private Trajectory trajectory = new Trajectory();
    private float panTiltLimit = 0.5f;
    Point2D.Float currentPanTiltRad = new Point2D.Float(0, 0);

    /** Make the GUI.
     * 
     * @param controller the pan tilt unit
     */
    public LabyrinthTableTiltControllerGUI(LabyrinthBallControllerInterface controller) {
        this.controller = controller;
        initComponents();
        calibrationPanel.setPreferredSize(new Dimension(w, h));
        calibrationPanel.requestFocusInWindow();
        controller.getSupport().addPropertyChangeListener(this);
        pack();
    }

    @Override
    public void paint(Graphics g) {
        final int r = 6;
        super.paint(g);

        // paint cross hairs to show center
        Graphics2D g2 = (Graphics2D) calibrationPanel.getGraphics();
        int w = calibrationPanel.getWidth(), h = calibrationPanel.getHeight();
        g2.setColor(Color.gray);
        g2.drawLine(w / 2, 0, w / 2, h);
        g2.drawLine(0, h / 2, w, h / 2);
        g2.setColor(Color.red) ;
        float x=w/2+w/2*currentPanTiltRad.x/controller.getTiltLimitRad();
        float y=h/2+h/2*(-currentPanTiltRad.y/controller.getTiltLimitRad());
        final int s=20;
        g2.drawOval((int)x-s/2, (int)y-s/2, s, s);

        trajectory.paint();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jButton1 = new javax.swing.JButton();
        statusLabel = new javax.swing.JLabel();
        calibrationPanel = new javax.swing.JPanel();
        jLabel5 = new javax.swing.JLabel();
        recordCB = new javax.swing.JCheckBox();
        clearBut = new javax.swing.JButton();
        loopTB = new javax.swing.JToggleButton();
        centerBut = new javax.swing.JButton();

        jButton1.setText("jButton1");

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("LabyrinthTableTiltControlller");
        setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosed(java.awt.event.WindowEvent evt) {
                formWindowClosed(evt);
            }
        });

        statusLabel.setText("status");
        statusLabel.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)));

        calibrationPanel.setBackground(new java.awt.Color(255, 255, 255));
        calibrationPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
        calibrationPanel.setToolTipText("Drag or click mouse to aim pan-tilt");
        calibrationPanel.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseEntered(java.awt.event.MouseEvent evt) {
                calibrationPanelMouseEntered(evt);
            }
            public void mouseExited(java.awt.event.MouseEvent evt) {
                calibrationPanelMouseExited(evt);
            }
            public void mousePressed(java.awt.event.MouseEvent evt) {
                calibrationPanelMousePressed(evt);
            }
            public void mouseReleased(java.awt.event.MouseEvent evt) {
                calibrationPanelMouseReleased(evt);
            }
        });
        calibrationPanel.addComponentListener(new java.awt.event.ComponentAdapter() {
            public void componentResized(java.awt.event.ComponentEvent evt) {
                calibrationPanelComponentResized(evt);
            }
        });
        calibrationPanel.addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
            public void mouseDragged(java.awt.event.MouseEvent evt) {
                calibrationPanelMouseDragged(evt);
            }
        });
        calibrationPanel.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyPressed(java.awt.event.KeyEvent evt) {
                calibrationPanelKeyPressed(evt);
            }
        });

        javax.swing.GroupLayout calibrationPanelLayout = new javax.swing.GroupLayout(calibrationPanel);
        calibrationPanel.setLayout(calibrationPanelLayout);
        calibrationPanelLayout.setHorizontalGroup(
            calibrationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 380, Short.MAX_VALUE)
        );
        calibrationPanelLayout.setVerticalGroup(
            calibrationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 283, Short.MAX_VALUE)
        );

        jLabel5.setText("<html>Drag or click  mouse to control table tilt.<br>Use <b>r</b> to toggle recording a tilt temporal trajectory.</html>");

        recordCB.setText("Record trajectory");
        recordCB.setEnabled(false);
        recordCB.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                recordCBActionPerformed(evt);
            }
        });

        clearBut.setText("Clear trajectory");
        clearBut.setEnabled(false);
        clearBut.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                clearButActionPerformed(evt);
            }
        });

        loopTB.setText("Loop trajectory");
        loopTB.setEnabled(false);
        loopTB.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                loopTBActionPerformed(evt);
            }
        });

        centerBut.setText("Center");
        centerBut.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                centerButActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGap(10, 10, 10)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(calibrationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addGroup(layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(centerBut)
                            .addComponent(jLabel5, javax.swing.GroupLayout.PREFERRED_SIZE, 195, javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(recordCB, javax.swing.GroupLayout.Alignment.TRAILING)
                            .addComponent(clearBut, javax.swing.GroupLayout.Alignment.TRAILING)
                            .addComponent(loopTB, javax.swing.GroupLayout.Alignment.TRAILING)))
                    .addComponent(statusLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 280, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                        .addGap(14, 14, 14)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(jLabel5, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addGap(50, 50, 50))
                            .addComponent(centerBut)))
                    .addGroup(layout.createSequentialGroup()
                        .addContainerGap()
                        .addComponent(recordCB)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(clearBut)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(loopTB)))
                .addGap(18, 18, 18)
                .addComponent(calibrationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addGap(18, 18, 18)
                .addComponent(statusLabel)
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    float scaleEvt(float x, float w) {
        float y = controller.getTiltLimitRad() * (2 * ((float) x / w - .5f));
        return y;
    }

    private float getPan(MouseEvent evt) {
        return scaleEvt(evt.getX(), calibrationPanel.getWidth());
    }

    private float getTilt(MouseEvent evt) {
        return scaleEvt(calibrationPanel.getHeight() - evt.getY(), calibrationPanel.getHeight());
    }

    private void setPanTilt(float pan, float tilt) {
        try {
            lastPanTilt.x = pan;
            lastPanTilt.y = tilt;
            statusLabel.setText(String.format("%.3f, %.3f", pan, tilt));
            controller.setTilts(pan, tilt);
        } catch (HardwareInterfaceException e) {
//            log.warning(e.toString());
        }
    }

    public Point getMouseFromPanTilt(Point2D.Float pt) {
        return new Point((int) (calibrationPanel.getWidth() * pt.x), (int) (calibrationPanel.getHeight() * pt.y));
    }

    public Point2D.Float getPanTiltFromMouse(Point mouse) {
        return new Point2D.Float((float) mouse.x / calibrationPanel.getWidth(), (float) mouse.y / calibrationPanel.getHeight());
    }

    private void calibrationPanelComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_calibrationPanelComponentResized
        w = calibrationPanel.getWidth();
        h = calibrationPanel.getHeight();
    }//GEN-LAST:event_calibrationPanelComponentResized

    private void calibrationPanelMouseDragged(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_calibrationPanelMouseDragged
        float pan = getPan(evt);
        float tilt = getTilt(evt);
        setPanTilt(pan, tilt);
        if (isRecordingEnabled()) {
            trajectory.add(pan, tilt, evt.getX(), evt.getY());
            repaint();
        }
    }//GEN-LAST:event_calibrationPanelMouseDragged

    private void calibrationPanelMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_calibrationPanelMousePressed
        lastMousePressLocation = evt.getPoint();
        controller.setControllerDisabledTemporarily(true);
        support.firePropertyChange(POSITION, lastMousePressLocation, evt.getPoint());
        lastMousePressLocation = evt.getPoint();
    }//GEN-LAST:event_calibrationPanelMousePressed

    private void calibrationPanelKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_calibrationPanelKeyPressed
        switch (evt.getKeyCode()) {
            case KeyEvent.VK_R:
                // send a message with pantilt and mouse filled in, tracker will fill in retina if there is a tracked locaton
                setRecordingEnabled(!isRecordingEnabled());
                repaint();
                break;
            case KeyEvent.VK_ESCAPE:
                support.firePropertyChange(Message.AbortRecording.name(), null, null);
                trajectory.clear();
                setRecordingEnabled(false);
                break;
            default:
                Toolkit.getDefaultToolkit().beep();
        }
    }//GEN-LAST:event_calibrationPanelKeyPressed

    private void calibrationPanelMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_calibrationPanelMouseReleased
        float pan = getPan(evt);
        float tilt = getTilt(evt);
        lastMousePressLocation = evt.getPoint();
        setPanTilt(pan, tilt);
      controller.setControllerDisabledTemporarily(false);

    }//GEN-LAST:event_calibrationPanelMouseReleased

    private void formWindowClosed(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosed
        controller.setControllerDisabledTemporarily(false);
    }//GEN-LAST:event_formWindowClosed

    private void calibrationPanelMouseEntered(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_calibrationPanelMouseEntered
        setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
//        calibrationPanel.requestFocus();
    }//GEN-LAST:event_calibrationPanelMouseEntered

    private void calibrationPanelMouseExited(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_calibrationPanelMouseExited
        setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
                controller.setControllerDisabledTemporarily(false);

    }//GEN-LAST:event_calibrationPanelMouseExited

    private void clearButActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_clearButActionPerformed
        support.firePropertyChange(Message.ClearRecording.name(), null, null);
        trajectory.clear();
        repaint();
    }//GEN-LAST:event_clearButActionPerformed

    private void recordCBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_recordCBActionPerformed
        setRecordingEnabled(recordCB.isSelected());
    }//GEN-LAST:event_recordCBActionPerformed

    private void loopTBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_loopTBActionPerformed
        trajectory.setPlaybackEnabled(loopTB.isSelected());

    }//GEN-LAST:event_loopTBActionPerformed

    private void centerButActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_centerButActionPerformed
        controller.centerTilts();
    }//GEN-LAST:event_centerButActionPerformed
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JPanel calibrationPanel;
    private javax.swing.JButton centerBut;
    private javax.swing.JButton clearBut;
    private javax.swing.JButton jButton1;
    private javax.swing.JLabel jLabel5;
    private javax.swing.JToggleButton loopTB;
    private javax.swing.JCheckBox recordCB;
    private javax.swing.JLabel statusLabel;
    // End of variables declaration//GEN-END:variables

    /** Property change events are fired to return events 
     * 
     * For sample messages "sample", the Point2D.Float object that is returned is the pan,tilt value for that point, i.e., the last 
     * pan,tilt value that has been set.
     * 
     * When samples have been chosen, "done" is passed.
     * 
     * @return the support. Add yourself as a listener to get notifications of new calibration points.
     */
    public PropertyChangeSupport getSupport() {
        return support;
    }

    /**
     * @return the recordingEnabled
     */
    private boolean isRecordingEnabled() {
        return recordingEnabled;
    }

    /**
     * @param recordingEnabled the recordingEnabled to set
     */
    private void setRecordingEnabled(boolean recordingEnabled) {
        boolean old = this.recordingEnabled;
        this.recordingEnabled = recordingEnabled;
        recordCB.setSelected(recordingEnabled);
        support.firePropertyChange(Message.SetRecordingEnabled.name(), old, recordingEnabled);
    }

    /**
     * @return the panTiltLimit
     */
    public float getPanTiltLimit() {
        return panTiltLimit;
    }

    /**
     * @param panTiltLimit the panTiltLimit to set
     */
    public void setPanTiltLimit(float panTiltLimit) {
        this.panTiltLimit = panTiltLimit;
    }

    class Trajectory extends ArrayList<TrajectoryPoint> {

        long lastTime;
        TrajectoryPlayer player = null;

        void add(float pan, float tilt, int x, int y) {
            if (isEmpty()) {
                start();
            }
            long now = System.currentTimeMillis();
            add(new TrajectoryPoint(now - lastTime, pan, tilt, x, y));
            lastTime = now;
        }

        void start() {
            lastTime = System.currentTimeMillis();
        }

        @Override
        public void clear() {
            if (player != null) {
                player.cancel();
            }
            super.clear();
        }

        private void setPlaybackEnabled(boolean selected) {
            if (selected) {
                if (player != null) {
                    player.cancel();
                }
                player = new TrajectoryPlayer();
                player.start();
            } else {
                if (player != null) {
                    player.cancel();
                }
            }
        }

        private void paint() {
            if (isEmpty()) {
                return;
            }
            int n = size();
            int[] x = new int[n], y = new int[n];
            for (int i = 0; i < n; i++) {
                x[i] = get(i).x;
                y[i] = get(i).y;
            }
            calibrationPanel.getGraphics().drawPolyline(x, y, n);
        }

        class TrajectoryPlayer extends Thread {

            boolean cancelMe = false;

            void cancel() {
                cancelMe = true;
                synchronized (this) {
                    interrupt();
                }
            }

            @Override
            public void run() {
                while (!cancelMe) {
                    for (TrajectoryPoint p : Trajectory.this) {
                        if (cancelMe) {
                            break;
                        }
                        setPanTilt(p.pan, p.tilt);
                        try {
                            Thread.sleep(p.timeMillis);
                        } catch (InterruptedException ex) {
                            break;
                        }
                    }
                }
            }
        }
    }

    class TrajectoryPoint {

        long timeMillis;
        float pan, tilt;
        int x, y;

        public TrajectoryPoint(long timeMillis, float pan, float tilt, int x, int y) {
            this.timeMillis = timeMillis;
            this.pan = pan;
            this.tilt = tilt;
            this.x = x;
            this.y = y;
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if(!isVisible()) return;
        if(evt.getSource() instanceof LabyrinthHardware){
            if(evt.getPropertyName()==LabyrinthHardware.PANTILT_CHANGE){
                currentPanTiltRad=(Point2D.Float)evt.getNewValue();
                repaint();
            }
        }
    }
    
    public void setPanTiltChange(){
        // void method to avoid complaints caused by property change on value change from FilterPanel
    }
}