/**
 * ThisTime 2.0 (2008-03-30)
 * Copyright 2007 Zach Scrivena
 * zachscrivena@gmail.com
 * http://thistime.sourceforge.net/
 *
 * Simple clock and timer program.
 *
 * TERMS AND CONDITIONS:
 * 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/>.
 */

package thistime;

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Timer;
import java.util.TimerTask;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;


/**
 * Timer unit.
 */
class TimerUnit
        extends JFrame
{
    /** timer refresh interval */
    private static final long TIMER_REFRESH_INTERVAL_MILLISECONDS = 200L;

    /** default "always on top" mode */
    private static final boolean DEFAULT_ALWAYS_ON_TOP_MODE = true;

    /** tooltip for timer display when "set" mode is ON */
    private static final String DISPLAY_TOOLTIP_SET_ON =
            "<html>To increase a number, scroll up or click above the dotted line;<br />to decrease it, scroll down or click below the dotted line.</html>";

    /** tooltip for timer display when "set" mode is OFF */
    private static final String DISPLAY_TOOLTIP_SET_OFF = "Right-click for timer options";

    /** parent ThisTime object */
    private final ThisTime parent;

    /** timer display for this timer unit */
    private final TimerDisplay display;

    /** "normalized" timer value to be displayed */
    private final TimerValue value;

    /** accumulated elapsed time in milliseconds, for finer-resolution time-keeping */
    private long accumulatedElapsedMilliseconds = 0L;

    /** timer for refreshing timer value */
    private final Timer timer;

    /** true for count up timer; false for count down timer */
    private volatile boolean countUp;

    /** is the timer alarm enabled? (default is true) */
    private volatile boolean alarmEnabled = true;

    /** "minimize to tray" mode (default is true) */
    private boolean minimizeToTray = true;

    /** is the timer running? (initially false) */
    private volatile boolean isRunning = false;

    /** is the timer display in "set" mode? */
    private boolean setMode = false;

    /** time marker used for computing elapsed time, in milliseconds since the epoch */
    private volatile long timeMarker = System.currentTimeMillis();

    /** alarm audio clip */
    private AudioClip alarm;

    /** is the alarm currently playing? */
    private boolean alarmPlaying = false;

    /** tray icon for this timer unit */
    private TrayIcon trayIcon;


    /**
    * Constructor.
    * This method must run on the EDT.
    *
    * @param parent
    *      parent ThisTime object
    * @param title
    *      title of this timer unit
    * @param countUp
    *      true for count up timer; false for count down timer
    */
    TimerUnit(
            final ThisTime parent,
            final String title,
            final boolean countUp)
    {
        /*********************
        * INITIALIZE FIELDS *
        *********************/

        this.parent = parent;
        this.countUp = countUp;

        value = new TimerValue();
        display = new TimerDisplay(value);
        display.setSetMode(setMode);

        /******************************
        * INITIALIZE FORM COMPONENTS *
        ******************************/

        initComponents();

        /*****************************
        * CONFIGURE FORM COMPONENTS *
        *****************************/

        setTitle(title);

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                closeTimer();
            }

            @Override
            public void windowDeiconified(WindowEvent e)
            {
                restoreTimer();
            }

            @Override
            public void windowIconified(WindowEvent e)
            {
                minimizeTimer();
            }
        });

        /* set "always on top" mode */
        try
        {
            setAlwaysOnTop(DEFAULT_ALWAYS_ON_TOP_MODE);
        }
        catch (Exception e)
        {
            /* ignore */
        }

        /* set frame icon */
        final Image[] frameIcons = new Image[2];
        final int countUpIconIndex = 0;
        final int countDownIconIndex = 1;

        try
        {
            frameIcons[countUpIconIndex] = ImageIO.read(TimerUnit.class.getResource("/thistime/resources/clock_bullet_go_up.png"));
            frameIcons[countDownIconIndex] = ImageIO.read(TimerUnit.class.getResource("/thistime/resources/clock_bullet_go_down.png"));
            setIconImage(frameIcons[this.countUp ? countUpIconIndex : countDownIconIndex]);
        }
        catch (Exception e)
        {
            SwingManipulator.showErrorDialog(
                    null,
                    "Initialization Error - " + getTitle(),
                    "Failed to load timer icons (" + e + ").\n" +
                    "This timer will proceed to run without displaying the timer icons.");
        }

        /* load alarm audio clip */
        try
        {
            alarm = Applet.newAudioClip(TimerUnit.class.getResource("/thistime/resources/timeralarm.wav"));
        }
        catch (Exception e)
        {
            alarm = null;

            SwingManipulator.showErrorDialog(
                    null,
                    "Initialization Error - " + getTitle(),
                    "Failed to load alarm audio clip (" + e + ").\n" +
                    "This timer will proceed to run without playing the alarm audio clip.");
        }

        /* prepare tray icon */
        try
        {
            trayIcon = new TrayIcon(
                    frameIcons[this.countUp ? countUpIconIndex : countDownIconIndex],
                    getTitle());

            trayIcon.setImageAutoSize(true);

            trayIcon.addActionListener(new ActionListener()
            {
                public void actionPerformed(ActionEvent e)
                {
                    restoreTimer();
                }
            });
        }
        catch (Exception e)
        {
            trayIcon = null;

            SwingManipulator.showErrorDialog(
                    null,
                    "Initialization Error - " + getTitle(),
                    "Failed to load timer tray icon (" + e + ").\n" +
                    "This timer will proceed to run without displaying the timer tray icon.");
        }

        /* button: "Start" */
        startButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                if (!isRunning)
                {
                    timeMarker = System.currentTimeMillis();
                    isRunning = true;
                }

                stopAlarm();
            }
        });

        /* button: "Stop" */
        stopButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                if (isRunning)
                {
                    isRunning = false;
                }

                stopAlarm();
            }
        });

        /* button: "Reset" */
        resetButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                synchronized (value)
                {
                    value.negative = false;
                    value.h = 0L;
                    value.m = 0;
                    value.s = 0;
                    accumulatedElapsedMilliseconds = 0L;
                }

                display.repaint();
                stopAlarm();
            }
        });

        /* button: "Switch" */
        switchButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                TimerUnit.this.countUp = !TimerUnit.this.countUp;

                try
                {
                    setIconImage(frameIcons[TimerUnit.this.countUp ? countUpIconIndex : countDownIconIndex]);

                    if (trayIcon != null)
                    {
                        trayIcon.setImage(frameIcons[TimerUnit.this.countUp ? countUpIconIndex : countDownIconIndex]);
                    }
                }
                catch (Exception ex)
                {
                    /* ignore */
                }

                stopAlarm();
            }
        });

        /* button: "Set/Lock" */
        final Icon[] setButtonIcons = new Icon[2];
        final int setIconIndex = 0;
        final int lockIconIndex = 1;
        setButtonIcons[setIconIndex] = new ImageIcon(TimerUnit.class.getResource("/thistime/resources/control_equalizer_blue.png"));
        setButtonIcons[lockIconIndex] = new ImageIcon(TimerUnit.class.getResource("/thistime/resources/control_equalizer_blue_cancel.png"));

        setButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                setMode = !setMode;
                display.setSetMode(setMode);

                if (setMode)
                {
                    setButton.setIcon(setButtonIcons[lockIconIndex]);
                    display.setToolTipText(DISPLAY_TOOLTIP_SET_ON);
                    resetButton.setEnabled(true);
                    switchButton.setEnabled(true);
                }
                else
                {
                    setButton.setIcon(setButtonIcons[setIconIndex]);
                    display.setToolTipText(DISPLAY_TOOLTIP_SET_OFF);
                    resetButton.setEnabled(false);
                    switchButton.setEnabled(false);
                }

                display.repaint();
                stopAlarm();
            }
        });

        /* set initial button states */
        if (setMode)
        {
            setButton.setIcon(setButtonIcons[lockIconIndex]);
            display.setToolTipText(DISPLAY_TOOLTIP_SET_ON);
            resetButton.setEnabled(true);
            switchButton.setEnabled(true);
        }
        else
        {
            setButton.setIcon(setButtonIcons[setIconIndex]);
            display.setToolTipText(DISPLAY_TOOLTIP_SET_OFF);
            resetButton.setEnabled(false);
            switchButton.setEnabled(false);
        }

        /* popup menu (Set Title, Enable Alarm, Show Buttons, Always on Top, Minimize to Tray) */
        final JPopupMenu popupMenu = new JPopupMenu();

        /* popup menu: "Set Title..." */
        final JMenuItem titleMenuItem = new JMenuItem("Set Title...", 't');
        titleMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                String newTitle = JOptionPane.showInputDialog(
                        TimerUnit.this,
                        "Timer title:",
                        "Set Title - " + getTitle(),
                        JOptionPane.QUESTION_MESSAGE);

                if (newTitle == null)
                {
                    return;
                }

                newTitle = newTitle.trim();

                if (newTitle.isEmpty())
                {
                    newTitle = title;
                }

                setTitle(newTitle);

                if (trayIcon != null)
                {
                    trayIcon.setToolTip(newTitle);
                }
            }
        });
        popupMenu.add(titleMenuItem);

        /* popup menu: "Enable Alarm" */
        final JMenuItem alarmMenuItem = new JCheckBoxMenuItem("Enable Alarm", alarmEnabled);
        alarmMenuItem.setMnemonic('a');
        alarmMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                alarmEnabled = alarmMenuItem.isSelected();

                if (!alarmEnabled)
                {
                    stopAlarm();
                }
            }
        });
        popupMenu.add(alarmMenuItem);

        /* popup menu: "Show Buttons" */
        final JMenuItem buttonsMenuItem = new JCheckBoxMenuItem("Show Buttons", true);
        buttonsMenuItem.setMnemonic('b');
        buttonsMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                final Container c = getContentPane();
                c.removeAll();
                c.add(display, BorderLayout.CENTER);

                if (buttonsMenuItem.isSelected())
                {
                    c.add(buttonsPanel, BorderLayout.SOUTH);
                }

                validate();
            }
        });
        popupMenu.add(buttonsMenuItem);
        popupMenu.addSeparator();

        /* popup menu: "Always on Top" */
        final JMenuItem topMenuItem = new JCheckBoxMenuItem("Always on Top", isAlwaysOnTop());
        topMenuItem.setMnemonic('t');
        topMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                setAlwaysOnTop(topMenuItem.isSelected());
            }
        });
        popupMenu.add(topMenuItem);

        /* popup menu: "Minimize to Tray" */
        final JMenuItem trayMenuItem = new JCheckBoxMenuItem("Minimize to Tray", minimizeToTray);
        trayMenuItem.setMnemonic('t');
        trayMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                minimizeToTray = trayMenuItem.isSelected();
            }
        });
        popupMenu.add(trayMenuItem);

        /* add popup menu to timer display */
        display.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mousePressed(MouseEvent e)
            {
                processMouseEvent(e);
            }

            @Override
            public void mouseReleased(MouseEvent e)
            {
                processMouseEvent(e);
            }

            private void processMouseEvent(
                    final MouseEvent e)
            {
                if (e.isPopupTrigger())
                {
                    popupMenu.show(e.getComponent(), e.getX(), e.getY());
                }
            }
        });

        /* key binding: ESCAPE key */
        for (JComponent c : new JComponent[] {display, buttonsPanel})
        {
            c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                    .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_MINIMIZE");

            c.getActionMap().put("ESCAPE_MINIMIZE", new AbstractAction()
            {
                public void actionPerformed(ActionEvent e)
                {
                    minimizeTimer();
                }
            });
        }

        /* layout frame components */
        setLayout(new BorderLayout());
        final Container c = getContentPane();
        c.add(display, BorderLayout.CENTER);

        if (buttonsMenuItem.isSelected())
        {
            c.add(buttonsPanel, BorderLayout.SOUTH);
        }

        setPreferredSize(new Dimension(350, 200));
        pack();

        /* center form on the screen */
        setLocationRelativeTo(null);

        /***************************
        * INITIALIZE TIMER THREAD *
        ***************************/

        timer = new Timer("Timer-Unit-Timer", true);

        timer.schedule(new TimerTask()
        {
            /**
            * Refresh timer value.
            */
            @Override
            public void run()
            {
                if (isRunning)
                {
                    /* update time marker */
                    final long currentTime = System.currentTimeMillis();
                    final long newElapsedMilliseconds = currentTime - timeMarker;
                    timeMarker = currentTime;

                    synchronized (value)
                    {
                        accumulatedElapsedMilliseconds += newElapsedMilliseconds;

                        if ((accumulatedElapsedMilliseconds >= 1000L) ||
                                (accumulatedElapsedMilliseconds <= -1000L))
                        {
                            /* accumulated enough elapsed time to trigger a change in displayed value */
                            final long accumulatedElapsedSeconds = accumulatedElapsedMilliseconds / 1000;
                            accumulatedElapsedMilliseconds -= (accumulatedElapsedSeconds * 1000);

                            /* timer value in seconds */
                            long seconds = value.s;
                            seconds += (value.m * 60);
                            seconds += (value.h * 60 * 60);

                            if (value.negative)
                            {
                                seconds = -seconds;
                            }

                            /* add elapsed time */
                            final long prevSeconds = seconds;

                            if (TimerUnit.this.countUp)
                            {
                                seconds += accumulatedElapsedSeconds;

                                if ((prevSeconds < 0L) && (seconds >= 0L))
                                {
                                    startAlarm();
                                }
                            }
                            else
                            {
                                seconds -= accumulatedElapsedSeconds;

                                if ((prevSeconds > 0L) && (seconds <= 0L))
                                {
                                    startAlarm();
                                }
                            }

                            /* compute "normalized" timer value for display */
                            if (seconds < 0L)
                            {
                                /* negative value */
                                value.negative = true;
                                value.h = seconds / (-60 * 60);
                                seconds = -(seconds + value.h * 60 * 60);
                            }
                            else
                            {
                                /* nonnegative value */
                                value.negative = false;
                                value.h = seconds / (60 * 60);
                                seconds -= value.h * 60 * 60;
                            }

                            value.m = (int) seconds / 60;
                            value.s = (int) seconds - value.m * 60;

                            display.repaint();
                        }
                    }
                }
            }
        }, 0L, TIMER_REFRESH_INTERVAL_MILLISECONDS);
    }


    /**
    * Start playing the alarm.
    */
    private void startAlarm()
    {
        if ((alarm == null) || (!alarmEnabled))
        {
            return;
        }

        synchronized (alarm)
        {
            if (!alarmPlaying)
            {
                alarmPlaying = true;
                alarm.loop();

                if (trayIcon != null)
                {
                    trayIcon.displayMessage(getTitle(), "Alarm Alert", MessageType.INFO);
                }
            }
        }
    }


    /**
    * Stop playing the alarm.
    */
    private void stopAlarm()
    {
        if (alarm == null)
        {
            return;
        }

        synchronized (alarm)
        {
            if (alarmPlaying)
            {
                alarmPlaying = false;
                alarm.stop();
            }
        }
    }


    /**
    * Close this timer unit.
    */
    private void closeTimer()
    {
        if (isRunning)
        {
            /* prompt user about running timer */
            final int choice = JOptionPane.showConfirmDialog(
                    this,
                    "This timer is still running. Close this timer now?",
                    "Confirm Close - " + getTitle(),
                    JOptionPane.YES_NO_OPTION,
                    JOptionPane.WARNING_MESSAGE);

            if (choice != JOptionPane.YES_OPTION)
            {
                return;
            }
        }

        parent.removeTimer(this);
        setVisible(false);
        timer.cancel();
        stopAlarm();
        dispose();
    }


    /**
    * Minimize this timer unit.
    */
    void minimizeTimer()
    {
        setExtendedState(JFrame.ICONIFIED);

        if (minimizeToTray && (trayIcon != null))
        {
            try
            {
                final SystemTray systemTray = SystemTray.getSystemTray();
                boolean alreadyInTray = false;

                for (TrayIcon t : systemTray.getTrayIcons())
                {
                    if (t.equals(trayIcon))
                    {
                        alreadyInTray = true;
                        break;
                    }
                }

                if (!alreadyInTray)
                {
                    systemTray.add(trayIcon);
                }

                setVisible(false);
            }
            catch (Exception e)
            {
                SwingManipulator.showErrorDialog(
                        this,
                        "Error - " + getTitle(),
                        "Failed to minimize timer to tray (" + e + ").\n" +
                        "This timer will continue to stay on the desktop.");
            }
        }
    }


    /**
    * Restore this timer unit.
    */
    void restoreTimer()
    {
        setVisible(true);
        setExtendedState(JFrame.NORMAL);
        toFront();

        if (trayIcon != null)
        {
            try
            {
                SystemTray.getSystemTray().remove(trayIcon);
            }
            catch (Exception e)
            {
                /* ignore */
            }
        }
    }

    /***************************
    * NETBEANS-GENERATED CODE *
    ***************************/

    /** 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()
    {

        buttonsPanel = new javax.swing.JPanel();
        startButton = new javax.swing.JButton();
        stopButton = new javax.swing.JButton();
        resetButton = new javax.swing.JButton();
        switchButton = new javax.swing.JButton();
        setButton = new javax.swing.JButton();

        buttonsPanel.setLayout(new java.awt.GridLayout(1, 0));

        startButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_play_blue.png"))); // NOI18N
        startButton.setMnemonic('a');
        startButton.setToolTipText("<html>St<u>a</u>rt timer</html>");
        buttonsPanel.add(startButton);

        stopButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_stop_blue.png"))); // NOI18N
        stopButton.setMnemonic('s');
        stopButton.setToolTipText("<html><u>S</u>top timer</html>");
        buttonsPanel.add(stopButton);

        resetButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_eject_blue.png"))); // NOI18N
        resetButton.setMnemonic('r');
        resetButton.setToolTipText("<html><u>R</u>eset timer value to zero</html>");
        buttonsPanel.add(resetButton);

        switchButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_fastforward_blue.png"))); // NOI18N
        switchButton.setMnemonic('w');
        switchButton.setToolTipText("<html>S<u>w</u>itch between counting up and down</html>");
        buttonsPanel.add(switchButton);

        setButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_equalizer_blue.png"))); // NOI18N
        setButton.setMnemonic('e');
        setButton.setToolTipText("<html>S<u>e</u>t/Lock timer value</html>");
        buttonsPanel.add(setButton);

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 0, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 0, Short.MAX_VALUE)
        );

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

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JPanel buttonsPanel;
    private javax.swing.JButton resetButton;
    private javax.swing.JButton setButton;
    private javax.swing.JButton startButton;
    private javax.swing.JButton stopButton;
    private javax.swing.JButton switchButton;
    // End of variables declaration//GEN-END:variables
}