/**
 * GmailAssistant 1.1 (2008-03-16)
 * Copyright 2008 Zach Scrivena
 * zachscrivena@gmail.com
 * http://gmailassistant.sourceforge.net/
 *
 * Notifier for multiple Gmail accounts.
 *
 * 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 version 2,
 * as published by the Free Software Foundation.
 *
 * 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 gmailassistant;

import java.awt.Color;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;


/**
 * Display popup messages on the desktop.
 */
class DesktopPopup
        extends JPanel
{
    /** timer refresh interval */
    private static final long TIMER_REFRESH_INTERVAL_MILLISECONDS = 200L;

    /** popup duration for first popup message (5 seconds) */
    private static final long FIRST_POPUP_DURATION_MILLISECONDS = 5000L;

    /** popup duration for subsequent popup messages (2 seconds) */
    private static final long POPUP_DURATION_MILLISECONDS = 2000L;

    /** additional popup duration for the last popup message (1 second) */
    private static final long LAST_POPUP_DURATION_MILLISECONDS = 1000L;

    /** default border color */
    private static final Color DEFAULT_BORDER_COLOR = (Color) Account.Property.COLOR.initialVal;

    /** tooltip text for the popup */
    private static final String POPUP_TOOLTIP = "<html>Click on the icon to skip to the next message.<br />Click on the text to dismiss messages.</html>";

    /** window that will contain the popup */
    private final JWindow window;

    /** popup data corresponding to each account */
    private final NavigableMap<Account,AccountPopupData> popupData =
            new TreeMap<Account,AccountPopupData>();

    /** timer for displaying popup messages */
    private final Timer timer;

    /** is the timer cancelled? */
    private volatile boolean timerCancelled = false;

    /** queued actions to be performed by the timer */
    private final Deque<ActionType> actions = new ArrayDeque<ActionType>();


    /**
    * Constructor.
    *
    * @param parent
    *      parent GmailAssistant object
    */
    DesktopPopup(
            final GmailAssistant parent)
    {
        /*******************************
        * INITIALIZE PANEL COMPONENTS *
        *******************************/

        initComponents();

        /******************************
        * CONFIGURE PANEL COMPONENTS *
        ******************************/

        setToolTipText(DesktopPopup.POPUP_TOOLTIP);

        this.icon.setToolTipText(DesktopPopup.POPUP_TOOLTIP);
        this.icon.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                skipMessage();
            }
        });

        this.text.setToolTipText(DesktopPopup.POPUP_TOOLTIP);
        this.text.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                cancelMessage();
            }
        });

        this.window = new JWindow();
        this.window.getContentPane().add(this);
        this.window.setAlwaysOnTop(true);
        this.window.pack();
        this.window.setFocusable(false);
        this.window.setVisible(false);

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

        this.timer = new Timer("Desktop-Popup-Timer", true);

        this.timer.schedule(new TimerTask()
        {
            SlidingAnimator animator;
            int width;
            int height;
            int X;
            int Y;

            @Override
            public void run()
            {
                ActionType action;
                ActionType interrupt;

                synchronized (DesktopPopup.this.actions)
                {
                    action = DesktopPopup.this.actions.pollFirst();
                }

                if ((action == ActionType.ALL) ||
                        (action == ActionType.RECENT))
                {
                    Entry<Account,AccountPopupData> entry;
                    Account ac = null;
                    AccountPopupData data;
                    boolean firstMessage = true;
                    boolean autoAdvance = true;

                    while (true)
                    {
                        /* get next account */
                        synchronized (DesktopPopup.this.popupData)
                        {
                            if (ac == null)
                            {
                                /* get first account */
                                entry = DesktopPopup.this.popupData.firstEntry();
                            }
                            else
                            {
                                /* get next account */
                                entry = DesktopPopup.this.popupData.higherEntry(ac);

                                if ((!autoAdvance) && (entry == null))
                                {
                                    /* reached last account; loop-around to first account again */
                                    entry = DesktopPopup.this.popupData.firstEntry();
                                    action = ActionType.ALL;
                                }
                            }

                            if (entry == null)
                            {
                                /* no more accounts left */
                                ac = null;
                                data = null;
                            }
                            else
                            {
                                ac = entry.getKey();
                                data = entry.getValue();
                            }
                        }

                        if (ac == null)
                        {
                            if (((Boolean) parent.getProperty(GmailAssistant.Property.ALERT_POPUP_PERSISTENT_MESSAGES)) &&
                                    (!firstMessage))
                            {
                                /* display persistent popup messages */
                                while (true)
                                {
                                    interrupt = sleepTillInterrupted(DesktopPopup.POPUP_DURATION_MILLISECONDS);

                                    if (interrupt == ActionType.SKIP)
                                    {
                                        autoAdvance = false;
                                        break;
                                    }
                                    else if (interrupt == ActionType.CANCEL)
                                    {
                                        setVisiblePopupMessage(false);
                                        return;
                                    }
                                }

                                /* loop-around to the first account */
                                continue;
                            }
                            else
                            {
                                /* exit loop */
                                break;
                            }
                        }

                        final int n = data.mails.length;
                        final Color borderColor = data.color;

                        for (int i = 0; i < n; i++)
                        {
                            final Mail m = data.mails[i];
                            final int k = i + 1;

                            interrupt = sleepTillInterrupted(0L);

                            if (interrupt == ActionType.SKIP)
                            {
                                autoAdvance = false;
                            }
                            else if (interrupt == ActionType.CANCEL)
                            {
                                setVisiblePopupMessage(false);
                                return;
                            }

                            boolean recent;

                            synchronized (data.lastIdLock)
                            {
                                recent = (m.sequenceNumber > data.lastId);
                            }

                            if ((action == ActionType.ALL) ||
                                    ((action == ActionType.RECENT) && recent))
                            {
                                final long duration = System.currentTimeMillis() - m.date.getTime();

                                final String popupMessageText =
                                        "<html><div style='white-space:nowrap'>" +
                                        "<b>" + k + " of " + n + "</b> unread " +
                                        ((n == 1) ? "mail" : "mails") +
                                        " for <b>" + data.username + "</b><br/>" +
                                        m.from + " (" + GmailAssistant.timeDurationString(duration) +
                                        ((duration >= 0) ? " ago" : " in the future") + ")<br />" +
                                        "<b>" + m.subject + "</b><br />" +
                                        "<i>" + m.snippet + "</i></div></html>";

                                try
                                {
                                    SwingUtilities.invokeAndWait(new Runnable()
                                    {
                                        public void run()
                                        {
                                            DesktopPopup.this.setBorder(BorderFactory.createLineBorder(borderColor, 4));
                                            DesktopPopup.this.text.setText(popupMessageText);
                                            DesktopPopup.this.window.pack();

                                            width = DesktopPopup.this.window.getWidth();
                                            height = DesktopPopup.this.window.getHeight();

                                            final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
                                            X = r.x + r.width - width;
                                            Y = r.y + r.height - height;

                                            DesktopPopup.this.window.setLocation(X, Y);
                                        }
                                    });
                                }
                                catch (Exception e)
                                {
                                    /* ignore */
                                }

                                if (firstMessage)
                                {
                                    /* animate entry of popup message */
                                    setVisiblePopupMessage(false);

                                    try
                                    {
                                        SwingUtilities.invokeAndWait(new Runnable()
                                        {
                                            public void run()
                                            {
                                                animator = new SlidingAnimator(
                                                        DesktopPopup.this.window,
                                                        width,
                                                        height,
                                                        X,
                                                        Y,
                                                        10,
                                                        SlidingAnimator.SlidingDirection.UP_IN);
                                            }
                                        });
                                    }
                                    catch (Exception e)
                                    {
                                        /* ignore */
                                    }

                                    animator.animate();
                                }

                                setVisiblePopupMessage(true);
                                firstMessage = false;

                                synchronized (data.lastIdLock)
                                {
                                    if (m.sequenceNumber > data.lastId)
                                    {
                                        data.lastId = m.sequenceNumber;
                                    }
                                }

                                /* pause before the next message */
                                while (true)
                                {
                                    interrupt = sleepTillInterrupted(firstMessage ?
                                        DesktopPopup.FIRST_POPUP_DURATION_MILLISECONDS :
                                        DesktopPopup.POPUP_DURATION_MILLISECONDS);

                                    if (interrupt == ActionType.SKIP)
                                    {
                                        autoAdvance = false;
                                        break;
                                    }
                                    else if (interrupt == ActionType.CANCEL)
                                    {
                                        setVisiblePopupMessage(false);
                                        return;
                                    }

                                    if (autoAdvance)
                                    {
                                        break;
                                    }
                                }
                            }
                        }
                    }

                    if (!firstMessage)
                    {
                        /* at least one popup message has been shown; animate exit */
                        interrupt = sleepTillInterrupted(DesktopPopup.LAST_POPUP_DURATION_MILLISECONDS);

                        if (interrupt == ActionType.CANCEL)
                        {
                            setVisiblePopupMessage(false);
                            return;
                        }

                        try
                        {
                            SwingUtilities.invokeAndWait(new Runnable()
                            {
                                public void run()
                                {
                                    final int width = DesktopPopup.this.window.getWidth();
                                    final int height = DesktopPopup.this.window.getHeight();

                                    final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
                                    final int X = r.x + r.width - width;
                                    final int Y = r.y + r.height - height;

                                    animator = new SlidingAnimator(
                                            DesktopPopup.this.window,
                                            width,
                                            height,
                                            X,
                                            Y,
                                            10,
                                            SlidingAnimator.SlidingDirection.DOWN_OUT);
                                }
                            });
                        }
                        catch (Exception e)
                        {
                            /* ignore */
                        }

                        animator.animate();
                        setVisiblePopupMessage(false);
                    }
                }
                else if (action == ActionType.TEST)
                {
                    /* display a test popup */
                    interrupt = sleepTillInterrupted(0L);

                    if (interrupt == ActionType.CANCEL)
                    {
                        setVisiblePopupMessage(false);
                        return;
                    }

                    final String popupMessageText = "<html>This is a test message.</html>";

                    try
                    {
                        SwingUtilities.invokeAndWait(new Runnable()
                        {
                            public void run()
                            {
                                DesktopPopup.this.setBorder(BorderFactory.createLineBorder(DesktopPopup.DEFAULT_BORDER_COLOR, 4));
                                DesktopPopup.this.text.setText(popupMessageText);
                                DesktopPopup.this.window.pack();

                                width = DesktopPopup.this.window.getWidth();
                                height = DesktopPopup.this.window.getHeight();

                                final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
                                X = r.x + r.width - width;
                                Y = r.y + r.height - height;

                                DesktopPopup.this.window.setLocation(X, Y);
                            }
                        });
                    }
                    catch (Exception e)
                    {
                        /* ignore */
                    }

                    /* animate entry of popup message */
                    setVisiblePopupMessage(false);

                    try
                    {
                        SwingUtilities.invokeAndWait(new Runnable()
                        {
                            public void run()
                            {
                                animator = new SlidingAnimator(
                                        DesktopPopup.this.window,
                                        width,
                                        height,
                                        X,
                                        Y,
                                        10,
                                        SlidingAnimator.SlidingDirection.UP_IN);
                            }
                        });
                    }
                    catch (Exception e)
                    {
                        /* ignore */
                    }

                    animator.animate();
                    setVisiblePopupMessage(true);

                    interrupt = sleepTillInterrupted(DesktopPopup.FIRST_POPUP_DURATION_MILLISECONDS);

                    if (interrupt == ActionType.CANCEL)
                    {
                        setVisiblePopupMessage(false);
                        return;
                    }

                    /* animate exit */
                    interrupt = sleepTillInterrupted(DesktopPopup.LAST_POPUP_DURATION_MILLISECONDS);

                    if (interrupt == ActionType.CANCEL)
                    {
                        setVisiblePopupMessage(false);
                        return;
                    }

                    try
                    {
                        SwingUtilities.invokeAndWait(new Runnable()
                        {
                            public void run()
                            {
                                final int width = DesktopPopup.this.window.getWidth();
                                final int height = DesktopPopup.this.window.getHeight();

                                final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
                                final int X = r.x + r.width - width;
                                final int Y = r.y + r.height - height;

                                animator = new SlidingAnimator(
                                        DesktopPopup.this.window,
                                        width,
                                        height,
                                        X,
                                        Y,
                                        10,
                                        SlidingAnimator.SlidingDirection.DOWN_OUT);
                            }
                        });
                    }
                    catch (Exception e)
                    {
                        /* ignore */
                    }

                    animator.animate();
                    setVisiblePopupMessage(false);
                }
                else if (action == ActionType.TERMINATE)
                {
                    DesktopPopup.this.timer.cancel();
                    DesktopPopup.this.timerCancelled = true;
                }
            }
        }, 0L, DesktopPopup.TIMER_REFRESH_INTERVAL_MILLISECONDS);
    }


    /**
    * Set visibility of the popup message.
    *
    * @param visible
    *      true to show popup message; false otherwise
    */
    private void setVisiblePopupMessage(
            final boolean visible)
    {
        try
        {
            SwingUtilities.invokeAndWait(new Runnable()
            {
                public void run()
                {
                    DesktopPopup.this.window.setVisible(visible);
                }
            });
        }
        catch (Exception e)
        {
            /* ignore */
        }
    }


    /**
    * Update popup messages for the specified account.
    *
    * @param ac
    *      account to be updated
    * @param username
    *      username for the account
    * @param color
    *      color for the account
    * @param mails
    *      unread mails for the account
    * @param showPopups
    *      true to show popup messages; false otherwise
    */
    void updateMessages(
            final Account ac,
            final String username,
            final Color color,
            final Mail[] mails,
            final boolean showPopups)
    {
        AccountPopupData data;

        synchronized (this.popupData)
        {
            data = this.popupData.get(ac);

            if (data == null)
            {
                data = new AccountPopupData();
                this.popupData.put(ac, data);
            }
        }

        data.username = username;
        data.color = color;
        data.mails = mails;
        Arrays.sort(data.mails);

        if (showPopups)
        {
            synchronized (this.actions)
            {
                this.actions.addLast(ActionType.RECENT);
            }
        }
        else
        {
            int maxId = 0;

            for (Mail m : data.mails)
            {
                if (m.sequenceNumber > maxId)
                {
                    maxId = m.sequenceNumber;
                }
            }

            synchronized (data.lastIdLock)
            {
                if (maxId > data.lastId)
                {
                    data.lastId = maxId;
                }
            }
        }
    }


    /**
    * Skip to the next popup message.
    */
    void skipMessage()
    {
        synchronized (this.actions)
        {
            this.actions.addFirst(ActionType.SKIP);
        }
    }


    /**
    * Cancel the current popup message.
    */
    void cancelMessage()
    {
        synchronized (this.actions)
        {
            this.actions.addFirst(ActionType.CANCEL);
        }
    }


    /**
    * Cancel display of all popup messages.
    */
    void cancelAllMessages()
    {
        synchronized (this.actions)
        {
            this.actions.clear();
            this.actions.addLast(ActionType.CANCEL);
        }
    }

    /**
    * Show all popup messages.
    */
    void showAllMessages()
    {
        synchronized (this.actions)
        {
            this.actions.clear();
            this.actions.addLast(ActionType.CANCEL);
            this.actions.addLast(ActionType.ALL);
        }
    }


    /**
    * Show a test popup message.
    */
    void test()
    {
        synchronized (this.actions)
        {
            this.actions.addLast(ActionType.TEST);
        }
    }


    /**
    * Terminate the timer thread.
    * This method blocks until the thread is cancelled.
    */
    void terminate()
    {
        synchronized (this.actions)
        {
            this.actions.clear();
        }

        do
        {
            synchronized (this.actions)
            {
                this.actions.addLast(ActionType.CANCEL);
                this.actions.addLast(ActionType.TERMINATE);
            }

            if (this.timerCancelled)
            {
                return;
            }

            try
            {
                Thread.sleep(DesktopPopup.TIMER_REFRESH_INTERVAL_MILLISECONDS / 10);
            }
            catch (Exception e)
            {
                /* ignore */
            }
        }
        while (true);
    }


    /**
    * Remove popup messages for the specified account.
    *
    * @param ac
    *      account for which to remove popup messages
    */
    void removeMessages(
            final Account ac)
    {
        synchronized (this.popupData)
        {
            this.popupData.remove(ac);
        }
    }


    /**
    * Sleep for the specified duration, or until interrupted.
    *
    * @pa