/**
 * 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.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.filechooser.FileNameExtensionFilter;


/**
 * "Load Profile" form.
 */
class ProfileLoader
        extends JFrame
{
    /** parent GmailAssistant object */
    private final GmailAssistant parent;

    /** file chooser for saving profile file */
    private final JFileChooser fileChooser;

    /** selected profile files to be loaded */
    private volatile File[] files = null;

    /** index of the currently selected profile file to be loaded */
    private int filesIndex = 0;


    /**
    * Constructor.
    *
    * @param parent
    *      parent GmailAssistant object
    */
    ProfileLoader(
            final GmailAssistant parent)
    {
        /*********************
        * INITIALIZE FIELDS *
        *********************/

        this.parent = parent;

        /* get current directory */
        File currentDirectory = new File(".");

        try
        {
            currentDirectory = currentDirectory.getCanonicalFile();
        }
        catch (Exception e)
        {
            /* ignore */
        }

        /* file chooser */
        this.fileChooser = new JFileChooser(currentDirectory);
        this.fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        this.fileChooser.setMultiSelectionEnabled(true);
        this.fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(
                GmailAssistant.NAME + " Profile (*." + GmailAssistant.PROFILE_FILENAME_EXTENSION + ")",
                GmailAssistant.PROFILE_FILENAME_EXTENSION));

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

        initComponents();

        /***************************
        * CONFIGURE FORM SETTINGS *
        ***************************/

        setTitle("Load Profiles - " + this.parent.getTitle());

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                ProfileLoader.this.cancelButton.doClick();
            }
        });

        /* inherit "always on top" behavior of parent */
        try
        {
            setAlwaysOnTop(this.parent.isAlwaysOnTop());
        }
        catch (Exception e)
        {
            /* ignore */
        }

        /* inherit program icon of parent */
        final List<Image> icons = this.parent.getIconImages();

        if (!icons.isEmpty())
        {
            setIconImage(icons.get(0));
        }

        /* field: "Profile Password" */
        this.passwordField.getDocument().addDocumentListener(new DocumentListenerAdapter()
        {
            @Override
            public void insertUpdate(DocumentEvent e)
            {
                checkForm();
            }

            @Override
            public void removeUpdate(DocumentEvent e)
            {
                checkForm();
            }

            @Override
            public void changedUpdate(DocumentEvent e)
            {
                checkForm();
            }
        });

        /* button: "OK" */
        this.okButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                proceedLoadProfile();
            }
        });

        /* button: "Cancel" */
        this.cancelButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                cancelLoadProfile();
            }
        });

        /* add standard editing popup menu to text fields */
        SwingManipulator.addStandardEditingPopupMenu(new JTextField[]
        {
            this.passwordField,
        });

        /* key binding: ENTER key */
        this.scrollPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "ENTER_OK_BUTTON");

        this.scrollPane.getActionMap().put("ENTER_OK_BUTTON", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
                ProfileLoader.this.okButton.doClick();
            }
        });

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

        this.scrollPane.getActionMap().put("ESCAPE_CANCEL_BUTTON", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
                ProfileLoader.this.cancelButton.doClick();
            }
        });

        /* center form on the parent form */
        setLocationRelativeTo(this.parent);
    }


    /**
    * Check selections specified by the user.
    * This method must run on the EDT.
    */
    private void checkForm()
    {
        String error = null;

        if (error != null)
        {
            this.loadError.setText("<html><font color='red'>" + error + "</font></html>");
            this.okButton.setEnabled(false);
            return;
        }

        this.loadError.setText(" ");
        this.okButton.setEnabled(true);
    }


    /**
    * Proceed to load currently selected profile file using the
    * user-specified settings on the form.
    * This method must run on the EDT.
    */
    private void proceedLoadProfile()
    {
        this.okButton.setEnabled(false);
        this.cancelButton.setEnabled(false);

        if (this.files == null)
        {
            this.setVisible(false);
            return;
        }

        File f;

        synchronized (this.files)
        {
            if ((this.filesIndex >= 0) && (this.filesIndex < this.files.length))
            {
                f = this.files[this.filesIndex];
            }
            else
            {
                this.setVisible(false);
                return;
            }
        }

        this.loadError.setText("<html><font color='blue'>Loading profile data...</font></html>");

        /* read encrypted profile data from binary file */
        byte[] salt = new byte[Encryptor.SALT_LENGTH];
        byte[] ciphertextBytes = new byte[0];

        try
        {
            final FileInputStream fis = new FileInputStream(f);
            final int length = (int) f.length();

            if (length < Encryptor.SALT_LENGTH)
            {
                throw new Exception("Invalid profile file length.");
            }

            this.loadError.setText("<html><font color='blue'>Loading profile data: Reading file...</font></html>");

            /* read salt */
            Arrays.fill(salt, (byte) 0x00);
            int offset = 0;

            while (offset < Encryptor.SALT_LENGTH)
            {
                final int n = fis.read(salt, offset, Encryptor.SALT_LENGTH - offset);

                if (n == -1)
                {
                    break;
                }

                offset += n;
            }

            /* read ciphertext */
            ciphertextBytes = new byte[length - Encryptor.SALT_LENGTH];
            Arrays.fill(ciphertextBytes, (byte) 0x00);
            offset = 0;

            while (offset < (length - Encryptor.SALT_LENGTH))
            {
                final int n = fis.read(ciphertextBytes, offset, length - Encryptor.SALT_LENGTH - offset);

                if (n == -1)
                {
                    break;
                }

                offset += n;
            }

            fis.close();
        }
        catch (Exception e)
        {
            Arrays.fill(ciphertextBytes, (byte) 0x00);
            Arrays.fill(salt, (byte) 0x00);

            this.loadError.setText("<html><font color='red'>Failed to load profile data</font></html>");

            SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
                    "Failed to read profile data from file \"" + f.getPath() + "\": " + e +
                    "\nPlease check that the file can be read from.");

            this.okButton.setEnabled(true);
            this.cancelButton.setEnabled(true);
            this.passwordField.selectAll();
            this.passwordField.requestFocus();
            return;
        }

        /* get user-specified password */
        final char[] password = SwingManipulator.getPasswordJPasswordField(this.passwordField);

        /* decrypt profile data */
        byte[] cleartextBytes = new byte[0];
        String[] cleartext = null;

        this.loadError.setText("<html><font color='blue'>Loading profile data: Decrypting file...</font></html>");

        try
        {
            cleartextBytes = Encryptor.decrypt(salt, String.valueOf(password), ciphertextBytes);
            Arrays.fill(password, '\0');
            Arrays.fill(ciphertextBytes, (byte) 0x00);
            Arrays.fill(salt, (byte) 0x00);
            cleartext = Encryptor.byteArrayToStringArray(cleartextBytes, GmailAssistant.PROPERTY_DELIMITER);
            Arrays.fill(cleartextBytes, (byte) 0x00);
        }
        catch (Exception e)
        {
            Arrays.fill(password, '\0');
            Arrays.fill(ciphertextBytes, (byte) 0x00);
            Arrays.fill(salt, (byte) 0x00);
            Arrays.fill(cleartextBytes, (byte) 0x00);

            this.loadError.setText("<html><font color='red'>Failed to load profile data</font></html>");

            SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
                    "Failed to decrypt profile data. Please check that the provided password is correct.");

            this.okButton.setEnabled(true);
            this.cancelButton.setEnabled(true);
            this.passwordField.selectAll();
            this.passwordField.requestFocus();
            return;
        }

        this.loadError.setText("<html><font color='blue'>Loading profile data: Parsing file...</font></html>");

        final List<Map<Account.Property,Object>> allAccountProperties = new ArrayList<Map<Account.Property,Object>>();
        Map<Account.Property,Object> accountProperties = null;
        String username = null;
        final List<String> errors = new ArrayList<String>();

        /* parse each line in the profile file */
        for (String s : cleartext)
        {
            if (s.isEmpty() || s.startsWith("#"))
            {
                /* ignore empty lines and comments */
            }
            else if (s.startsWith("[") && s.endsWith("]"))
            {
                /* start recording properties for a new account */
                username = s.substring(1, s.length() - 1);
                accountProperties = Account.getNewDefaultProperties();
                Account.setProperty(accountProperties, Account.Property.USERNAME, Account.propertyStringToValue(Account.Property.USERNAME, username));
                allAccountProperties.add(accountProperties);
            }
            else if (s.contains(":"))
            {
                if (accountProperties == null)
                {
                    /* program property */
                    final String keyString = s.substring(0, s.indexOf(":")).trim();
                    final GmailAssistant.Property p = GmailAssistant.KEY_STRING_TO_PROPERTY.get(keyString);

                    if (p == null)
                    {
                        errors.add("Failed to recognize program property key \"" + keyString + "\" in \"" + s + "\".");
                    }
                    else
                    {
                        final String valString = s.substring(s.indexOf(":") + 1);
                        final Object val = GmailAssistant.propertyStringToValue(p, valString);

                        if (val == null)
                        {
                            errors.add("Failed to parse program property value \"" + valString + "\" in \"" + s + "\".");
                        }
                        else
                        {
                            this.parent.setProperty(p, val);
                        }
                    }
                }
                else
                {
                    /* account property */
                    final String keyString = s.substring(0, s.indexOf(":")).trim();
                    final Account.Property p = Account.KEY_STRING_TO_PROPERTY.get(keyString);

                    if (p == null)
                    {
                        errors.add("Failed to recognize account property key \"" + keyString + "\" in \"" + s + "\".");
                    }
                    else
                    {
                        final String valString = s.substring(s.indexOf(":") + 1);
                        final Object val = Account.propertyStringToValue(p, valString);

                        if (val == null)
                        {
                            errors.add("Failed to parse account property value \"" + valString + "\" in \"" + s + "\".");
                        }
                        else
                        {
                            Account.setProperty(accountProperties, p, val);
                        }
                    }
                }
            }
        }

        this.loadError.setText("<html><font color='blue'>Loading profile data: Done!</font></html>");

        /* report any errors */
        if (!errors.isEmpty())
        {
            this.loadError.setText("<html><font color='blue'>Loading profile data: Done! (errors encountered)</font></html>");

            final StringBuilder sb = new StringBuilder();

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

            SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
                    GmailAssistant.NAME + " encountered the following " +
                    ((errors.size() == 1) ? "error" : "errors") +
                    " when parsing profile file \"" + f.getPath() + "\":" +
                    sb.toString());
        }

        /* repopulate "Options" form */
        if (this.parent.options != null)
        {
            this.parent.options.repopulateOptions();
        }

        /* add new accounts loaded from the file */
        for (Map<Account.Property,Object> p : allAccountProperties)
        {
            this.parent.addNewAccount(p);
        }

        /* load the next profile file */
        loadNextProfile();
    }


    /**
    * Cancel loading of the currently selected profile file.
    * This method must run on the EDT.
    */
    private void cancelLoadProfile()
    {
        /* load the next profile file */
        loadNextProfile();
    }


    /**
    * Present a file chooser for the user to select the profile files for loading.
    * This method must run on the EDT.
    */
    void showForm()
    {
        final int val = this.fileChooser.showOpenDialog(this);

        if (val == JFileChooser.APPROVE_OPTION)
        {
            this.files = this.fileChooser.getSelectedFiles();

            synchronized (this.files)
            {
                this.filesIndex = -1;
            }

            loadNextProfile();
        }
    }


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

        synchronized (this.files)
        {
            this.filesIndex = -1;
        }

        loadNextProfile();
    }


    /**
    * Load the next profile file that was selected.
    * This method must run on the EDT.
    */
    void loadNextProfile()
    {
        if (this.files == null)
        {
            this.setVisible(false);
            return;
        }

        File f;

        synchronized (this.files)
        {
            while (true)
            {
                this.filesIndex++;

                if ((this.filesIndex >= 0) && (this.filesIndex < this.files.length))
                {
                    f = this.files[this.filesIndex];

                    if (f.isFile())
                    {
                        break;
                    }
                    else
                    {
                        SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
                                "The file \"" + f.getPath() + "\" does not exist.");
                    }
                }
                else
                {
                    this.setVisible(false);
                    return;
                }
            }

            this.loadTitle.setText("Loading profile " +
                ((this.files.length > 1) ?
                    ((this.filesIndex + 1) + " of " + this.files.length) : ""));

            this.loadFilename.setText(f.getPath());
        }

        /* update and show form for user to specify profile password */
        this.passwordField.setText("");
        this.loadError.setText(" ");

        this.okButton.setEnabled(true);
        this.cancelButton.setEnabled(true);
        this.passwordField.selectAll();
        this.passwordField.requestFocus();

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

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

        buttonsPanel = new javax.swing.JPanel();
        okButton = new javax.swing.JButton();
        cancelButton = new javax.swing.JButton();
        loadTitle = new javax.swing.JLabel();
        loadFilename = new javax.swing.JLabel();
        scrollPane = new javax.swing.JScrollPane();
        panel = new javax.swing.JPanel();
        loadPanel = new javax.swing.JPanel();
        passwordLabel = new javax.swing.JLabel();
        passwordField = new javax.swing.JPasswordField();
        lock = new javax.swing.JLabel();
        loadError = new javax.swing.JLabel();

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);

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

        okButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/tick.png"))); // NOI18N
        okButton.setMnemonic('O');
        okButton.setText("OK");
        okButton.setToolTipText("Load profile data from the specified file");
        buttonsPanel.add(okButton);

        cancelButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/cross.png"))); // NOI18N
        cancelButton.setMnemonic('C');
        cancelButton.setText("Cancel");
        cancelButton.setToolTipText("Cancel loading of profile data");
        buttonsPanel.add(cancelButton);

        loadTitle.setText("Loading profile");
        loadTitle.setVerticalAlignment(javax.swing.SwingConstants.TOP);

        loadFilename.setFont(new java.awt.Font("Tahoma", 1, 11));
        loadFilename.setText("C:\\Path\\To\\File");
        loadFilename.setVerticalAlignment(javax.swing.SwingConstants.TOP);

        scrollPane.setBorder(null);
        scrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

        panel.setLayout(new javax.swing.BoxLayout(panel, javax.swing.BoxLayout.Y_AXIS));

        loadPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(""));

        passwordLabel.setDisplayedMnemonic('p');
        passwordLabel.setLabelFor(passwordField);
        passwordLabel.setText("Profile password:");
        passwordLabel.setToolTipText("Password to be used for decrypting the profile data");

        passwordField.setToolTipText("Password to be used for decrypting the profile data");

        lock.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        lock.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/lock.png"))); // NOI18N
        lock.setToolTipText("GmailAssistant encrypts your profile data using AES-128 encryption");

        loadError.setText("<html><font color='red'>load error</font></html>");
        loadError.setVerticalAlignment(javax.swing.SwingConstants.TOP);

        javax.swing.GroupLayout loadPanelLayout = new javax.swing.GroupLayout(loadPanel);
        loadPanel.setLayout(loadPanelLayout);
        loadPanelLayout.setHorizontalGroup(
            loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(loadPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(loadPanelLayout.createSequentialGroup()
                        .addComponent(passwordLabel)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(passwordField, javax.swing.GroupLayout.DEFAULT_SIZE, 165, Short.MAX_VALUE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(lock))
                    .addComponent(loadError, javax.swing.GroupLayout.DEFAULT_SIZE, 272, Short.MAX_VALUE))
                .addContainerGap())
        );
        loadPanelLayout.setVerticalGroup(
            loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(loadPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                        .addComponent(passwordLabel)
                        .addComponent(passwordField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                    .addComponent(lock))
                .addGap(18, 18, 18)
                .addComponent(loadError, javax.swing.GroupLayout.DEFAULT_SIZE, 38, Short.MAX_VALUE))
        );

        panel.add(loadPanel);

        scrollPane.setViewportView(panel);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(scrollPane, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE)
                    .addComponent(buttonsPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE)
                    .addComponent(loadTitle, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE)
                    .addComponent(loadFilename, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(loadTitle)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(loadFilename)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 99, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle<