/*
 * Copyright 2007 Perry Nguyen <pfnguyen@hanhuy.com>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import java.awt.event.ActionEvent;
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.awt.EventQueue;
import java.awt.Font;
import java.awt.Image;
import java.awt.Point;
import java.io.InputStream;
import java.io.IOException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.concurrent.SynchronousQueue;
import java.util.prefs.Preferences;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.Clip;
import javax.imageio.ImageIO;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.Action;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JFormattedTextField;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.SwingUtilities;
import javax.swing.text.MaskFormatter;

public class StopWatch {
    JFormattedTextField timeField;
    final JFrame frame;
    JPopupMenu menu;
    Action newAction   = new NewAction();
    Action startAction = new StartAction();
    Action stopAction  = new StopAction();
    Action resetAction = new ResetAction();
    Action exitAction  = new CloseAction();
    Worker worker = new Worker();
    Thread thread;
    TimerJob job;
    final static String defaultValue = "00:00:00.0";
    static Preferences prefs = Preferences.userNodeForPackage(StopWatch.class);
    static int windowsOpened = 0;

    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        new StopWatch();
    }

    StopWatch() throws Exception {
        String loc = prefs.get("location", null);
        InputStream in = getClass().getClassLoader().getResourceAsStream(
                "clock32.png");
        Image icon = ImageIO.read(in);
        in.close();
        Point p = null;
        if (loc != null) {
            String[] parts = loc.split("x");
            int x = Integer.parseInt(parts[0]);
            int y = Integer.parseInt(parts[1]);
            p = new Point(x, y);
        }
        final JFrame f = new JFrame("StopWatch Timer");
        f.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosed(WindowEvent e) {
                windowsOpened--;
                System.out.println("windows open: " + windowsOpened);
                if (windowsOpened == 0)
                    System.exit(0);
            }
        });
        frame = f;
        f.setIconImages(Arrays.asList(icon));
        MaskFormatter format = new MaskFormatter("##:##:##.#");
        timeField = new JFormattedTextField(format);
        timeField.setValue(defaultValue);
        Font font = new Font(Font.MONOSPACED, Font.BOLD, 24);
        timeField.setFont(font);
        f.setUndecorated(true);
        f.add(timeField);
        f.pack();
        if (p != null)
            f.setLocation(p);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        MouseAdapter m = new MouseAdapter() {
            Point mp = null; // current drag location start
            Point fp = null; // frame translated point
            Point fl = null; // final location
            @Override
            public void mousePressed(MouseEvent e) {
                maybeShowPopup(e);
            }
            @Override
            public void mouseReleased(MouseEvent e) {
                maybeShowPopup(e);
                if (fl != null)
                    prefs.put("location", String.format("%dx%d", fl.x, fl.y));
                mp = null;
            }
            @Override
            public void mouseDragged(MouseEvent e) {
                if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
                    return;
                Point p = e.getLocationOnScreen();
                if (mp == null) {
                    mp = e.getPoint();
                    fp = SwingUtilities.convertPoint(
                            timeField, e.getPoint(), f);
                }
                p.x = p.x - fp.x;
                p.y = p.y - fp.y;
                f.setLocation(p);
                fl = p;
            }
        };
        timeField.addMouseListener(m);
        timeField.addMouseMotionListener(m);
        ActionMap actionMap = timeField.getActionMap();
        InputMap inputMap = timeField.getInputMap();
        actionMap.put(newAction.getValue(Action.NAME), newAction);
        actionMap.put(startAction.getValue(Action.NAME), startAction);
        actionMap.put(stopAction.getValue(Action.NAME),  stopAction);
        actionMap.put(resetAction.getValue(Action.NAME), resetAction);
        actionMap.put(exitAction.getValue(Action.NAME), exitAction);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
                startAction.getValue(Action.NAME));
        inputMap.put((KeyStroke) newAction.getValue(Action.ACCELERATOR_KEY),
                newAction.getValue(Action.NAME));
        inputMap.put((KeyStroke) startAction.getValue(Action.ACCELERATOR_KEY),
                startAction.getValue(Action.NAME));
        inputMap.put((KeyStroke) stopAction.getValue(Action.ACCELERATOR_KEY),
                stopAction.getValue(Action.NAME));
        inputMap.put((KeyStroke) resetAction.getValue(Action.ACCELERATOR_KEY),
                resetAction.getValue(Action.NAME));
        inputMap.put((KeyStroke) exitAction.getValue(Action.ACCELERATOR_KEY),
                exitAction.getValue(Action.NAME));
        menu = new JPopupMenu();
        menu.add(newAction);
        menu.addSeparator();
        menu.add(startAction);
        menu.add(stopAction);
        menu.add(resetAction);
        menu.addSeparator();
        menu.add(exitAction);
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                f.setVisible(true);
                windowsOpened++;
            }
        });
        thread = new Thread(worker);
        thread.start();
    }

    private void maybeShowPopup(MouseEvent e) {
        if (e.isPopupTrigger())
            menu.show(e.getComponent(), e.getX(), e.getY());
    }


    private class Worker implements Runnable {
        volatile boolean shutdown = false;
        SynchronousQueue<TimerJob> queue = new SynchronousQueue<TimerJob>();
        public void run() {
            while (!shutdown) {
                try {
                    TimerJob job = queue.take();
                    job.run();
                }
                catch (InterruptedException e) {
                    if (!shutdown)
                        e.printStackTrace();
                }
            }
        }
        void start(TimerJob job) {
            queue.add(job);
        }
    }
    private interface TimerJobCallback {
        public void onInterval();
    }
    private class TimerJob implements Runnable {
        TimerJobCallback cb;
        TimerJob(TimerJobCallback callback) {
            cb = callback;
        }
        volatile boolean stop;
        public void run() {
            while (!stop) {
                try {
                    // we display tenths of seconds, but make sure there isn't
                    // a misstep.
                    Thread.sleep(50);
                    cb.onInterval();
                }
                catch (InterruptedException e) {
                    if (!stop)
                        e.printStackTrace();
                    stop = true;
                }
            }
        }
        void stop() {
            stop = true;
        }
    }
    private class StartAction extends AbstractAction {
        {
            putValue(Action.NAME, "Start");
            putValue(Action.MNEMONIC_KEY, KeyEvent.VK_S);
            putValue(Action.ACCELERATOR_KEY,
                    KeyStroke.getKeyStroke("control S"));
        }
        public void actionPerformed(ActionEvent e) {
            setEnabled(false);
            try {
                timeField.commitEdit();
            }
            catch (ParseException ex) { }
            timeField.setEditable(false);
            final TimeDisplay d = new TimeDisplay((String)timeField.getValue());
            TimerJobCallback cb = new TimerJobCallback() {
                public void onInterval() {
                    EventQueue.invokeLater(new Runnable() {
                        public void run() {
                            timeField.setValue(d.getCurrentString());
                        }
                    });
                }
            };
            job = new TimerJob(cb);
            worker.start(job);
            stopAction.setEnabled(true);
            resetAction.setEnabled(false);
            exitAction.setEnabled(false);
        }
    }
    private class StopAction extends AbstractAction {
        {
            putValue(Action.NAME, "Stop");
            putValue(Action.MNEMONIC_KEY, KeyEvent.VK_T);
            putValue(Action.ACCELERATOR_KEY,
                    KeyStroke.getKeyStroke("control T"));
            setEnabled(false);
        }
        public void actionPerformed(ActionEvent e) {
            setEnabled(false);
            if (job != null)
                job.stop();
            job = null;
            startAction.setEnabled(true);
            exitAction.setEnabled(true);
            timeField.setEditable(true);
            if (!defaultValue.equals(timeField.getValue()))
                resetAction.setEnabled(true);
        }
    }
    private class ResetAction extends AbstractAction {
        {
            putValue(Action.NAME, "Reset");
            putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
            putValue(Action.ACCELERATOR_KEY,
                    KeyStroke.getKeyStroke("control R"));
            setEnabled(false);
        }
        public void actionPerformed(ActionEvent e) {
            setEnabled(false);
            timeField.setValue(defaultValue);
        }
    }
    private class CloseAction extends AbstractAction {
        {
            putValue(Action.NAME, "Close Window");
            putValue(Action.MNEMONIC_KEY, KeyEvent.VK_W);
            putValue(Action.ACCELERATOR_KEY,
                    KeyStroke.getKeyStroke("control W"));
        }
        public void actionPerformed(ActionEvent e) {
            frame.setVisible(false);
            frame.dispose();
        }
    }
    private class NewAction extends AbstractAction {
        {
            putValue(Action.NAME, "New Window");
            putValue(Action.MNEMONIC_KEY, KeyEvent.VK_N);
            putValue(Action.ACCELERATOR_KEY,
                    KeyStroke.getKeyStroke("control N"));
        }
        public void actionPerformed(ActionEvent e) {
            try {
                new StopWatch();
            }
            catch (Exception ex) {
                JOptionPane.showMessageDialog(frame, ex.getMessage(),
                        "Unable to open new window", JOptionPane.ERROR_MESSAGE);
                ex.printStackTrace();
            }
        }
    }

    private class TimeDisplay {
        long startTime = System.currentTimeMillis();
        boolean finished = false;
        long duration = 0; // in tenths of seconds
        String durationLength;

        TimeDisplay(String duration) {
            this.duration = parseDuration(duration);
            durationLength = duration;
        }

        String getCurrentString() {
            long h, m, s, c;
            h = m = s = c = 0;
            // tenths of seconds
            long elapsed = (System.currentTimeMillis() - startTime) / 100;
            if (duration == 0) {
                c = elapsed % 10L;
                elapsed = elapsed / 10L;
                s = elapsed % 60L;
                elapsed = elapsed / 60L;
                m = elapsed % 60L;
                elapsed = elapsed / 60L;
                h = elapsed % 60L;
                elapsed = elapsed / 60L;
            } else {
                // countdown
                long remaining = duration - elapsed;
                if (finished)
                    return defaultValue;
                if (remaining <= 0) {
                    finished = true;
                    remaining = 0;
                    EventQueue.invokeLater(new Runnable() {
                        public void run() {
                            stopAction.actionPerformed(null);
                            InputStream in = getClass().getClassLoader()
                                    .getResourceAsStream("alarm.wav");
                            try {
                                AudioInputStream ain = AudioSystem
                                        .getAudioInputStream(in);
                                Clip c = AudioSystem.getClip();
                                c.open(ain);
                                c.loop(2);
                            } catch (Exception e) {
                                e.printStackTrace();
                            } finally {
                                try {
                                    if (in != null)
                                        in.close();
                                }
                                catch (IOException e) { /* ignore */ }
                            }
                            JOptionPane.showMessageDialog(frame,
                                    durationLength + " timer finished!");
                        }
                    });
                }

                c = remaining % 10L;
                remaining = remaining / 10L;
                s = remaining % 60L;
                remaining = remaining / 60L;
                m = remaining % 60L;
                remaining = remaining / 60L;
                h = remaining % 60L;
                remaining = remaining / 60L;

            }
            return String.format("%02d:%02d:%02d.%d", h, m, s, c);
        }

        long parseDuration(String d) {
            String[] parts = d.split(":");
            int h = Integer.parseInt(parts[0]);
            int m = Integer.parseInt(parts[1]);
            parts = parts[2].split("\\.");
            int s = Integer.parseInt(parts[0]);
            int c = Integer.parseInt(parts[1]);

            return (h * 60 * 60 * 10) + (m * 60 * 10) + (s * 10) + c;
        }
    }
}

