/**
 * MediaSniper 3.0 (2008-08-02)
 * Copyright 2007 - 2008 Zach Scrivena
 * zachscrivena@gmail.com
 * http://mediasniper.sourceforge.net/
 *
 * Simple program for downloading media files from popular websites.
 *
 * TERMS AND CONDITIONS:
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.freeshell.zs.mediasniper;

import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.Image;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
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.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumnModel;
import javax.swing.text.JTextComponent;
import org.freeshell.zs.common.Debug;
import org.freeshell.zs.common.Downloader;
import org.freeshell.zs.common.ResourceManipulator;
import org.freeshell.zs.common.SimpleProperties;
import org.freeshell.zs.common.StringManipulator;
import org.freeshell.zs.common.SwingManipulator;
import org.freeshell.zs.common.TerminatingException;


/**
 * Simple program for downloading media files from popular websites.
 */
class MediaSniper
        extends JFrame
{
    /** default program properties file */
    private static final String DEFAULT_PROGRAM_PROPERTIES =
            "/org/freeshell/zs/mediasniper/resources/properties.txt";

    /** refresh interval, in milliseconds */
    private static final long REFRESH_INTERVAL_MILLISECONDS = 100L;

    /* thread pool for webpage parsers */
    private final ExecutorService parsersThreadPool =
            Executors.newCachedThreadPool();

    /* thread pool for media file downloads */
    private final ExecutorService downloadsThreadPool;

    /** webpage parsers */
    private final HashSet<WebpageParser> parsers = new HashSet<WebpageParser>();

    /** media file downloads */
    private final List<Download> downloads = new ArrayList<Download>();

    /** table model for table of downloads */
    private final DownloadsTableModel downloadsTableModel = new DownloadsTableModel();

    /** program properties */
    final SimpleProperties properties;

    /** definitions for parsing webpages */
    volatile List<Definition> definitions = null;

    /** local filename manager */
    final LocalFilenameManager localFilenameManager = new LocalFilenameManager();

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

    /** tray icon for program */
    private TrayIcon trayIcon = null;

    /** perform periodic refresh of the status label? */
    volatile private boolean refreshStatus = true;


    /**
    * Main entry point for the program.
    *
    * @param args
    *     command-line arguments
    */
    public static void main(
            final String args[])
    {
        /************************************
        * SCHEDULE GUI CREATION ON THE EDT *
        ************************************/

        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                try
                {
                    /* use system look and feel if possible */
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                }
                catch (Exception e)
                {
                    /* ignore */
                }

                try
                {
                    final MediaSniper ms = new MediaSniper(args);
                    ms.setVisible(true);
                }
                catch (TerminatingException e)
                {
                    SwingManipulator.showErrorDialog(
                            null,
                            "Initialization Error",
                            e.getMessage() +
                            "\nPlease see Help > Usage for more information.");

                    System.exit(1);
                }
                catch (Exception e)
                {
                    SwingManipulator.showErrorDialog(
                            null,
                            "Initialization Error",
                            "Failed to initialize program because of an unexpected error: " + e +
                            "\nPlease file a bug report to help improve this program." +
                            "\n\n" + Debug.getSystemInformationString() +
                            "\n\n" + Debug.getStackTraceString(e));

                    System.exit(1);
                }
            }
        });
    }


    /**
    * Constructor.
    *
    * @param args
    *     command-line arguments
    */
    MediaSniper(
            final String[] args)
    {
        /*********************************
        * INITIALIZE PROGRAM PROPERTIES *
        *********************************/

        /* read default program properties */
        try
        {
            properties = new SimpleProperties(ResourceManipulator.resourceAsString(MediaSniper.DEFAULT_PROGRAM_PROPERTIES));
        }
        catch (Exception ex)
        {
            throw new TerminatingException("(INTERNAL) Failed to load default program properties (" + ex + ").");
        }

        /* read user-editable program properties */
        final SimpleProperties editableProperties;

        try
        {
            editableProperties = new SimpleProperties(ResourceManipulator.resourceAsString(properties.getString("editable.properties")));
        }
        catch (Exception ex)
        {
            throw new TerminatingException("(INTERNAL) Failed to load user-editable program properties (" + ex + ").");
        }

        /* add user-specified properties */
        for (String s : args)
        {
            final String[] kv = StringManipulator.parseKeyValueString(s);

            if (editableProperties.get(kv[0]) == null)
            {
                /* not a valid user-editable program property */
                if (properties.get(kv[0]) == null)
                {
                    throw new TerminatingException("\"" + kv[0] + "\" is not a valid program property.");
                }
                else
                {
                    throw new TerminatingException("\"" + kv[0] + "\" is not a user-editable program property.");
                }
            }
            else
            {
                properties.set(kv[0], kv[1]);
            }
        }

        downloadsThreadPool = Executors.newFixedThreadPool(properties.getInt("max.concurrent.downloads"));

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

        initComponents();

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

        setTitle(properties.getString("name"));
        setAlwaysOnTop(properties.getBoolean("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: "Exit" */
        exitItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                closeProgram();
            }
        });

        /* menu item: "Always on Top" */
        topItem.setSelected(properties.getBoolean("always.on.top"));
        topItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                properties.setBoolean("always.on.top", topItem.isSelected());
                refreshAlwaysOnTopMode();
            }
        });

        /* menu item: "Minimize to Tray" */
        trayItem.setSelected(properties.getBoolean("minimize.to.tray"));
        trayItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                properties.setBoolean("minimize.to.tray", trayItem.isSelected());
            }
        });

        /* menu item: "Restricted ASCII Filenames" */
        restrictedItem.setSelected(properties.getBoolean("restricted.ascii.filenames"));
        restrictedItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                properties.setBoolean("restricted.ascii.filenames", restrictedItem.isSelected());
            }
        });

        /* menu item: "Short (8.3) Filenames" */
        shortItem.setSelected(properties.getBoolean("short.filenames"));
        shortItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                properties.setBoolean("short.filenames", shortItem.isSelected());
            }
        });

        /* menu item: "SOCKS Proxy" */
        proxyItem.setSelected(properties.getBoolean("proxy"));
        proxyItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                properties.setBoolean("proxy", proxyItem.isSelected());
                refreshProxyMode();
            }
        });

        /* menu item: "Usage..." */
        usageItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                /* list user-editable program properties and their current values */
                final List<String> lines = new ArrayList<String>();

                synchronized (editableProperties)
                {
                    for (String s : editableProperties.keySet())
                    {
                        lines.add("* " + s + " - " +
                                editableProperties.getString(s)
                                + " (\"" + properties.getAsString(s) + "\")");
                    }
                }

                Collections.sort(lines);
                final StringBuilder sb = new StringBuilder();

                for (String s : lines)
                {
                    sb.append("\n");
                    sb.append(s);
                }

                try
                {
                    SwingManipulator.showInfoDialog(
                            MediaSniper.this,
                            "Usage - " + properties.getString("name"),
                            "Usage information for " + properties.getString("name"),
                            ResourceManipulator.resourceAsString(properties.getString("usage")) +
                            sb.toString(),
                            10);
                }
                catch (Exception ee)
                {
                    SwingManipulator.showErrorDialog(
                            MediaSniper.this,
                            getTitle(),
                            "(INTERNAL) Failed to read \"Usage\" text from JAR file.");
                }
            }
        });

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

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

        /* menu item: "Check for Update" */
        checkItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                new Thread(new Runnable()
                {
                    public void run()
                    {
                        checkForUpdate(true, true);
                    }
                }).start();
            }
        });

        /* webpage URL text area */
        SwingManipulator.addStandardEditingPopupMenu(new JTextComponent[] {urlText});

        /* button: "Go!" */
        goButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                urlText.requestFocus();
                parseWebpage();
            }
        });

        /* button: "Paste and Go!" */
        pasteButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                urlText.selectAll();
                urlText.paste();
                urlText.requestFocus();
                parseWebpage();
            }
        });

        /* button: "Clear" */
        clearButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                urlText.setText("");
                urlText.requestFocus();
            }
        });

        /* table of downloads */
        downloadsTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
        final TableColumnModel colModel = downloadsTable.getColumnModel();
        colModel.getColumn(0).setMinWidth(0);
        colModel.getColumn(1).setMinWidth(0);
        colModel.getColumn(2).setMinWidth(0);
        colModel.getColumn(0).setPreferredWidth(80);
        colModel.getColumn(1).setPreferredWidth(10);
        colModel.getColumn(2).setPreferredWidth(40);

        downloadsTable.setToolTipText("Mouse-over any row for more information");

        downloadsTable.setDefaultRenderer(TableStringCell.class, new TableStringCellRenderer(
                downloadsTable.getForeground(),
                downloadsTable.getBackground(),
                downloadsTable.getSelectionForeground(),
                downloadsTable.getSelectionBackground()));

        downloadsTable.setDefaultRenderer(TableProgressCell.class, new TableProgressCellRenderer());

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

        final Image iconImage;

        try
        {
            iconImage = ImageIO.read(MediaSniper.class.getResource("/org/freeshell/zs/mediasniper/resources/icon.png"));
        }
        catch (Exception e)
        {
            throw new TerminatingException("Failed to load program icon image.");
        }

        /* set program icon */
        setIconImage(iconImage);

        /* setup tray icon */
        try
        {
            trayIcon = new TrayIcon(iconImage, properties.getString("name") + " (click to restore)");
            trayIcon.setImageAutoSize(true);

            trayIcon.addActionListener(new ActionListener()
            {
                @Override
                public void actionPerformed(ActionEvent e)
                {
                    restoreProgram();
                }
            });

            trayIcon.addMouseListener(new MouseAdapter()
            {
                @Override
                public void mouseClicked(MouseEvent e)
                {
                    restoreProgram();
                }
            });
        }
        catch (Exception e)
        {
            trayIcon = null;
        }

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

        /*********************************************
        * START CHECK FOR UPDATE & LOAD DEFINITIONS *
        *********************************************/

        new Thread(new Runnable()
        {
            public void run()
            {
                checkForUpdate(false, false);
                loadDefinitions();
            }
        }).start();

        /***********************************************
        * START MONITORING NUMBER OF ACTIVE DOWNLOADS *
        ***********************************************/

        new Thread(new Runnable()
        {
            public void run()
            {
                while (true)
                {
                    if (refreshStatus)
                    {
                        int numActiveDownloads = 0;

                        synchronized (downloads)
                        {
                            for (Download d : downloads)
                            {
                                if (!d.isCompleted())
                                {
                                    numActiveDownloads++;
                                }
                            }
                        }

                        final String statusString;

                        if (numActiveDownloads == 0)
                        {
                            statusString = "No active downloads";
                        }
                        else if (numActiveDownloads == 1)
                        {
                            statusString = "1 active download";
                        }
                        else
                        {
                            statusString = numActiveDownloads + " active downloads";
                        }

                        SwingManipulator.updateLabel(status, statusString);
                    }

                    Debug.sleep(10 * REFRESH_INTERVAL_MILLISECONDS);
                }
            }
        }).start();

        /****************************
        * REFRESH PROGRAM SETTINGS *
        ****************************/

        refreshAlwaysOnTopMode();
        refreshProxyMode();
        refreshDownloadDirectory();
    }


    /**
    * Check for update.
    * This method should run on a dedicated worker thread, not the EDT.
    *
    * @param showPromptIfLatest
    *     should a user prompt be displayed even if the current program is the latest release?
    * @param showPromptOnError
    *     should a user prompt be displayed if an error occurs?
    */
    private void checkForUpdate(
            final boolean showPromptIfLatest,
            final boolean showPromptOnError)
    {
        refreshStatus = false;
        SwingManipulator.updateLabel(status, "Checking for update...");

        try
        {
            /* try to retrieve latest defintions from homepage */
            final StringBuilder sb = new StringBuilder();

            final Downloader d = new Downloader(
                    new URL(properties.getString("update.url")),
                    sb);

            new Thread(d).start();

            while (true)
            {
                if (d.isProgressUpdated())
                {
                    final int percent = d.getProgressPercent();

                    SwingManipulator.updateLabel(
                            status,
                            "Checking for update: " + d.getProgressString() +
                                    ((percent >= 0) ? " (" + percent + "%)" : ""));
                }

                if (d.isCompleted())
                {
                    d.waitUntilCompleted();
                    break;
                }

                Debug.sleep(REFRESH_INTERVAL_MILLISECONDS);
            }

            final String[] lines;

            synchronized (sb)
            {
                lines = sb.toString().split("[\n\r\u0085\u2028\u2028]++");
            }

            /* process update information */
            SwingManipulator.updateLabel(status, "Checking for update...");

            for (String s : lines)
            {
                if (s.isEmpty() || s.startsWith("#"))
                {
                    /* ignore empty lines and comments */
                }
                else if (s.contains(":"))
                {
                    /* process "key:value" pair */
                    final String[] kv = StringManipulator.parseKeyValueString(s);
                    properties.setString(kv[0], kv[1]);
                }
            }
        }
        catch (Exception e)
        {
            if (showPromptOnError)
            {
                SwingManipulator.showErrorDialog(
                        this,
                        "Check for Update - " + getTitle(),
                        "Failed to retrieve update information from the " + properties.getString("name") +
                        " homepage (" + e + ").\nPlease try again later. If the problem persists, please visit the homepage manually.");
            }

            SwingManipulator.updateLabel(status, " ");
            refreshStatus = true;
            return;
        }

        /* show user prompt if an update is available */
        try
        {
            final String version = properties.getString("update.version");
            final String date = properties.getString("update.date");
            final String download = properties.getString("update.download");
            final String comment = properties.getString("update.comment");

            if (properties.getString("date").compareTo(date) < 0)
            {
                /* new release is available */
                final int choice = SwingManipulator.showOptionTextDialog(
                        this,
                        "A new release of " + properties.getString("name") + " is available",
                        properties.getString("name") + " " + version + " (" + date +
                        ") is available for download. " +
                        ((Double.parseDouble(properties.getString("version")) < Double.parseDouble(version)) ?
                        "This is a major update and is strongly recommended." :
                        "This is a minor update and is recommended if you are currently experiencing problems.") +
                        "\n\n" + comment,
                        8,
                        "Check for Update - " + getTitle(),
                        JOptionPane.DEFAULT_OPTION,
                        JOptionPane.INFORMATION_MESSAGE,
                        null,
                        new String[] {"Download Now", "Cancel"},
                        0);

                /* download now */
                if ((choice == 0) &&
                        Desktop.isDesktopSupported())
                {
                    try
                    {
                        Desktop.getDesktop().browse(new URI(download));
                    }
                    catch (Exception ex)
                    {
                        /* ignore */
                    }
                }
            }
            else
            {
                /* this release is up-to-date */
                if (showPromptIfLatest)
                {
                    SwingManipulator.showInfoDialog(
                            this,
                            "Check for Update - " + getTitle(),
                            "No update found",
                            "This release of " + properties.getString("name") + " " + properties.getString("version") + " (" +
                            properties.getString("date") + ") is already up-to-date.",
                            5);
                }
            }
        }
        catch (Exception e)
        {
            /* unable to parse */
            if (showPromptOnError)
            {
                SwingManipulator.showWarningDialog(
                        this,
                        "Check for Update - " + getTitle(),
                        "Failed to parse update information from the " + properties.getString("name") +
                        " homepage (" + e + ").\nPlease try again later. If the problem persists, please visit the homepage manually.");
            }
        }

        SwingManipulator.updateLabel(status, " ");
        refreshStatus = true;
    }


    /**
    * Load definitions, from the homepage if possible.
    * This method should run on a dedicated worker thread, not the EDT.
    */
    private void loadDefinitions()
    {
        refreshStatus = false;
        SwingManipulator.updateLabel(status, "Loading definitions...");
        String content = null;

        try
        {
            /* try to retrieve latest defintions from homepage */
            final StringBuilder sb = new StringBuilder();

            final Downloader d = new Downloader(
                    new URL(properties.getString("definitions.url")),
                    sb);

            new Thread(d).start();

            while (true)
            {
                if (d.isProgressUpdated())
                {
                    final int percent = d.getProgressPercent();

                    SwingManipulator.updateLabel(
                            status,
                            "Loading definitions: " + d.getProgressString() +
                                    ((percent >= 0) ? " (" + percent + "%)" : ""));
                }

                if (d.isCompleted())
                {
                    d.waitUntilCompleted();
                    break;
                }

                Debug.sleep(REFRESH_INTERVAL_MILLISECONDS);
            }

            synchronized (sb)
            {
                content = sb.toString();
            }
        }
        catch (Exception e)
        {
            SwingManipulator.showWarningDialog(
                    this,
                    "Loading Definitions - " + getTitle(),
                    "Failed to retrieve the latest definitions from the " + properties.getString("name") +
                    " webpage (" + e + ").\nBuilt-in definitions will be used instead.");

            try
            {
                /* try to read built-in defintions */
                content = ResourceManipulator.resourceAsString(properties.getString("definitions.local"));
            }
            catch (Exception ex)
            {
                SwingManipulator.showErrorDialog(
                    this,
                    "Loading Definitions - " + getTitle(),
                    "(INTERNAL) Failed to read built-in definitions (" + ex + ").\nProgram terminated.");

                System.exit(1);
            }
        }

        final String[] lines = content.split("[\n\r\u0085\u2028\u2028]++");

        /* process definitions */
        SwingManipulator.updateLabel(status, "Loading definitions...");
        final List<Definition> defs = new ArrayList<Definition>();
        Definition current = null;

        for (String s : lines)
        {
            if (s.isEmpty() || s.startsWith("#"))
            {
                /* ignore empty lines and comments */
            }
            else if (s.startsWith("[") && s.endsWith("]"))
            {
                /* start collecting mappings for a new definition */
                current = new Definition(s.substring(1, s.length() - 1).trim());
                defs.add(current);
            }
            else if (s.contains(":"))
            {
                /* process "key:value" pair */
                final String[] kv = StringManipulator.parseKeyValueString(s);

                if (current == null)
                {
                    /* meta-data about the definitions */
                    properties.setString(kv[0], kv[1]);
                }
                else
                {
                    /* add mapping to current definition */
                    current.put(kv[0], kv[1]);
                }
            }
        }

        definitions = defs;
        SwingManipulator.updateLabel(status, " ");
        refreshStatus = true;
    }


    /**
    * Parse the displayed webpage URL.
    * This method must run on the EDT.
    */
    private void parseWebpage()
    {
        final String u = urlText.getText().replace("\n", "").trim();

        if (u.isEmpty())
        {
            return;
        }

        final URL url;

        try
        {
            if (u.contains("://"))
            {
                url = new URL(u);
            }
            else
            {
                /* assume HTTP protocol */
                url = new URL("http://" + u);
            }
        }
        catch (Exception e)
        {
            /* invalid URL */
            SwingManipulator.showErrorDialog(
                    this,
                    properties.getString("name"),
                    "Invalid webpage URL.");

            return;
        }

        try
        {
            /* start new thread for webpage parser */
            final WebpageParser parser = new WebpageParser(this, url);
            parser.setVisible(true);
            parser.toFront();

            parsersThreadPool.submit(parser);
            urlText.setText("");
        }
        catch (UnsupportedOperationException e)
        {
            SwingManipulator.showErrorDialog(
                    this,
                    properties.getString("name"),
                    properties.getString("name") + " does not know how to parse this webpage.");
        }
    }


    /**
    * Close the program.
    */
    private void closeProgram()
    {
        /* count number of active downloads */
        int numActiveDownloads = 0;

        synchronized (downloads)
        {
            for (Download d : downloads)
            {
                if (!d.isCompleted())
                {
                    numActiveDownloads++;
                }
            }
        }

        if (numActiveDownloads > 0)
        {
            /* prompt user about active downloads */
            final int choice = JOptionPane.showConfirmDialog(
                    MediaSniper.this,
                    "There" +
                    ((numActiveDownloads == 1) ? " is" : " are") +
                    " still " + numActiveDownloads + " active" +
                    ((numActiveDownloads == 1) ? " download" : " downloads") +
                    ". Exit now?",
                    "Confirm Exit - " + properties.getString("name"),
                    JOptionPane.YES_NO_OPTION,
                    JOptionPane.WARNING_MESSAGE);

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

        /* count number of active parsers */
        final int numActiveParsers;

        synchronized (parsers)
        {
            numActiveParsers = parsers.size();
        }

        if (numActiveParsers > 0)
        {
            /* prompt user about active parsers */
            final int choice = JOptionPane.showConfirmDialog(
                    MediaSniper.this,
                    "There" +
                    ((numActiveParsers == 1) ? " is" : " are") +
                    " still " + numActiveParsers + " active webpage" +
                    ((numActiveParsers == 1) ? " parser" : " parsers") +
                    ". Exit now?",
                    "Confirm Exit - " + properties.getString("name"),
                    JOptionPane.YES_NO_OPTION,
                    JOptionPane.WARNING_MESSAGE);

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

        setVisible(false);
        dispose();
        System.exit(0);
    }


    /**
    * Minimize the program, to the tray if necessary.
    * This method must run on the EDT.
    */
    private void minimizeProgram()
    {
        if (properties.getBoolean("minimize.to.tray") &&
                (trayIcon != null))
        {
            try
            {
                SystemTray.getSystemTray().add(trayIcon);
                setVisible(false);
            }
            catch (Exception e)
            {
                /* ignore */
            }
        }

        setExtendedState(JFrame.ICONIFIED);

        final JFrame[] frames = new JFrame[]
        {
            about
        };

        for (JFrame f : frames)
        {
            if (f != null)
            {
                f.setVisible(false);
            }
        }

        /* minimize frame of webpage parsers */
        synchronized (parsers)
        {
            for (WebpageParser parser : parsers)
            {
                parser.setVisible(false);
            }
        }
    }


    /**
    * Restore the program from the tray.
    * This method must run on the EDT.
    */
    private void restoreProgram()
    {
        setVisible(true);

        /* restore frame of opened webpage parsers */
        synchronized (parsers)
        {
            for (WebpageParser parser : parsers)
            {
                parser.setVisible(true);
            }
        }

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

        setExtendedState(JFrame.NORMAL);
        toFront();
    }


    /**
    * Refresh the specified download on the table.
    *
    * @param d
    *     download to be refreshed
    */
    void refreshDownloadOnTable(
            final Download d)
    {
        synchronized (downloads)
        {
            final int i = downloads.indexOf(d);

            if (i >= 0)
            {
                downloadsTableModel.fireTableRowsUpdated(i, i);
            }
        }
    }


    /**
    * Add the specified download.
    *
    * @param d
    *     download to be added
    */
    void addDownload(
            final Download d)
    {
        synchronized (downloads)
        {
            downloads.add(d);
        }

        downloadsThreadPool.submit(d);
        downloadsTableModel.fireTableDataChanged();
    }


    /**
    * Add the specified webpage parser.
    *
    * @param p
    *     parser to be added
    */
    void addParser(
            final WebpageParser p)
    {
        synchronized (parsers)
        {
            parsers.add(p);
        }
    }


    /**
    * Remove the specified webpage parser.
    *
    * @param p
    *     parser to be removed
    */
    void removeParser(
            final WebpageParser p)
    {
        synchronized (parsers)
        {
            parsers.remove(p);
        }
    }


    /**
    * Refresh the "Always on Top" mode of the program.
    * This method must run on the EDT.
    */
    private void refreshAlwaysOnTopMode()
    {
        final boolean state = properties.getBoolean("always.on.top");

        final JFrame[] frames = new JFrame[]
        {
            this,
            about
        };

        for (JFrame f : frames)
        {
            if (f != null)
            {
                try
                {
                    f.setAlwaysOnTop(state);
                }
                catch (Exception ee)
                {
                    /* ignore */
                }
            }
        }

        synchronized (parsers)
        {
            /* propagate mode to other forms */
            for (WebpageParser p : parsers)
            {
                try
                {
                    p.setAlwaysOnTop(state);
                }
                catch (Exception ee)
                {
                    /* ignore */
                }
            }
        }
    }


    /**
    * Refresh SOCKS proxy mode.
    */
    private void refreshProxyMode()
    {
        if (properties.getBoolean("proxy"))
        {
            Debug.setSystemProperty("socksProxyHost", properties.getString("proxy.host"));
            Debug.setSystemProperty("socksProxyPort", properties.getString("proxy.port"));
            Debug.setSystemProperty("java.net.socks.username", properties.getString("proxy.username"));
            Debug.setSystemProperty("java.net.socks.password", properties.getString("proxy.password"));
        }
        else
        {
            Debug.setSystemProperty("socksProxyHost", "");
            Debug.setSystemProperty("socksProxyPort", "");
            Debug.setSystemProperty("java.net.socks.username", "");
            Debug.setSystemProperty("java.net.socks.password", "");
        }
    }


    /**
    * Refresh download directory.
    */
    private void refreshDownloadDirectory()
    {
        try
        {
            properties.setFile("download.directory", properties.getFile("download.directory").getCanonicalFile());
        }
        catch (Exception e)
        {
            try
            {
                properties.setFile("download.directory", new File(".").getCanonicalFile());
            }
            catch (Exception ex)
            {
                try
                {
                    properties.setFile("download.directory", new File(System.getProperty("user.dir")).getCanonicalFile());
                }
                catch (Exception exx)
                {
                    /* ignore */
                }
            }
        }
    }

    /*****************
    * INNER CLASSES *
    *****************/

    /**
    * Represent the model for the table of media file downloads.
    */
    private class DownloadsTableModel
            extends AbstractTableModel
    {
        /** name of each column (headers) */
        private final String[] columnNames =
        {
            "Title",
            "Size",
            "Progress"
        };

        /** class of each column */
        private final Class[] columnClasses =
        {
            TableStringCell.class,
            TableStringCell.class,
            TableProgressCell.class
        };

        /** cached copy of the table cell contents, for rendering */
        private final Map<Download,Object[]> contents = new HashMap<Download,Object[]>();

        @Override
        public int getRowCount()
        {
            synchronized (downloads)
            {
                return downloads.size();
            }
        }

        @Override
        public int getColumnCount()
        {
            return columnNames.length;
        }

        @Override
        public String getColumnName(
                int col)
        {
            return columnNames[col];
        }

        @Override
        public Class getColumnClass(
                int col)
        {
            return columnClasses[col];
        }

        @Override
        public Object getValueAt(
                int row,
                int col)
        {
            Download d;

            synchronized (downloads)
            {
                d = downloads.get(row);
            }

            Object[] c = contents.get(d);

            if (c == null)
            {
                c = new Object[]
                {
                    new TableStringCell(d),
                    new TableStringCell(d),
                    new TableProgressCell(d)
                };

                contents.put(d, c);

                /* initialize invariant properties of the cell contents */

                /* Title */
                ((TableStringCell) c[0]).align = JLabel.LEFT;

                /* Size */
                ((TableStringCell) c[1]).align = JLabel.RIGHT;
            }

            switch (col)
            {
                case 0:
                    /* Title */
                    ((TableStringCell) c[0]).text = "<html>" + d.getTitle() + "</html>";
                    break;

                case 1:
                    /* Size */
                    ((TableStringCell) c[1]).value = d.getLength();
                    ((TableStringCell) c[1]).text = d.getLengthString();
                    break;

                case 2:
                    /* Progress */
                    ((TableProgressCell) c[2]).percent = d.getProgressPercent();
                    ((TableProgressCell) c[2]).text = d.getProgressString();
                    break;

                default:
                    return null;
            }

            return c[col];
        }
    }

    /***************************
    * 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.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        status = new javax.swing.JLabel();
        urlPanel = new javax.swing.JPanel();
        buttonsPanel = new javax.swing.JPanel();
        goButton = new javax.swing.JButton();
        pasteButton = new javax.swing.JButton();
        clearButton = new javax.swing.JButton();
        urlPane = new javax.swing.JScrollPane();
        urlText = new javax.swing.JTextArea();
        downloadsPanel = new javax.swing.JPanel();
        downloadsPane = new javax.swing.JScrollPane();
        downloadsTable = new javax.swing.JTable();
        menuBar = new javax.swing.JMenuBar();
        fileMenu = new javax.swing.JMenu();
        exitItem = new javax.swing.JMenuItem();
        optionsMenu = new javax.swing.JMenu();
        topItem = new javax.swing.JCheckBoxMenuItem();
        trayItem = new javax.swing.JCheckBoxMenuItem();
        optionsSeparator = new javax.swing.JSeparator();
        restrictedItem = new javax.swing.JCheckBoxMenuItem();
        shortItem = new javax.swing.JCheckBoxMenuItem();
        proxyItem = new javax.swing.JCheckBoxMenuItem();
        helpMenu = new javax.swing.JMenu();
        usageItem = new javax.swing.JMenuItem();
        aboutItem = new javax.swing.JMenuItem();
        checkItem = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);

        status.setText(" ");
        getContentPane().add(status, java.awt.BorderLayout.PAGE_END);

        urlPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Webpage URL"));
        urlPanel.setLayout(new java.awt.BorderLayout());

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

        goButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/mediasniper/resources/wand.png"))); // NOI18N
        goButton.setMnemonic('g');
        goButton.setText("Go!");
        goButton.setToolTipText("Parse this webpage URL");
        goButton.setHorizontalAlignment(javax.swing.SwingConstants.LEADING);
        buttonsPanel.add(goButton);

        pasteButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/mediasniper/resources/page_paste.png"))); // NOI18N
        pasteButton.setMnemonic('p');
        pasteButton.setText("Paste and Go!");
        pasteButton.setToolTipText("Paste the webpage URL from the clipboard and parse it");
        pasteButton.setHorizontalAlignment(javax.swing.SwingConstants.LEADING);
        buttonsPanel.add(pasteButton);

        clearButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/mediasniper/resources/bin_closed.png"))); // NOI18N
        clearButton.setMnemonic('c');
        clearButton.setText("Clear");
        clearButton.setToolTipText("Clear this webpage URL");
        clearButton.setHorizontalAlignment(javax.swing.SwingConstants.LEADING);
        buttonsPanel.add(clearButton);

        urlPanel.add(buttonsPanel, java.awt.BorderLayout.LINE_END);

        urlPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        urlText.setColumns(20);
        urlText.setLineWrap(true);
        urlText.setRows(5);
        urlPane.setViewportView(urlText);

        urlPanel.add(urlPane, java.awt.BorderLayout.CENTER);

        getContentPane().add(urlPanel, java.awt.BorderLayout.PAGE_START);

        downloadsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Media File Downloads"));

        downloadsPane.setPreferredSize(new java.awt.Dimension(500, 250));

        downloadsTable.setAutoCreateRowSorter(true);
        downloadsTable.setModel(downloadsTableModel);
        downloadsPane.setViewportView(downloadsTable);

        javax.swing.GroupLayout downloadsPanelLayout = new javax.swing.GroupLayout(downloadsPanel);
        downloadsPanel.setLayout(downloadsPanelLayout);
        downloadsPanelLayout.setHorizontalGroup(
            downloadsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(downloadsPane, javax.swing.GroupLayout.DEFAULT_SIZE, 474, Short.MAX_VALUE)
        );
        downloadsPanelLayout.setVerticalGroup(
            downloadsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(downloadsPane, javax.swing.GroupLayout.DEFAULT_SIZE, 195, Short.MAX_VALUE)
        );

        getContentPane().add(downloadsPanel, java.awt.BorderLayout.CENTER);

        fileMenu.setMnemonic('f');
        fileMenu.setText("File");

        exitItem.setMnemonic('x');
        exitItem.setText("Exit");
        fileMenu.add(exitItem);

        menuBar.add(fileMenu);

        optionsMenu.setMnemonic('o');
        optionsMenu.setText("Options");

        topItem.setMnemonic('t');
        topItem.setSelected(true);
        topItem.setText("Always on Top");
        optionsMenu.add(topItem);

        trayItem.setMnemonic('m');
        trayItem.setSelected(true);
        trayItem.setText("Minimize to Tray");
        optionsMenu.add(trayItem);
        optionsMenu.add(optionsSeparator);

        restrictedItem.setMnemonic('r');
        restrictedItem.setSelected(true);
        restrictedItem.setText("Restricted ASCII Filenames");
        optionsMenu.add(restrictedItem);

        shortItem.setMnemonic('s');
        shortItem.setSelected(true);
        shortItem.setText("Short (8.3) Filenames");
        optionsMenu.add(shortItem);

        proxyItem.setSelected(true);
        proxyItem.setText("SOCKS Proxy");
        optionsMenu.add(proxyItem);

        menuBar.add(optionsMenu);

        helpMenu.setMnemonic('h');
        helpMenu.setText("Help");

        usageItem.setMnemonic('u');
        usageItem.setText("Usage...");
        helpMenu.add(usageItem);

        aboutItem.setMnemonic('a');
        aboutItem.setText("About...");
        helpMenu.add(aboutItem);

        checkItem.setMnemonic('c');
        checkItem.setText("Check for Update");
        helpMenu.add(checkItem);

        menuBar.add(helpMenu);

        setJMenuBar(menuBar);

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

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JMenuItem aboutItem;
    private javax.swing.JPanel buttonsPanel;
    private javax.swing.JMenuItem checkItem;
    private javax.swing.JButton clearButton;
    private javax.swing.JScrollPane downloadsPane;
    private javax.swing.JPanel downloadsPanel;
    private javax.swing.JTable downloadsTable;
    private javax.swing.JMenuItem exitItem;
    private javax.swing.JMenu fileMenu;
    private javax.swing.JButton goButton;
    private javax.swing.JMenu helpMenu;
    private javax.swing.JMenuBar menuBar;
    private javax.swing.JMenu optionsMenu;
    private javax.swing.JSeparator optionsSeparator;
    private javax.swing.JButton pasteButton;
    private javax.swing.JCheckBoxMenuItem proxyItem;
    private javax.swing.JCheckBoxMenuItem restrictedItem;
    private javax.swing.JCheckBoxMenuItem shortItem;
    private javax.swing.JLabel status;
    private javax.swing.JCheckBoxMenuItem topItem;
    private javax.swing.JCheckBoxMenuItem trayItem;
    private javax.swing.JScrollPane urlPane;
    private javax.swing.JPanel urlPanel;
    private javax.swing.JTextArea urlText;
    private javax.swing.JMenuItem usageItem;
    // End of variables declaration//GEN-END:variables
}