/**
 * 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.AWTException;
import java.awt.Color;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
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.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumnModel;


/**
 * Notifier for multiple Gmail accounts.
 * This is the main class of the program.
 */
public class GmailAssistant
        extends JFrame
        implements ListSelectionListener
{
    /** program name */
    static final String NAME = "GmailAssistant";

    /** program version */
    static final String VERSION = "1.1";

    /** program date */
    static final String DATE = "2008-03-16";

    /** URL for homepage */
    static final String HOMEPAGE = "http://gmailassistant.sourceforge.net/";

    /** program description text, to be shown in "about" dialog */
    static final String DESCRIPTION =
            GmailAssistant.NAME + " " +
            GmailAssistant.VERSION + " (" +
            GmailAssistant.DATE + ")\n" +
            "Copyright 2008 Zach Scrivena\n" +
            "zachscrivena@gmail.com\n" +
            GmailAssistant.HOMEPAGE;

    /** URL for latest release information */
    static final String LATEST_RELEASE_URL =
            "http://gmailassistant.sourceforge.net/latest.txt";

    /** URL for bug reporting */
    static final String BUG_REPORT =
            "http://sourceforge.net/tracker/?func=add&group_id=214554&atid=1030145";

    /** default filename for profile file */
    static final String PROFILE_DEFAULT_FILENAME = "profile";

    /** filename extension for profile files */
    static final String PROFILE_FILENAME_EXTENSION = "ga";

    /** map: key string ---> property */
    static final Map<String,Property> KEY_STRING_TO_PROPERTY = new HashMap<String,Property>();

    /** delimiter string for separating different property key:value pairs */
    static final String PROPERTY_DELIMITER = "\n";

    /** email accounts */
    final List<Account> accounts = new ArrayList<Account>();

    /** table model for table of accounts */
    private final AccountsTableModel accountsTableModel = new AccountsTableModel();

    /** "Load Profile" form */
    private ProfileLoader profileLoader = null;

    /** "Save Profile" form */
    private ProfileSaver profileSaver = null;

    /** "Options" form */
    Options options = null;

    /** "About" form */
    private About about = null;

    /** tray icon for program */
    final SimpleTrayIcon trayIcon;

    /** popup alerter */
    final DesktopPopup popup;

    /** keyboard LED blinker */
    final KeyboardLedBlinker led;

    /** chime player */
    final ChimePlayer chime;

    /** last used account ID */
    private int lastId;

    /** mutex lock for this.lastId */
    private final Object lastIdLock = new Object();

    /** program properties */
    private final Map<Property,Object> properties;


    /**
    * Static initialization block.
    */
    static
    {
        /* create map: key string ---> property */
        for (Property p : Property.values())
        {
            if (p.keyString != null)
            {
                GmailAssistant.KEY_STRING_TO_PROPERTY.put(p.keyString, p);
            }
        }
    }


    /**
    * Constructor.
    */
    public GmailAssistant()
    {
        /*********************************
        * INITIALIZE PROGRAM PROPERTIES *
        *********************************/

        this.properties = new HashMap<Property,Object>();

        for (Property p : Property.values())
        {
            properties.put(p, p.initialVal);
        }

        /*********************
        * INITIALIZE ALERTS *
        *********************/

        this.popup = new DesktopPopup(this);
        this.chime = new ChimePlayer(this);
        this.led = new KeyboardLedBlinker();

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

        initComponents();

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

        setTitle(GmailAssistant.NAME);
        setAlwaysOnTop((Boolean) getProperty(Property.ALWAYS_ON_TOP));

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowIconified(WindowEvent e)
            {
                minimizeProgram();
            }

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

            @Override
            public void windowClosing(WindowEvent e)
            {
                closeProgram();
            }
        });

        /* tooltips */
        ToolTipManager.sharedInstance().setInitialDelay(0);

        /* menu item: "Load Profiles..." */
        this.loadMenuItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (GmailAssistant.this.profileLoader == null)
                {
                    GmailAssistant.this.profileLoader = new ProfileLoader(GmailAssistant.this);
                }

                GmailAssistant.this.profileLoader.showForm();
            }
        });

        /* menu item: "Save Profile..." */
        this.saveMenuItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (GmailAssistant.this.profileSaver == null)
                {
                    GmailAssistant.this.profileSaver = new ProfileSaver(GmailAssistant.this);
                }

                GmailAssistant.this.profileSaver.showForm();
            }
        });

        /* menu item: "Exit" */
        this.exitMenuItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                closeProgram();
            }
        });

        /* menu item: "Options..." */
        this.optionsMenuItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (GmailAssistant.this.options == null)
                {
                    GmailAssistant.this.options = new Options(GmailAssistant.this);
                }

                GmailAssistant.this.options.setVisible(true);
                GmailAssistant.this.options.setExtendedState(JFrame.NORMAL);
                GmailAssistant.this.options.toFront();
            }
        });

        /* menu item: "Usage..." */
        this.usageMenuItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                try
                {
                    SwingManipulator.showInfoDialog(GmailAssistant.this,
                            "Usage Information - " + GmailAssistant.NAME,
                            "Usage information for " + GmailAssistant.NAME,
                            ResourceManipulator.resourceAsString("/gmailassistant/resources/usage_text.txt"),
                            10);
                }
                catch (Exception ee)
                {
                    SwingManipulator.showErrorDialog(GmailAssistant.this, getTitle(),
                            "(INTERNAL) Failed to read \"Usage\" text.");
                }
            }
        });

        /* menu item: "About..." */
        this.aboutMenuItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (GmailAssistant.this.about == null)
                {
                    GmailAssistant.this.about = new About(GmailAssistant.this);
                }

                GmailAssistant.this.about.setVisible(true);
                GmailAssistant.this.about.setExtendedState(JFrame.NORMAL);
                GmailAssistant.this.about.toFront();
            }
        });

        /* button: "Add" */
        this.addButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                addNewAccount(null);
                valueChanged(null);
            }
        });

        /* button: "Remove" */
        this.removeButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                removeAccounts();
                valueChanged(null);
            }
        });

        /* button: "Edit" */
        this.editButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                editAccounts();
                valueChanged(null);
            }
        });

        /* button: "Enable" */
        this.enableButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                enableAccounts();
                valueChanged(null);
            }
        });

        /* button: "Disable" */
        this.disableButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                disableAccounts();
                valueChanged(null);
            }
        });

        /* key binding: ESCAPE key */
        this.accountsPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_CANCEL_BUTTON");

        this.accountsPane.getActionMap().put("ESCAPE_CANCEL_BUTTON", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.setExtendedState(JFrame.ICONIFIED);
            }
        });

        /* key binding: DELETE key */
        this.accountsPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "DELETE_REMOVE_BUTTON");

        this.accountsPane.getActionMap().put("DELETE_REMOVE_BUTTON", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.removeButton.doClick();
            }
        });

        /* table of accounts */
        this.accountsTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
        final TableColumnModel colModel = this.accountsTable.getColumnModel();
        colModel.getColumn(0).setMinWidth(0);
        colModel.getColumn(1).setMinWidth(0);
        colModel.getColumn(2).setMinWidth(0);
        colModel.getColumn(3).setMinWidth(0);
        colModel.getColumn(0).setPreferredWidth(5);
        colModel.getColumn(1).setPreferredWidth(80);
        colModel.getColumn(2).setPreferredWidth(40);
        colModel.getColumn(3).setPreferredWidth(200);

        this.accountsTable.setToolTipText("Mouse-over any row for more information");
        this.accountsTable.getSelectionModel().addListSelectionListener(this);

        this.accountsTable.setDefaultRenderer(TableCellContent.class, new AccountsTableCellRenderer(
                this.accountsTable.getForeground(),
                this.accountsTable.getBackground(),
                this.accountsTable.getSelectionForeground(),
                this.accountsTable.getSelectionBackground()));

        /* accounts popup menu (add, remove, edit, enable, disable) */
        final JPopupMenu accountsPopupMenu = new JPopupMenu();

        /* accounts popup menu: "Add" */
        final JMenuItem addMenuItem = new JMenuItem("Add", 'A');
        addMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.addButton.doClick();
            }
        });
        accountsPopupMenu.add(addMenuItem);

        /* accounts popup menu: "Remove" */
        final JMenuItem removeMenuItem = new JMenuItem("Remove", 'R');
        removeMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.removeButton.doClick();
            }
        });
        accountsPopupMenu.add(removeMenuItem);

        /* accounts popup menu: "Edit" */
        final JMenuItem editMenuItem = new JMenuItem("Edit", 't');
        editMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.editButton.doClick();
            }
        });
        accountsPopupMenu.add(editMenuItem);

        /* accounts popup menu: "Enable" */
        final JMenuItem enableMenuItem = new JMenuItem("Enable", 'E');
        enableMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.enableButton.doClick();
            }
        });
        accountsPopupMenu.add(enableMenuItem);

        /* accounts popup menu: "Disable" */
        final JMenuItem disableMenuItem = new JMenuItem("Disable", 'D');
        disableMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.disableButton.doClick();
            }
        });
        accountsPopupMenu.add(disableMenuItem);
        accountsPopupMenu.addSeparator();

        /* accounts popup menu: "Select All" */
        final JMenuItem selectAllMenuItem = new JMenuItem("Select All", 'S');
        selectAllMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.accountsTable.selectAll();
            }
        });
        accountsPopupMenu.add(selectAllMenuItem);

        /* accounts popup menu: "Clear All" */
        final JMenuItem clearAllMenuItem = new JMenuItem("Clear All", 'C');
        clearAllMenuItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.accountsTable.clearSelection();
            }
        });
        accountsPopupMenu.add(clearAllMenuItem);

        this.accountsTable.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())
                {
                    /* display popup menu */
                    final int i = GmailAssistant.this.accountsTable.rowAtPoint(e.getPoint());

                    if ((i >= 0) && (!GmailAssistant.this.accountsTable.isRowSelected(i)))
                    {
                        GmailAssistant.this.accountsTable.setRowSelectionInterval(i, i);
                    }

                    accountsPopupMenu.show(e.getComponent(), e.getX(), e.getY());
                }
                else if ((e.getClickCount() >= 2) && SwingUtilities.isLeftMouseButton(e))
                {
                    /* open selected account for editing */
                    final int i = GmailAssistant.this.accountsTable.rowAtPoint(e.getPoint());

                    if (i >= 0)
                    {
                        GmailAssistant.this.accountsTable.setRowSelectionInterval(i, i);
                        GmailAssistant.this.editButton.doClick();
                    }
                }
            }
        });

        valueChanged(null);

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

        /*************************************************
        * SETUP PROGRAM ICON, TRAY ICON, AND POPUP MENU *
        *************************************************/

        /* setup program icon */
        try
        {
            setIconImage(ImageIO.read(GmailAssistant.class.getResource("/gmailassistant/resources/ga_logo_64.png")));
        }
        catch (IOException e)
        {
            throw new TerminatingException("Failed to load program icon image.");
        }

        /* create popup menu for system tray icon */
        final PopupMenu trayMenu = new PopupMenu(GmailAssistant.NAME);

        /* popup menu: "Check Mail Now" */
        final MenuItem checkItem = new MenuItem("Check Mail Now");
        checkItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                checkMailNow();
            }
        });
        trayMenu.add(checkItem);

        /* popup menu: "Tell me again..." */
        final MenuItem againItem = new MenuItem("Tell me Again...");
        againItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                GmailAssistant.this.popup.showAllMessages();
            }
        });
        trayMenu.add(againItem);

        /* popup menu: "Reset Alerts" */
        final MenuItem resetItem = new MenuItem("Reset Alerts");
        resetItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                resetAlerts();
            }
        });
        trayMenu.add(resetItem);
        trayMenu.addSeparator();

        /* popup menu: "Restore" */
        final MenuItem restoreItem = new MenuItem("Restore");
        restoreItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                restoreProgram();
            }
        });
        trayMenu.add(restoreItem);

        /* popup menu: "Exit" */
        final MenuItem exitItem = new MenuItem("Exit");
        exitItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                closeProgram();
            }
        });
        trayMenu.add(exitItem);

        try
        {
            this.trayIcon = new SimpleTrayIcon(this);
            this.trayIcon.setPopupMenu(trayMenu);
            this.trayIcon.addActionListener(new ActionListener()
            {
                public void actionPerformed(ActionEvent e)
                {
                    restoreProgram();
                }
            });

            SystemTray.getSystemTray().add(this.trayIcon);
        }
        catch (IOException e)
        {
            throw new TerminatingException("Failed to load system tray icon images.");
        }
        catch (AWTException e)
        {
            throw new TerminatingException("Failed to set system tray icon.");
        }
    }


    /**
    * Respond to a list selection event on the table of accounts.
    *
    * @param e
    *      list selection event
    */
    @Override
    public void valueChanged(
            ListSelectionEvent e)
    {
        final int[] rows = this.accountsTable.getSelectedRows();
        final int numRows = rows.length;

        /* updateMessages button states */
        if (numRows == 0)
        {
            this.removeButton.setEnabled(false);
            this.editButton.setEnabled(false);
            this.enableButton.setEnabled(false);
            this.disableButton.setEnabled(false);
        }
        else
        {
            this.removeButton.setEnabled(true);
            this.editButton.setEnabled(true);
            this.enableButton.setEnabled(true);
            this.disableButton.setEnabled(true);
        }
    }


    /**
    * Check for unread mails in all accounts now.
    */
    private void checkMailNow()
    {
        synchronized (this.accounts)
        {
            for (Account ac : this.accounts)
            {
                ac.checkMailNow();
            }
        }
    }


    /**
    * Reset all alerts.
    */
    private void resetAlerts()
    {
        this.trayIcon.setNormalIcon();
        this.popup.cancelAllMessages();
        this.chime.cancelAll();
        this.chime.stopPeriodicBell();
        this.led.cancelAll();
    }


    /**
    * Refresh the specified account on the table.
    *
    * @param ac
    *      account to be refreshed
    */
    void refreshAccountOnTable(
            final Account ac)
    {
        synchronized (this.accounts)
        {
            final int i = this.accounts.indexOf(ac);

            if (i >= 0)
            {
                this.accountsTableModel.fireTableRowsUpdated(i, i);
                valueChanged(null);
            }
        }

        refreshUnreadMailCount();
    }


    /**
    * Load the specified profile files.
    * This method must run on the EDT.
    *
    * @param files
    *      profile files to be loaded
    */
    private void loadProfiles(
            final File[] files)
    {
        if (this.profileLoader == null)
        {
            this.profileLoader = new ProfileLoader(this);
        }

        this.profileLoader.loadProfiles(files);
    }


    /**
    * Add a new account with the specified properties.
    * The account form is made visible for editing.
    *
    * @param properties
    *      account properties for the new account; if null, the default properties
    *      will be applied
    */
    void addNewAccount(
            final Map<Account.Property,Object> properties)
    {
        this.addButton.setEnabled(false);

        /* increment last account ID used */
        int id;

        synchronized (this.lastIdLock)
        {
            id = ++this.lastId;
        }

        /* determine if account form should be displayed for editing */
        boolean editAccount = true;

        if (properties != null)
        {
            if (((Boolean) properties.get(Account.Property.ENABLED)) &&
                (((char[]) properties.get(Account.Property.PASSWORD)).length == 0))
            {
                editAccount = true;
                properties.put(Account.Property.ENABLED, false);
            }
            else
            {
                editAccount = false;
            }
        }

        /* create new account */
        final Account ac = new Account(
                this,
                id,
                (properties == null) ? Account.getNewDefaultProperties() : properties);

        /* add newly created account to table of accounts */
        synchronized (this.accounts)
        {
            this.accounts.add(ac);
        }

        this.accountsTableModel.fireTableDataChanged();
        valueChanged(null);
        refreshUnreadMailCount();

        /* make account form visible for editing, if necessary */
        if (editAccount)
        {
            ac.editAccount();
        }

        this.addButton.setEnabled(true);
    }


    /**
    * Remove the selected accounts.
    */
    private void removeAccounts()
    {
        this.removeButton.setEnabled(false);

        synchronized (this.accounts)
        {
            final List<Account> accountsToRemove = new ArrayList<Account>();

            for (int i : this.accountsTable.getSelectedRows())
            {
                final Account ac = this.accounts.get(this.accountsTable.convertRowIndexToModel(i));
                accountsToRemove.add(ac);
            }

            for (Account ac : accountsToRemove)
            {
                this.accounts.remove(ac);
                ac.removeAccount();
            }
        }

        this.accountsTableModel.fireTableDataChanged();
        refreshUnreadMailCount();
        this.removeButton.setEnabled(true);
    }


    /**
    * Refresh the total number of unread mails for all accounts.
    */
    private void refreshUnreadMailCount()
    {
        int total = 0;
        int totalPopup = 0;
        int totalChime = 0;
        int totalPeriodicBell = 0;
        int totalBlink = 0;

        boolean error = false;

        synchronized (this.accounts)
        {
            for (Account ac : this.accounts)
            {
                final int unreadMails = (Integer) ac.getProperty(Account.Property.UNREAD_MAILS);
                final int n = (unreadMails > 0) ? unreadMails