package com.knutejohnson.pi;

import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import javax.swing.*;

/**
 * <p>
 * Thermometer is a Java program to run on a Pi with a small touch screen
 * attached (ie UCTRONICS 3.5 inch touch screen) to display the temperature
 * collected from up to six remote Pis running one of the TemperatureSender??
 * programs.
 * </p>
 * <p>
 * Thermometer has two display panels, the TempPanel and the SelectPanel.
 * The TempPanel displays the temperature and location/description from a
 * selected channel and the SelectPanel allows selecting the channel to be
 * displayed.  Touching the TempPanel display will switch the display to the
 * SelectPanel.  Touching one of the channel select buttons on the SelectPanel
 * will switch the display to the TempPane with the selected channel now
 * displayed.
 * </p>
 * <p>
 * A forty pixel arc is drawn on the upper left corner of the TempPanel.
 * Touching inside of this arc will shut cause the shutdown of the Thermometer
 * program.
 * </p>
 * <p>
 * The data to be displayed is transmitted from another Pi running one of the
 * TemperatureSender?? applications.  The TemperatureSender?? application reads
 * the temperature from a temperature sensor and transmits its channel number,
 * location/description and temperature via multicast packets.
 * </p>
 * <p>
 * The default temperature display is in degrees fahrenheit.  To change the
 * display to Celsius, use the celsius command line option.  To display the
 * program in a regular window with a border and controls, use the notouch
 * command line option.
 * </p>
 *
 * <table class="striped" style="width: 100%;">
 * <caption>Command Line Options</caption>
 * <tr><th>Option<th>Description
 * <tr><td>celsius<td>display the temperature in degrees celsius, default is fahrenheit
 * <tr><td>notouch<td>used if running on a computer without a touch screen, adds a frame to the program window and unhides the mouse cursor
 * <tr><td>help<td>display usage and command line option descriptions
 * </table>
 *
 * <table class="striped" style="width: 100%;">
 * <caption>Program Revisions</caption>
 * <tr><th>Version<th>Date<th>Modification
 * <tr><td>0.10<td style="white-space: nowrap;">03 Jan 2019<td>incept
 * <tr><td>0.11<td>04 Jan 2019<td>documentation update for TemperatureSender variants
 * </table>
 *
 * @version 0.11 - 4 January 2019
 * @author  Knute Johnson
 */
public class Thermometer extends JFrame {
    /** Serial version UID */
    private static final long serialVersionUID = 1000;

    /** Program version */
    private static final String VERSION = "0.11";

    /** Program Date */
    private static final String DATE = "4 January 2019";

    /** Width of your touch screen */
    private static final int W = 480;

    /** Height of your touch screen */
    private static final int H = 320;

    /** Number of channels */
    private static final int CHANNELS = 6;

    /** Multicast port */
    private static final int PORT = 56789;

    /** Mulitcast socket address */
    private static final String IPADDR = "239.0.0.123";

    /** Receiver reference */
    private final Receiver rcvr;

    /** SelectPanel reference */
    private final SelectPanel selectPanel;

    /** TempPanel reference */
    private final TempPanel tempPanel;

    /** Invisible cursor */
    private final Cursor invisibleCursor;

    /** Celsius flag */
    private static boolean celsius;

    /** Touch screen flag */
    private static boolean touch = true;

    /** Current channel to display */
    private volatile int channel = 0;

    /** Channel data objects */
    private final Channel[] channels = new Channel[CHANNELS];

    /**
     * Creates a new Thermometer object, initializes the display panels and
     * multicast packet receiver, sizes the frame and displays it.
     */
    public Thermometer() {
        super(String.format("Thermometer - %s - %s",VERSION,DATE));

        // create the Channel objects for the channels array
        for (int i=0; i<channels.length; i++)
            channels[i] = new Channel(i);

        // if this is a touch screen don't add a decoration to the frame
        if (touch)
            setUndecorated(true);
        setLayout(new BorderLayout());

        // create the invisible cursor
        invisibleCursor = Toolkit.getDefaultToolkit().createCustomCursor(
         new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB),
         new Point(0,0),"Invisible Cursor");

        // create the panels and add the tempPanel to the frame
        selectPanel = new SelectPanel();
        tempPanel = new TempPanel();
        add(tempPanel,BorderLayout.CENTER);

        // create the receiver
        rcvr = new Receiver();

        // disposes the window and stops the receiver when the window is closed
        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent we) {
                stop();
            }
        });

        setSize(W,H);
        setLocation(0,0);
        setVisible(true);
    }

    /**
     * Starts the receiver
     */
    private void start() {
        rcvr.start();
    }

    /**
     * Disposes the Thermometer frame and stops the receiver
     */
    private void stop() {
        dispose();
        rcvr.stop();
    }

    /**
     * The TempPanel displays the temperature and the location for the selected
     * channel.  On startup channel 0 is selected.
     */
    private class TempPanel extends JPanel {
        /** Serial version UID */
        private static final long serialVersionUID = 1000;

        /**
         * Creates a new TempPanel
         */
        public TempPanel() {
            if (touch)
                setCursor(invisibleCursor);

            addMouseListener(new MouseAdapter() {
                public void mousePressed(MouseEvent me) {
                    // if touch screen add a touch area in the upper left that
                    // will shut down the program when touched
                    if (touch) {
                        double distance = Math.sqrt(
                         Math.pow(me.getX(),2.0) +
                         Math.pow(me.getY(),2.0));
                        if (distance < 40.0) {
                            Thermometer.this.stop();
                            return;
                        }
                    }
                    // touching the tempPanel switches the display to the
                    // selectPanel
                    Thermometer.this.remove(tempPanel);
                    Thermometer.this.add(selectPanel);
                    Thermometer.this.validate();
                    Thermometer.this.repaint();
                }
            });
        }

        /**
         * Draws the temperature and location/description data on the TempPanel
         *
         * @param   g2D graphics context
         */
        @Override public void paintComponent(Graphics g2D) {
            Graphics2D g = (Graphics2D)g2D;
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
             RenderingHints.VALUE_ANTIALIAS_ON);

            // fill background with dark blue
            g.setColor(Color.BLUE.darker());
            g.fillRect(0,0,getWidth(),getHeight());

            // select text color based on age of data
            long now = System.currentTimeMillis();
            if (now - channels[channel].getTime() < 120000)
                g.setColor(Color.WHITE);
            else if (now - channels[channel].getTime() < 240000)
                g.setColor(Color.YELLOW.darker());
            else
                g.setColor(Color.RED);

            // if touch screen draw the arc around the area in the upper left
            // where a touch will shut down program
            if (touch)
                g.drawOval(-40,-40,80,80);

            // convert the String temperature of a Channel to a double
            double temp;
            try {
                temp = Double.parseDouble(channels[channel].getTemp()) / 1000.0;
            } catch (NumberFormatException nfe) {
                temp = -40.0;
            }
            // if display is to be in fahrenheit rescale the temperature
            if (!celsius)
                temp = temp * 9.0 / 5.0 + 32.0;

            String tempStr = String.format("%.0f\u00b0",temp);

            g.setFont(new Font("Liberation Sans",Font.BOLD,220));
            FontMetrics fm = g.getFontMetrics();
            g.drawString(tempStr,
             centerString(tempStr,fm,getWidth()),
             fm.getAscent());

            String locStr = channels[channel].getLocation();

            g.setFont(new Font("Liberation Sans",Font.BOLD,36));
            fm = g.getFontMetrics();
            g.drawString(locStr,
             centerString(locStr,fm,getWidth()),
             getHeight() - fm.getHeight());
        }

        /**
         * Utility method to calculate the x value to draw a string in the
         * center of a display area.
         *
         * @param   str     String to display
         * @param   fm      FontMetrics for the current Graphics object
         * @param   width   Width of the drawing area in pixels
         *
         * @return  an x value to use with Graphics#drawString to center the
         *          text in the drawing area
         */
        private int centerString(String str, FontMetrics fm, int width) {
            return (width - fm.stringWidth(str)) / 2;
        }
    }

    /**
     * The SelectPanel is used to select the channel to display.  There are six
     * display channels available.
     */
    private class SelectPanel extends JPanel {
        /** Serial version UID */
        private static final long serialVersionUID = 1000;

        /** List of channel select buttons */
        private final java.util.List<JButton> chanButtons =
         new ArrayList<>(CHANNELS);

        /**
         * Creates a new SelectPanel object
         */
        public SelectPanel() {
            if (touch)
                setCursor(invisibleCursor);

            setLayout(new GridLayout(2,3));

            // create the channel buttons
            for (int i=0; i<CHANNELS; i++) {
                JButton chanButton = new JButton(Integer.toString(i));
                add(chanButton);
                chanButtons.add(chanButton);
                chanButton.addActionListener(event -> {
                    channel = chanButtons.indexOf(event.getSource());
                    Thermometer.this.remove(selectPanel);
                    Thermometer.this.add(tempPanel);
                    Thermometer.this.validate();
                    Thermometer.this.repaint();
                });
            }
        }

        /**
         * Gets a reference to the channel buttons list
         *
         * @return  reference to channel buttons list
         */
        public java.util.List<JButton> getButtons() {
            return chanButtons;
        }
    }

    /**
     * Class to hold data for a specified channel including the location,
     * temperature and the last time data was received for this channel.
     */
    private class Channel {
        /** Channel number of this channel */
        private final int channel;

        /** Location of this channel */
        private String location = "";

        /** Last temperature received for this channel, default is -40 degrees
         */
        private String temperature = "";

        /** Time in millis since epoch that the last temperature was received
         *  for this channel
         */
        private long time;

        /**
         * Create a new Channel object with the specified channel number
         *
         * @param   channel channel number
         */
        public Channel(int channel) {
            this.channel = channel;
        }

        /**
         * Get the channel number of this channel
         *
         * @return  channel number of this channel
         */
        public int getChannel() {
            return channel;
        }

        /**
         * Get the location of this channel
         *
         * @return  location of this channel
         */
        public synchronized String getLocation() {
            return location;
        }

        /**
         * Get the latest temperature reported for this channel
         *
         * @return  latest reported temperature
         */
        public synchronized String getTemp() {
            return temperature;
        }

        /**
         * Get the time of the last temperature update
         *
         * @return  time of the last temperature update
         */
        public synchronized long getTime() {
            return time;
        }

        /**
         * Set the location of this channel
         *
         * @param   location to be set
         */
        public synchronized void setLocation(String location) {
            this.location = location;
        }

        /**
         * Set the temperature for this channel and update the time
         *
         * @param   temperature to be set
         */
        public synchronized void setTemp(String temperature) {
            this.temperature = temperature;
            time = System.currentTimeMillis();
        }
    }

    /**
     * Receiver listens for multicast packets from the TemperatureSender??
     * program and stores that information in the appropriate channel.
     */
    private class Receiver implements Runnable {
        /** Main thread */
        private final Thread thread;

        /** Run flag used to stop the main thread */
        private volatile boolean runFlag;

        /**
         * Multicast socket to receive data from the TemperatureSender??
         * program
         */
        private MulticastSocket socket;

        /**
         * Create a new Receiver object
         */
        public Receiver() {
            thread = new Thread(this,"Thermometer.Receiver");
        }

        /**
         * Sets the runFlag to true and starts the main thread running if it
         * has not yet been started
         */
        public void start() {
            if (thread.getState() == Thread.State.NEW) {
                runFlag = true;
                thread.setPriority(Thread.MAX_PRIORITY);
                thread.start();
            }
        }

        /**
         * Receives the data packets from the TemperatureSender?? programs and
         * updates the appropriate Channel objects with current data.
         */
        @Override public void run() {
            while (runFlag) {
                byte[] buf = new byte[256];
                DatagramPacket packet = new DatagramPacket(buf,buf.length);
                try {
                    socket = new MulticastSocket(PORT);
                    socket.joinGroup(InetAddress.getByName(IPADDR));
                    socket.setSoTimeout(135000);  // 2:15 timeout
                    while (true) {
                        // A received packet for any channel will update the
                        // tempPanel.  If no packets are received for 2:15 the
                        // socket will timeout and the tempPanel will be
                        // updated
                        try {
                            socket.receive(packet);
                            String[] str = new String(packet.getData(),
                             packet.getOffset(),packet.getLength(),
                             StandardCharsets.UTF_8).split(",");
                            System.out.printf("%s %s %s,%s,%s\n",
                             LocalTime.now().format(
                             DateTimeFormatter.ofPattern("HH:mm")),
                             packet.getAddress().getHostName(),
                             str[0],str[1],str[2]);
                            int chan = Integer.parseInt(str[1]);
                            if (chan < 0 || chan >= CHANNELS)
                                throw new IOException("bad channel number");
                            channels[chan].setTemp(str[2]);
                            channels[chan].setLocation(str[0]);
                            EventQueue.invokeLater(() -> {
                             selectPanel.getButtons().get(chan).setText(
                             channels[chan].getLocation());
                             tempPanel.repaint();
                            });
                        } catch (SocketTimeoutException ste) {
                            tempPanel.repaint();
                        }
                    }
                } catch (IOException ioe) {
                    // if runFlag is false, the program is being shut down and
                    // we don't need the exception displayed
                    if (runFlag)
                        ioe.printStackTrace();
                } finally {
                    socket.close();
                }
            }
        }

        /**
         * Sets the runFlag to false and closes the multicast socket to stop
         * the main thread.
         */
        public void stop() {
            runFlag = false;
            socket.close();
        }
    }

    /**
     * Program entry point, parses the command line options and creates a new
     * Thermometer object and starts it running.
     *
     * @param   args    command line arguments
     */
    public static void main(String... args) {
        for (String arg : args) {
            if ("celsius".startsWith(arg.toLowerCase()))
                celsius = true;
            if ("notouch".startsWith(arg.toLowerCase()))
                touch = false;
            if ("help".startsWith(arg.toLowerCase())) {
                System.out.println(
                 "java -jar Thermometer.jar [celsius] [notouch] [help]");
                System.out.println(
                 "   celsius    -   display the temperature in celsius, " +
                                    "default is fahrenheit\n" +
                 "   notouch    -   unhide the cursor and add a frame around " +
                                    "the program window\n" +
                 "   help       -   display this message");
                System.exit(0);
            }

        }

        EventQueue.invokeLater(() -> new Thermometer().start());
    }
}