package com.knutejohnson.pi.guigpio;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.lang.reflect.*;
import java.nio.charset.*;
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import javax.swing.*;

/**
 *  GUIGPIO
 *
 *  Java GUI program to control GPIO pins on a Raspberry Pi.  Requires pigpiod
 *  to be running.  Uses the pigpio pipe interface.
 *
 *  @author     Knute Johnson
 *  @version    0.24.2 - 14 November 2020
 */
public class GUIGPIO extends JFrame implements Runnable {
    /** Serial version UID */
    private static final long serialVersionUID = 1L;

    /** Program Version */
    public static final String VERSION = "0.24.1";

    /** Program Date */
    public static final String DATE = "17 September 2020";

    /** Program Author */
    public static final String AUTHOR = "Knute Johnson";

    /** Number of panels */
    private static final int PANELS = 8;

    /** GPIO pin modes */
    private enum MODE {
     /** Input */
     INPUT,
     /** Output */
     OUTPUT,
     /** Alt 5 */
     ALT5,
     /** Alt 4 */
     ALT4,
     /** Alt 0 */
     ALT0,
     /** Alt 1 */
     ALT1,
     /** Alt 2 */
     ALT2,
     /** Alt 3 */
     ALT3 }

    /** Output type (momentary or latching) */
    private enum OUTPUT_TYPE {
     /** Momentary */
     MOMENTARY,
     /** Latching */
     LATCHING };

    /** GPIO pull modes */
    private enum PULL {
     /** Off */
     OFF,
     /** Up */
     UP,
     /** Down */
     DOWN };

    /** GPIO state */
    private enum STATE {
     /** High */
     HIGH,
     /** Low */
     LOW };

    /** List of GPIOPanels */
    private final java.util.List<GPIOPanel> panels = new ArrayList<>(PANELS);

    /** Reader for /dev/pigerr */
    private final BufferedReader pigerr;

    /** Reader for pigout */
    private final BufferedReader pigout;

    /** PrintStream for pigpio */
    private final PrintStream pigpio;

    /** Main program thread, watches pigerr for input */
    private final Thread thread;

    /** Run flag for main thread */
    private volatile boolean runFlag;

    /**
     * GUIGPIO program constructor.
     *
     * @throws  IOException on error setting up pigpio streams
     */
    public GUIGPIO() throws IOException {
        setTitle(String.format("GUIGPIO - Version: %s - Date: %s",
         VERSION,DATE));

        // initialize readers and streams for pigpiod
        pigerr = new BufferedReader(new FileReader("/dev/pigerr"));
        pigout = new BufferedReader(new FileReader("/dev/pigout"));
        pigpio = new PrintStream("/dev/pigpio");

        // empty /dev/pigout and get pigpio version
        while (pigout.ready())
            pigout.readLine();
        pigpio.printf("PIGPV\n");
        System.out.printf("pigpio version: %s%n",pigout.readLine());

        // main thread
        thread = new Thread(this);
        
        // initialize buttons
        for (int i=0; i<PANELS; i++)
            panels.add(new GPIOPanel());

        // window control
        addWindowListener(new WindowAdapter() {
            public void windowOpened(WindowEvent we) {
                panels.stream().forEach(GPIOPanel::start);
                start();
            }
            public void windowClosing(WindowEvent we) {
                dispose();
            }
            public void windowClosed(WindowEvent we) {
                panels.stream().forEach(GPIOPanel::stop);
                stop();
            }
        });

        // menus
        JMenuBar menuBar = new JMenuBar();
        JMenu file = menuBar.add(new JMenu("File"));
        JMenu help = menuBar.add(new JMenu("Help"));

        JMenuItem mi;

        mi = file.add("Quit");
        mi.addActionListener(event -> dispose());

        try (InputStream is =
         getClass().getResourceAsStream("directions.html")) {
            if (is != null) {
                try (BufferedReader reader = new BufferedReader(
                 new InputStreamReader(is,Charset.defaultCharset()))) {

                    String directions = reader.lines().collect(joining("\n"));

                    mi = help.add(new JMenuItem("Directions"));
                    mi.addActionListener(event ->
                     JOptionPane.showMessageDialog(help,new JLabel(directions),
                      "GUIGPIO Directions",JOptionPane.INFORMATION_MESSAGE));
                    help.addSeparator();
                }
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }

        mi = help.add("About");
        mi.addActionListener(event ->
         JOptionPane.showMessageDialog(GUIGPIO.this,"GUIGPIO\nVersion: " +
         VERSION + "\nDate: " + DATE + "\nAuthor: " + AUTHOR,"About GUIGPIO",
         JOptionPane.INFORMATION_MESSAGE));

        setJMenuBar(menuBar);

        // assemble gui
        setLayout(new GridBagLayout());

        GridBagConstraints c = new GridBagConstraints();
        c.insets = new Insets(2,2,2,2);
        c.gridy = 0;

        for (int i=0; i<PANELS; i++) {
            add(panels.get(i),c);
            if (i == (PANELS / 2) - 1)  // half way
                ++c.gridy;
        }

        // show window in center of screen
        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    /**
     * Starts the main program thread
     */
    public void start() {
        if (thread.getState() == Thread.State.NEW) {
            runFlag = true;
            thread.start();
        }
    }

    /**
     * Main program thread prints any messages from /dev/pigerr to console
     */
    public void run() {
        while (runFlag) {
            try {
                if (pigerr.ready())
                    System.out.printf("pigerr: %s\n",pigerr.readLine());
                Thread.sleep(200);
            } catch (IOException|InterruptedException ex) {
                if (runFlag)
                    ex.printStackTrace();
            }
        }
    }

    /**
     * Stops main thread
     */
    public void stop() {
        runFlag = false;
    }

    /**
     * Creates the GPIO panels
     */
    public class GPIOPanel extends JPanel implements Runnable {
        /** Serial version UID */
        private static final long serialVersionUID = 1L;

        /** Panel thread */
        private final Thread thread;

        /** Panel thread run flag */
        private volatile boolean runFlag;

        /** GPIO */
        protected volatile int gpio;

        /** GPIO mode */
        protected volatile MODE mode = MODE.INPUT;

        /** Output type */
        protected volatile OUTPUT_TYPE type = OUTPUT_TYPE.MOMENTARY;

        /** GPIO pull */
        protected volatile PULL pull = PULL.OFF;

        /** GPIO state */
        protected volatile STATE state = STATE.LOW;

        /** Foreground color for inactive, background color for active state */
        private Color activeColor = new Color(0xE30B56);

        /** Contrasting color */
        private Color inactiveColor = Color.WHITE;

        /** List for JLabels */
        private final java.util.List<JLabel> labels = new ArrayList<>();

        /** GPIO JLabel */
        private final JLabel gpioLabel;

        /** Mode JLabel */
        private final JLabel modeLabel;

        /** Type JLabel */
        private final JLabel typeLabel;

        /** Pull JLabel */
        private final JLabel pullLabel;

        /** State JLabel */
        private final JLabel stateLabel;

        /**
         * Constructs a new GPIOPanel
         */
        public GPIOPanel() {
            setToolTipText("Right Click to Configure");

            thread = new Thread(this);

            setPreferredSize(new Dimension(120,90));

            setBorder(BorderFactory.createLineBorder(activeColor,2));

            gpioLabel = new JLabel(String.format("GPIO %d",gpio));
            gpioLabel.setOpaque(true);
            labels.add(gpioLabel);

            modeLabel = new JLabel(mode.name());
            modeLabel.setOpaque(true);
            labels.add(modeLabel);

            typeLabel = new JLabel(type.name());
            typeLabel.setOpaque(true);
            labels.add(typeLabel);

            pullLabel = new JLabel(String.format("PULL %s",pull.name()));
            pullLabel.setOpaque(true);
            labels.add(pullLabel);

            stateLabel = new JLabel(state.name());
            stateLabel.setOpaque(true);
            labels.add(stateLabel);

            setLayout(new GridLayout(5,1));

            labels.forEach(GPIOPanel.this::add);

            addMouseListener(new MouseAdapter() {
                public void mousePressed(MouseEvent me) {
                    switch (me.getButton()) {
                        case MouseEvent.BUTTON1:
                            if (mode == MODE.OUTPUT) {
                                if (type == OUTPUT_TYPE.MOMENTARY) {
                                    state = STATE.HIGH;
                                } else {
                                    state = state == STATE.HIGH ?
                                     STATE.LOW : STATE.HIGH;
                                }
                            }
                            break;
                        case MouseEvent.BUTTON2:
                            break;
                        case MouseEvent.BUTTON3:
                            GPIOOptionsDialog dialog = new GPIOOptionsDialog(
                             GUIGPIO.this,true,GPIOPanel.this);
                            dialog.pack();
                            dialog.setLocationRelativeTo(GPIOPanel.this);
                            dialog.setVisible(true);
                            break;
                        default: break;
                    }
                }
                public void mouseReleased(MouseEvent me) {
                    if (me.getButton() == MouseEvent.BUTTON1) {
                        if (mode == MODE.OUTPUT) {
                            if (type == OUTPUT_TYPE.MOMENTARY) {
                                state = STATE.LOW;
                            }
                        }
                    }
                }
            });
        }

        /**
         * Start the GPIOPanel thread
         */
        private void start() {
            if (thread.getState() == Thread.State.NEW) {
                runFlag = true;
                thread.start();
            }
        }

        /**
         * GPIOPanel thread monitors the associated GPIO and displays that
         * information on the panel.
         */
        public void run() {
            while (runFlag) {
                try {
                    Thread.sleep(50);

                    if (gpio != 0) {
                        synchronized (pigpio) {
                            try {
                                if (mode == MODE.OUTPUT) {
                                    do {
                                        pigpio.printf("WRITE %d %d\n",gpio,
                                         state == STATE.HIGH ? 1 : 0);
                                    } while (!pigout.readLine().equals("0")) ;
                                } else if (mode == MODE.INPUT) {
                                    int status;
                                    do {
                                        pigpio.printf("READ %d\n",gpio);
                                        status = Integer.parseInt(
                                         pigout.readLine());
                                    } while (status < 0) ;
                                    state = status == 1 ? STATE.HIGH :
                                     STATE.LOW;
                                }
                            } catch (IOException ioe) {
                                ioe.printStackTrace();
                            }
                        }
                    }

                    EventQueue.invokeLater(() -> {
                        gpioLabel.setText(String.format("GPIO %d",gpio));
                        modeLabel.setText(mode.name());
                        typeLabel.setText(type.name());
                        pullLabel.
                         setText(String.format("PULL %s",pull.name()));
                        stateLabel.setText(state.name());
                        if (state == STATE.HIGH) {
                            labels.forEach(l ->l.setForeground(inactiveColor));
                            labels.forEach(l ->l.setBackground(activeColor));
                        } else {
                            labels.forEach(l -> l.setForeground(activeColor));
                            labels.forEach(l -> l.setBackground(inactiveColor));
                        }
                    });
                } catch (InterruptedException ie) {
                    if (runFlag)
                        ie.printStackTrace();
                }
            }
        }

        /**
         * Stops the panel thread
         */
        public void stop() {
            runFlag = false;
            thread.interrupt();
        }

        /**
         * Dialog to set panel options for GPIO, input or output, pull and
         * momentary or latching.
         */
        public class GPIOOptionsDialog extends JDialog {
            /** Serial version UID */
            private static final long serialVersionUID = 1L;

            /** OK button */
            private final JButton okButton;
            
            /** Cancel button */
            private final JButton cancelButton;

            /**
             * Creates a GPIOOptions dialog
             *
             * @param   owner       Owning frame
             * @param   modal       Modal state of dialog
             * @param   gpioPanel   Anchor for this dialog
             */
            public GPIOOptionsDialog(Frame owner,boolean modal,
             GPIOPanel gpioPanel) {
                super(owner,"GPIO Options",modal);
                JPanel dataPanel = new JPanel(new GridBagLayout());

                addWindowListener(new WindowAdapter() {
                    public void windowOpened(WindowEvent we) {
                        // make OK button and Cancel button the same size
                        okButton.setPreferredSize(
                         cancelButton.getPreferredSize());
                        okButton.revalidate();
                    }
                    public void windowClosing(WindowEvent we) {
                        dispose();
                    }
                });

                GridBagConstraints c = new GridBagConstraints();
                c.insets = new Insets(2,2,2,2);
                c.anchor = GridBagConstraints.WEST;

                c.gridy = 0;
                dataPanel.add(new JLabel("GPIO",JLabel.LEFT),c);
                Vector<Integer> vec = new Vector<>();
                vec.add(0);
                IntStream.rangeClosed(2,27).forEach(vec::add);
                JComboBox<Integer> gpioBox = new JComboBox<>(vec);
                gpioBox.setSelectedItem(gpio);
                gpioBox.addActionListener(event -> {
                    int gpio = (Integer)gpioBox.getSelectedItem();
                    for (GPIOPanel panel : panels) {
                        if (gpio != 0 && !panel.equals(gpioPanel) &&
                         gpio == panel.gpio) {
                            gpioBox.hidePopup();
                            JOptionPane.showMessageDialog(gpioBox,
                             "Duplicate GPIOs Not Allowed!","Error!",
                             JOptionPane.ERROR_MESSAGE);
                            return;
                        }
                        //
                    }
                });
                dataPanel.add(gpioBox,c);
        
                ++c.gridy;
                ButtonGroup modeGroup = new ButtonGroup();
                dataPanel.add(new JLabel("MODE",JLabel.LEFT),c);
                JRadioButton input = new JRadioButton("INPUT",
                 gpioPanel.mode == MODE.INPUT);
                dataPanel.add(input,c);
                modeGroup.add(input);
                JRadioButton output = new JRadioButton("OUTPUT",
                 gpioPanel.mode == MODE.OUTPUT);
                dataPanel.add(output,c);
                modeGroup.add(output);

                ++c.gridy;
                ButtonGroup pullGroup = new ButtonGroup();
                dataPanel.add(new JLabel("PULL",JLabel.LEFT),c);
                JRadioButton pullOff = new JRadioButton("OFF",
                 gpioPanel.pull == PULL.OFF);
                pullOff.setActionCommand("OFF");
                dataPanel.add(pullOff,c);
                pullGroup.add(pullOff);
                JRadioButton pullUp = new JRadioButton("UP",
                 gpioPanel.pull == PULL.UP);
                pullUp.setActionCommand("UP");
                dataPanel.add(pullUp,c);
                pullGroup.add(pullUp);
                JRadioButton pullDown = new JRadioButton("DOWN",
                 gpioPanel.pull == PULL.DOWN);
                pullDown.setActionCommand("DOWN");
                dataPanel.add(pullDown,c);
                pullGroup.add(pullDown);
    
                ++c.gridy;
                ButtonGroup typeGroup = new ButtonGroup();
                dataPanel.add(new JLabel("TYPE",JLabel.LEFT),c);
                JRadioButton typeMomentary = new JRadioButton("MOMENTARY",
                 gpioPanel.type == OUTPUT_TYPE.MOMENTARY);
                dataPanel.add(typeMomentary,c);
                typeGroup.add(typeMomentary);
                JRadioButton typeLatching = new JRadioButton("LATCHING",
                 gpioPanel.type == OUTPUT_TYPE.LATCHING);
                dataPanel.add(typeLatching,c);
                typeGroup.add(typeLatching);

                add(dataPanel,BorderLayout.CENTER);
    
                JPanel buttonPanel = new JPanel(new GridBagLayout());
                c = new GridBagConstraints();
                c.insets = new Insets(2,2,2,2);
                c.gridy = 0;

                c.anchor = GridBagConstraints.EAST;
                okButton = new JButton("OK");
                getRootPane().setDefaultButton(okButton);
                okButton.addActionListener(ev -> {
                    dispose();
                    gpio = (Integer)gpioBox.getSelectedItem();
                    // force state to low if GPIO is set to zero
                    if (gpio == 0)
                        state = STATE.LOW;
                    mode = output.isSelected() ? MODE.OUTPUT : MODE.INPUT;
                    pull = PULL.valueOf(
                     pullGroup.getSelection().getActionCommand());
                    type = typeMomentary.isSelected() ?
                     OUTPUT_TYPE.MOMENTARY : OUTPUT_TYPE.LATCHING;
                    synchronized (pigpio) {
                        try {
                             writeDataToGPIO();
                        } catch (IOException ioe) {
                             ioe.printStackTrace();
                        }
                    }
                });
                buttonPanel.add(okButton,c);

                c.anchor = GridBagConstraints.WEST;
                cancelButton = new JButton("Cancel");
                cancelButton.addActionListener(ev -> {
                    dispose();
                });
                buttonPanel.add(cancelButton,c);
    
                add(buttonPanel,BorderLayout.SOUTH);
            }
        }

        /**
         * Writes the mode, state if output mode, and pull to GPIO
         *
         * @throws IOException if an I/O error occurs
         */
        private void writeDataToGPIO() throws IOException {
            synchronized (pigpio) {
                if (mode == MODE.OUTPUT) {
                    pigpio.printf("MODES %d W\n",gpio);
                    pigout.readLine();
                    pigpio.printf("WRITE %d %d\n",gpio,
                     state == STATE.HIGH ? 1 : 0);
                    pigout.readLine();
                } else {
                    pigpio.printf("MODES %d R\n",gpio);
                    pigout.readLine();
                }

                if (pull == PULL.OFF) {
                    pigpio.printf("PUD %d O\n",gpio);
                    pigout.readLine();
                } else if (pull == PULL.UP) {
                    pigpio.printf("PUD %d U\n",gpio);
                    pigout.readLine();
                } else if (pull == PULL.DOWN) {
                    pigpio.printf("PUD %d D\n",gpio);
                    pigout.readLine();
                }
            }
        }
    }

    /**
     * GUIGPIO program entry point.
     *
     * @param   args    command line arguments (not used)
     */
    public static void main(String... args) {
        EventQueue.invokeLater(() -> {
            try {
                new GUIGPIO();
            } catch (IOException ioe) {
                JOptionPane.showMessageDialog(null,
                 "IOException in constructor\n".concat(ioe.toString()).
                 concat("\nProbably caused by pigpiod NOT running"),
                 "FATAL ERROR!",JOptionPane.ERROR_MESSAGE);
            }
        });
    }
}