/**
 * GmailAssistant 2.0 (2008-09-07)
 * Copyright 2008 Zach Scrivena
 * zachscrivena@gmail.com
 * http://gmailassistant.sourceforge.net/
 *
 * Notifier for multiple Gmail and Google Apps email 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 org.freeshell.zs.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.Deque;
import java.util.HashMap;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import org.freeshell.zs.common.Debug;
import org.freeshell.zs.common.Debug.ValueCapsule;
import org.freeshell.zs.common.SlidingAnimator;
import org.freeshell.zs.common.SwingManipulator;


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

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

    /** 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;

    /** border width of the popup */
    private static final int POPUP_BORDER_WIDTH = 4;

    /** popup test message string */
    private static final String TEST_STRING =
            "<html><div style='white-space:nowrap'><b>1 of 1</b> unread mail for <b>testaccount</b><br />" +
            "Sender name here (1 second ago)<br />" +
            "<b>Subject line here</b><br />" +
            "<i>This is a popup test message...</i></div></html>";

    /** popup message string for the case of no unread mails */
    private static final String NO_UNREAD_MAILS_STRING =
            "<html><br />There are no unread mails at this time.</html>";

    /** parent GmailAssistant object */
    private final GmailAssistant parent;

    /** default border color */
    private final Color defaultBorderColor;

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

    /** window that will contain the popup */
    private final JWindow window = new JWindow();

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

    /** last mail sequence number for each account */
    private final Map<Account,Integer> lastMailSequenceNumbers = new HashMap<Account,Integer>();


    /**
    * Constructor.
    *
    * @param parent
    *      parent GmailAssistant object
    */
    DesktopPopup(
            final GmailAssistant parent)
    {
        this.parent = parent;
        defaultBorderColor = new Color(parent.properties.getInt("color"));

        /*******************************
        * INITIALIZE PANEL COMPONENTS *
        *******************************/

        initComponents();

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

        setToolTipText(POPUP_TOOLTIP);

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

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

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

        /*********************************************
        * INITIALIZE THREAD TO MONITOR POPUP EVENTS *
        *********************************************/

        new Thread(new Runnable()
        {
            public void run()
            {
                NextPopupEventIteration:
                while (true)
                {
                    Debug.sleep(REFRESH_INTERVAL_MILLISECONDS);

                    ActionType action;
                    ActionType interrupt;

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

                    if ((action == ActionType.ALL) ||
                            (action == ActionType.RECENT))
                    {
                        Mail m = null;
                        Mail prev = null;
                        boolean shownFirstMail = false;
                        boolean manualAdvance = false;
                        int[] coordinates = null;
                        final StringBuilder message = new StringBuilder();
                        int k = 0;

                        ShowNextMailIteration:
                        while (true)
                        {
                            /* advance to the next mail without looping back */
                            m = advanceToNextMail(m, false);

                            if (m == null)
                            {
                                /* no more mails without looping back */
                                if (shownFirstMail)
                                {
                                    if (manualAdvance)
                                    {
                                        /* advance to the next mail with loop back */
                                        m = advanceToNextMail(m, true);

                                        if (m == null)
                                        {
                                            /* no more mails even with loop back */
                                            break ShowNextMailIteration;
                                        }
                                        else
                                        {
                                            /* start showing all unread messages; not just recent ones */
                                            action = ActionType.ALL;
                                        }
                                    }
                                    else
                                    {
                                        /* wait for user to advance or dismiss popup, if necessary */
                                        if (parent.properties.getBoolean("alert.popup.persistent.messages"))
                                        {
                                            while (true)
                                            {
                                                interrupt = sleepTillInterrupted(POPUP_DURATION_MILLISECONDS);

                                                /* check for interruption */
                                                if (interrupt == ActionType.ADVANCE)
                                                {
                                                    manualAdvance = true;
                                                    continue ShowNextMailIteration;
                                                }
                                                else if (interrupt == ActionType.CANCEL)
                                                {
                                                    SwingManipulator.setVisibleWindow(window, false);
                                                    continue NextPopupEventIteration;
                                                }
                                            }
                                        }
                                        else
                                        {
                                            break ShowNextMailIteration;
                                        }
                                    }
                                }
                                else
                                {
                                    /* have not shown a single mail until now */
                                    if (action == ActionType.ALL)
                                    {
                                        showSingleMessage(NO_UNREAD_MAILS_STRING, defaultBorderColor);
                                    }

                                    continue NextPopupEventIteration;
                                }
                            }

                            /* determine index of the next mail within its account */
                            if ((prev != null) &&
                                    prev.account.equals(m.account) &&
                                    (m.compareTo(prev) > 0))
                            {
                                k++;
                            }
                            else
                            {
                                k = 1;
                            }

                            prev = m;

                            /* should this mail be shown? */
                            boolean showMail = true;

                            /* check if only recent mails should be shown */
                            if (action == ActionType.RECENT)
                            {
                                final Integer last;

                                synchronized (lastMailSequenceNumbers)
                                {
                                    last = lastMailSequenceNumbers.get(m.account);
                                }

                                if ((last != null) && (m.sequenceNumber <= last))
                                {
                                    showMail = false;
                                }
                            }

                            if (showMail)
                            {
                                /* populate popup message */
                                final long duration = System.currentTimeMillis() - m.date.getTime();
                                int n = m.account.getTotalNumMails();
                                n = Math.max(n, k);

                                message.setLength(0);
                                message.append("<html><div style='white-space:nowrap'><b>");
                                message.append(k);
                                message.append(" of ");
                                message.append(n);
                                message.append("</b> unread ");
                                message.append((n == 1) ? "mail" : "mails");
                                message.append(" for <b>");
                                message.append(m.account.properties.getString("username"));
                                message.append("</b><br/>");
                                message.append(m.from);
                                message.append(" (");
                                message.append(GmailAssistant.timeDurationString(duration));
                                message.append((duration >= 0) ? " ago" : " in the future");
                                message.append(")<br /><b>");
                                message.append(m.subject);
                                message.append("</b><br /><i>");
                                message.append(m.snippet);
                                message.append("</i></div></html>");

                                coordinates = populatePopupMessage(
                                        message.toString(),
                                        (Color) m.account.properties.get("color.object"));

                                /* animate popup entry if this is the first mail shown */
                                if (!shownFirstMail)
                                {
                                    SlidingAnimator.animate(
                                            window,
                                            coordinates[0],
                                            coordinates[1],
                                            SlidingAnimator.Direction.UP_IN,
                                            10);

                                    shownFirstMail = true;
                                    interrupt = sleepTillInterrupted(FIRST_POPUP_DURATION_MILLISECONDS);
                                }
                                else
                                {
                                    interrupt = sleepTillInterrupted(POPUP_DURATION_MILLISECONDS);
                                }

                                /* update last mail sequence number for this account */
                                synchronized (lastMailSequenceNumbers)
                                {
                                    final Integer last = lastMailSequenceNumbers.get(m.account);

                                    if ((last == null) || (m.sequenceNumber > last))
                                    {
                                        lastMailSequenceNumbers.put(m.account, m.sequenceNumber);
                                    }
                                }

                                /* check for interruption */
                                if (interrupt == ActionType.ADVANCE)
                                {
                                    manualAdvance = true;
                                    continue ShowNextMailIteration;
                                }
                                else if (interrupt == ActionType.CANCEL)
                                {
                                    SwingManipulator.setVisibleWindow(window, false);
                                    continue NextPopupEventIteration;
                                }

                                if (manualAdvance)
                                {
                                    /* wait for user to advance or dismiss popup */
                                    while (true)
                                    {
                                        interrupt = sleepTillInterrupted(POPUP_DURATION_MILLISECONDS);

                                        /* check for interruption */
                                        if (interrupt == ActionType.ADVANCE)
                                        {
                                            manualAdvance = true;
                                            continue ShowNextMailIteration;
                                        }
                                        else if (interrupt == ActionType.CANCEL)
                                        {
                                            SwingManipulator.setVisibleWindow(window, false);
                                            continue NextPopupEventIteration;
                                        }
                                    }
                                }
                            }
                        }

                        if (shownFirstMail)
                        {
                            if (sleepTillInterrupted(LAST_POPUP_DURATION_MILLISECONDS) == ActionType.CANCEL)
                            {
                                SwingManipulator.setVisibleWindow(window, false);
                                continue NextPopupEventIteration;
                            }

                            /* at least one mail was shown; proceed to animate exit */
                            SlidingAnimator.animate(
                                    window,
                                    coordinates[0],
                                    coordinates[1],
                                    SlidingAnimator.Direction.DOWN_OUT,
                                    10);
                        }
                    }
                    else if (action == ActionType.TEST)
                    {
                        showSingleMessage(TEST_STRING, defaultBorderColor);
                    }
                    else if (action == ActionType.CANCEL)
                    {
                        SwingManipulator.setVisibleWindow(window, false);
                    }
                    else if (action == ActionType.TERMINATE)
                    {
                        break NextPopupEventIteration;
                    }
                }
            }
        }).start();
    }


    /**
    * Advance to the next mail that comes after the specified current mail.
    *
    * @param mail
    *     current mail; null if the first mail among all accounts is to be returned
    * @param loopback
    *     should we loop back to the first account if there are no accounts left?
    *     (loopback occurs once only)
    * @return
    *     the next mail after the specified current mail; null if there is no such mail
    */
    private Mail advanceToNextMail(
            final Mail mail,
            final boolean loopback)
    {
        Account ac = null;
        Mail m = null;

        if (mail == null)
        {
            /* get the first account */
            ac = parent.getFirstAccount();

            if (ac == null)
            {
                /* there are zero accounts */
                return null;
            }
            else
            {
                m = ac.getFirstMail();
            }
        }
        else
        {
            /* get the next mail in the same account */
            ac = mail.account;
            m = ac.getNextMail(mail);
        }

        boolean seenLast = false;

        while (m == null)
        {
            /* advance to the next account */
            ac = parent.getNextAccount(ac);

            if (ac == null)
            {
                /* there are no more accounts */
                if (seenLast)
                {
                    /* cannot find any mail */
                    return null;
                }

                if (loopback)
                {
                    /* loop back to the first account */
                    ac = parent.getFirstAccount();

                    if (ac == null)
                    {
                        /* there are zero accounts */
                        return null;
                    }

                    seenLast = true;
                }
                else
                {
                    /* cannot find any mail */
                    return null;
                }
            }

            /* get the first mail of this account */
            m = ac.getFirstMail();
        }

        return m;
    }


    /**
    * Show a single popup message.
    * This method blocks until the animation has finished; it should NOT be called on the EDT.
    *
    * @param message
    *     message text to be displayed on the popup message
    * @param color
    *     color of the popup message border
    */
    private void showSingleMessage(
            final String message,
            final Color color)
    {
        /******************************
        * (1) POPULATE POPUP MESSAGE *
        ******************************/

        final int[] coordinates = populatePopupMessage(message, color);

        if (sleepTillInterrupted(0L) == ActionType.CANCEL)
        {
            SwingManipulator.setVisibleWindow(window, false);
            return;
        }

        /***************************
        * (2) ANIMATE POPUP ENTRY *
        ***************************/

        SlidingAnimator.animate(
                window,
                coordinates[0],
                coordinates[1],
                SlidingAnimator.Direction.UP_IN,
                10);

        /***********************************
        * (3) DISPLAY POPUP FOR SOME TIME *
        ***********************************/

        if (sleepTillInterrupted(FIRST_POPUP_DURATION_MILLISECONDS) == ActionType.CANCEL)
        {
            SwingManipulator.setVisibleWindow(window, false);
            return;
        }

        /**************************
        * (4) ANIMATE POPUP EXIT *
        **************************/

        SlidingAnimator.animate(
                window,
                coordinates[0],
                coordinates[1],
                SlidingAnimator.Direction.DOWN_OUT,
                10);

        SwingManipulator.setVisibleWindow(window, false);
    }


    /**
    * Populate the popup message with the specified message text.
    * This method blocks until the popup message is populated; it should NOT be called on the EDT.
    *
    * @param message
    *     message text to be displayed on the popup message
    * @param color
    *     color of the popup message border
    * @return
    *     an int array containing the x and y coordinates of the window origin, in that order
    */
    private int[] populatePopupMessage(
            final String message,
            final Color color)
    {
        final ValueCapsule<int[]> coordinates = new ValueCapsule<int[]>();

        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                text.setText(message);
                setBorder(BorderFactory.createLineBorder(color, POPUP_BORDER_WIDTH));
                window.pack();

                final int width = window.getWidth();
                final int height = window.getHeight();
                final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();

                int x = r.x + r.width - width;
                int y = r.y + r.height - height;

                window.setLocation(x, y);
                coordinates.set(new int[] {x, y});
            }
        });

        return coordinates.get();
    }


    /**
    * Update popup messages for the specified account.
    *
    * @param ac
    *     account to be updated
    * @param alertPopup
    *     is the "Popup" alert active for the specified account?
    */
    void updateMessages(
            final Account ac,
            final boolean alertPopup)
    {
        if (alertPopup)
        {
            showRecentMessages();
        }
        else
        {
            /* update last mail sequence number for this account */
            final Mail m = ac.getLastMail();

            if (m != null)
            {
                synchronized (lastMailSequenceNumbers)
                {
                    final Integer last = lastMailSequenceNumbers.get(ac);

                    if ((last == null) || (m.sequenceNumber > last))
                    {
                        lastMailSequenceNumbers.put(ac, m.sequenceNumber);
                    }
                }
            }
        }
    }


    /**
    * Advance to the next popup message.
    */
    void advanceMessage()
    {
        synchronized (actions)
        {
            actions.addFirst(ActionType.ADVANCE);
        }
    }


    /**
    * Dismiss the popup message.
    */
    void dismissPopup()
    {
        synchronized (actions)
        {
            actions.addFirst(ActionType.CANCEL);
        }
    }


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

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


    /**
    * Show recent popup messages.
    */
    private void showRecentMessages()
    {
        synchronized (actions)
        {
            actions.addLast(ActionType.RECENT);
        }
    }


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


    /**
    * Terminate the popup thread.
    */
    void terminate()
    {
        synchronized (actions)
        {
            actions.clear();
            actions.addLast(ActionType.CANCEL);
            actions.addLast(ActionType.TERMINATE);
        }
    }


    /**
    * Sleep for the specified duration, or until interrupted.
    *
    * @param duration
    *      sleep duration in milliseconds
    * @return
    *      action that caused the interruption (CANCEL or ADVANCE) which would be dequeued
    *      from the action queue; null if no interruption occurred
    */
    private ActionType sleepTillInterrupted(
            final long duration)
    {
        if (duration <= 0L)
        {
            synchronized (actions)
            {
                final ActionType interrupt = actions.peekFirst();

                if ((interrupt == ActionType.CANCEL) ||
                        (interrupt == ActionType.ADVANCE))
                {
                    return actions.pollFirst();
                }
                else
                {
                    return null;
                }
            }
        }
        else
        {
            final long start = System.currentTimeMillis();

            while (true)
            {
                if ((System.currentTimeMillis() - start) >= duration)
                {
                    return null;
                }

                synchronized (actions)
                {
                    final ActionType interrupt = actions.peekFirst();

                    if ((interrupt == ActionType.CANCEL) ||
                            (interrupt == ActionType.ADVANCE))
                    {
                        return actions.pollFirst();
                    }
                }

                Debug.sleep(REFRESH_INTERVAL_MILLISECONDS / 10);
            }
        }
    }

    /******************
    * NESTED CLASSES *
    ******************/

    /**
    * Types of action.
    */
    private enum ActionType
    {
        ALL,
        RECENT,
        ADVANCE,
        CANCEL,
        TEST,
        TERMINATE
    }

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

        panel = new javax.swing.JPanel();
        icon = new javax.swing.JLabel();
        text = new javax.swing.JLabel();

        setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(255, 102, 0), 4));
        setOpaque(false);

        panel.setBackground(new java.awt.Color(255, 255, 255));
        panel.setBorder(javax.swing.BorderFactory.createEmptyBorder(3, 3, 3, 3));

        icon.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        icon.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/gmailassistant/resources/ga_logo_40.png"))); // NOI18N
        icon.setFocusable(false);

        text.setText("Text");
        text.setVerticalAlignment(javax.swing.SwingConstants.TOP);

        javax.swing.GroupLayout panelLayout = new javax.swing.GroupLayout(panel);
        panel.setLayout(panelLayout);
        panelLayout.setHorizontalGroup(
            panelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(panelLayout.createSequentialGroup()
                .addComponent(icon, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(text, javax.swing.GroupLayout.PREFERRED_SIZE, 319, javax.swing.GroupLayout.PREFERRED_SIZE))
        );
        panelLayout.setVerticalGroup(
            panelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(icon, javax.swing.GroupLayout.PREFERRED_SIZE, 67, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addComponent(text, javax.swing.GroupLayout.PREFERRED_SIZE, 67, javax.swing.GroupLayout.PREFERRED_SIZE)
        );

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(panel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(panel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        );
    }// </editor-fold>//GEN-END:initComponents

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JLabel icon;
    private javax.swing.JPanel panel;
    private javax.swing.JLabel text;
    // End of variables declaration//GEN-END:variables
}