/* CertificatePane.java
   Copyright (C) 2008 Red Hat, Inc.

This file is part of IcedTea.

IcedTea 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, version 2.

IcedTea 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 IcedTea; see the file COPYING.  If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version.
*/

package net.sourceforge.jnlp.security.viewer;

import static net.sourceforge.jnlp.runtime.Translator.R;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.DefaultTableModel;

import net.sourceforge.jnlp.security.CertificateUtils;
import net.sourceforge.jnlp.security.KeyStores;
import net.sourceforge.jnlp.security.SecurityUtil;
import net.sourceforge.jnlp.security.SecurityDialog;
import net.sourceforge.jnlp.security.KeyStores.Level;
import net.sourceforge.jnlp.util.FileUtils;
import net.sourceforge.jnlp.util.logging.OutputController;

public class CertificatePane extends JPanel {

    /**
     * The certificates stored in the certificates file.
     */
    private ArrayList<X509Certificate> certs = null;

    private static final Dimension TABLE_DIMENSION = new Dimension(500, 200);

    /**
     * "Issued To" and "Issued By" string pairs for certs.
     */
    private String[][] issuedToAndBy = null;
    private final String[] columnNames = { R("CVIssuedTo"), R("CVIssuedBy") };

    private final CertificateType[] certificateTypes = new CertificateType[] {
            new CertificateType(KeyStores.Type.CA_CERTS),
            new CertificateType(KeyStores.Type.JSSE_CA_CERTS),
            new CertificateType(KeyStores.Type.CERTS),
            new CertificateType(KeyStores.Type.JSSE_CERTS),
            new CertificateType(KeyStores.Type.CLIENT_CERTS)
        };

    JTabbedPane tabbedPane;
    private final JTable userTable;
    private final JTable systemTable;
    private JComboBox certificateTypeCombo;
    private KeyStores.Type currentKeyStoreType;
    private KeyStores.Level currentKeyStoreLevel;

    /** JComponents that should be disbled for system store */
    private final List<JComponent> disableForSystem;

    private JDialog parent;
    private JComponent defaultFocusComponent = null;

    /**
     * The Current KeyStore. Only one table/tab is visible for interaction to
     * the user. This KeyStore corresponds to that.
     */
    private KeyStore keyStore = null;

    public CertificatePane(JDialog parent) {
        super();
        this.parent = parent;

        userTable = new JTable(null);
        systemTable = new JTable(null);
        disableForSystem = new ArrayList<JComponent>();

        addComponents();

        currentKeyStoreType = ((CertificateType) (certificateTypeCombo.getSelectedItem())).getType();
        if (tabbedPane.getSelectedIndex() == 0) {
            currentKeyStoreLevel = Level.USER;
        } else {
            currentKeyStoreLevel = Level.SYSTEM;
        }

        repopulateTables();
    }

    /**
     * Reads the user's trusted.cacerts keystore.
     */
    private void initializeKeyStore() {
        try {
            keyStore = KeyStores.getKeyStore(currentKeyStoreLevel, currentKeyStoreType);
        } catch (Exception e) {
            OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
        }
    }

    //create the GUI here.
    private void addComponents() {

        JPanel main = new JPanel(new BorderLayout());

        JPanel certificateTypePanel = new JPanel(new BorderLayout());
        certificateTypePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

        JLabel certificateTypeLabel = new JLabel(R("CVCertificateType"));

        certificateTypeCombo = new JComboBox(certificateTypes);
        certificateTypeCombo.addActionListener(new CertificateTypeListener());

        certificateTypePanel.add(certificateTypeLabel, BorderLayout.LINE_START);
        certificateTypePanel.add(certificateTypeCombo, BorderLayout.CENTER);

        JPanel tablePanel = new JPanel(new BorderLayout());

        // User Table
        DefaultTableModel userTableModel = new DefaultTableModel(issuedToAndBy, columnNames);
        userTable.setModel(userTableModel);
        userTable.getTableHeader().setReorderingAllowed(false);
        userTable.setFillsViewportHeight(true);
        JScrollPane userTablePane = new JScrollPane(userTable);
        userTablePane.setPreferredSize(TABLE_DIMENSION);
        userTablePane.setSize(TABLE_DIMENSION);
        userTablePane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        // System Table
        DefaultTableModel systemTableModel = new DefaultTableModel(issuedToAndBy, columnNames);
        systemTable.setModel(systemTableModel);
        systemTable.getTableHeader().setReorderingAllowed(false);
        systemTable.setFillsViewportHeight(true);
        JScrollPane systemTablePane = new JScrollPane(systemTable);
        systemTablePane.setPreferredSize(TABLE_DIMENSION);
        systemTablePane.setSize(TABLE_DIMENSION);
        systemTablePane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        tabbedPane = new JTabbedPane();
        tabbedPane.addTab(R("CVUser"), userTablePane);
        tabbedPane.addTab(R("CVSystem"), systemTablePane);
        tabbedPane.addChangeListener(new TabChangeListener());

        JPanel buttonPanel = new JPanel(new FlowLayout());

        String[] buttonNames = { R("CVImport"), R("CVExport"), R("CVRemove"), R("CVDetails") };
        char[] buttonMnemonics = { KeyEvent.VK_I,
                                                                        KeyEvent.VK_E,
                                                                        KeyEvent.VK_M,
                                                                        KeyEvent.VK_D };
        ActionListener[] listeners = { new ImportButtonListener(),
                                                                                new ExportButtonListener(),
                                                                                new RemoveButtonListener(),
                                                                                new DetailsButtonListener() };
        JButton button;

        //get the max width
        int maxWidth = 0;
        for (int i = 0; i < buttonNames.length; i++) {
            button = new JButton(buttonNames[i]);
            maxWidth = Math.max(maxWidth, button.getMinimumSize().width);
        }

        for (int i = 0; i < buttonNames.length; i++) {
            button = new JButton(buttonNames[i]);
            button.setMnemonic(buttonMnemonics[i]);
            button.addActionListener(listeners[i]);
            button.setSize(maxWidth, button.getSize().height);
            // import and remove buttons
            if (i == 0 || i == 2) {
                disableForSystem.add(button);
            }
            buttonPanel.add(button);
        }

        tablePanel.add(tabbedPane, BorderLayout.CENTER);
        tablePanel.add(buttonPanel, BorderLayout.SOUTH);

        main.add(certificateTypePanel, BorderLayout.NORTH);
        main.add(tablePanel, BorderLayout.CENTER);

        if (parent != null) {
            JPanel closePanel = new JPanel(new BorderLayout());
            closePanel.setBorder(BorderFactory.createEmptyBorder(7, 7, 7, 7));
            JButton closeButton = new JButton(R("ButClose"));
            closeButton.addActionListener(new CloseButtonListener());
            defaultFocusComponent = closeButton;
            closePanel.add(closeButton, BorderLayout.EAST);
            main.add(closePanel, BorderLayout.SOUTH);
        }

        setLayout(new GridLayout(0,1));
        add(main);

    }

    /**
     * Read in the optionPane's keystore to issuedToAndBy.
     */
    private void readKeyStore() {

        Enumeration<String> aliases = null;
        certs = new ArrayList<X509Certificate>();
        try {

            //Get all of the X509Certificates and put them into an ArrayList
            aliases = keyStore.aliases();
            while (aliases.hasMoreElements()) {
                Certificate c = keyStore.getCertificate(aliases.nextElement());
                if (c instanceof X509Certificate) {
                    certs.add((X509Certificate) c);
                }
            }

            //get the publisher and root information
            issuedToAndBy = new String[certs.size()][2];
            for (int i = 0; i < certs.size(); i++) {
                X509Certificate c = certs.get(i);
                issuedToAndBy[i][0] =
                                        SecurityUtil.getCN(c.getSubjectX500Principal().getName());
                issuedToAndBy[i][1] =
                                        SecurityUtil.getCN(c.getIssuerX500Principal().getName());
            }
        } catch (Exception e) {
            //TODO
           OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
        }
    }

    /**
     * Re-reads the certs file and repopulates the JTable. This is typically
     * called after a certificate was deleted from the keystore.
     */
    private void repopulateTables() {
        initializeKeyStore();
        readKeyStore();
        DefaultTableModel tableModel = new DefaultTableModel(issuedToAndBy, columnNames);

        userTable.setModel(tableModel);

        tableModel = new DefaultTableModel(issuedToAndBy, columnNames);
        systemTable.setModel(tableModel);
    }

    public void focusOnDefaultButton() {
        if (defaultFocusComponent != null) {
            defaultFocusComponent.requestFocusInWindow();
        }
    }

    private char[] getPassword(final String label) {
        JPasswordField jpf = new JPasswordField();
        int result = JOptionPane.showConfirmDialog(parent,
                                new Object[]{label, jpf},  R("CVPasswordTitle"),
                                JOptionPane.OK_CANCEL_OPTION,
                                JOptionPane.INFORMATION_MESSAGE);
        if (result == JOptionPane.OK_OPTION) {
            return jpf.getPassword();
        }
        else {
            return null;
        }
    }

    /** Allows storing KeyStores.Types in a JComponent */
    private static class CertificateType {
        private final KeyStores.Type type;

        public CertificateType(KeyStores.Type type) {
            this.type = type;
        }

        public KeyStores.Type getType() {
            return type;
        }

        @Override
        public String toString() {
            return KeyStores.toDisplayableString(null, type);
        }
    }

    /** Invoked when a user selects a different certificate type */
    private class CertificateTypeListener implements ActionListener {
        @Override
        @SuppressWarnings("unchecked")//this is just certificateTypeCombo, nothing else
        public void actionPerformed(ActionEvent e) {
            JComboBox source = (JComboBox) e.getSource();
            CertificateType type = (CertificateType) source.getSelectedItem();
            currentKeyStoreType = type.getType();
            repopulateTables();
        }
    }

    /**
     * Invoked when a user selects a different tab (switches from user to system
     * or vice versa). Changes the currentKeyStore Enables or disables buttons.
     */
    private class TabChangeListener implements ChangeListener {
        @Override
        public void stateChanged(ChangeEvent e) {
            JTabbedPane source = (JTabbedPane) e.getSource();
            switch (source.getSelectedIndex()) {
                case 0:
                    currentKeyStoreLevel = Level.USER;
                    for (JComponent component : disableForSystem) {
                        component.setEnabled(true);
                    }
                    break;
                case 1:
                    currentKeyStoreLevel = Level.SYSTEM;
                    for (JComponent component : disableForSystem) {
                        component.setEnabled(false);
                    }
                    break;
            }
            repopulateTables();

        }
    }

    private class ImportButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {

            JFileChooser chooser = new JFileChooser();
            int returnVal = chooser.showOpenDialog(parent);
            if (returnVal == JFileChooser.APPROVE_OPTION) {
                try {
                    KeyStore ks = keyStore;
                    if (currentKeyStoreType == KeyStores.Type.CLIENT_CERTS) {
                        char[] password = getPassword(R("CVImportPasswordMessage"));
                        if (password != null) {
                            CertificateUtils.addPKCS12ToKeyStore(
                                       chooser.getSelectedFile(), ks, password);
                        } else {
                            return;
                        }
                    } else {
                        CertificateUtils.addToKeyStore(chooser.getSelectedFile(), ks);
                    }
                    File keyStoreFile = new File(KeyStores
                                        .getKeyStoreLocation(currentKeyStoreLevel, currentKeyStoreType));
                    if (!keyStoreFile.isFile()) {
                        FileUtils.createRestrictedFile(keyStoreFile, true);
                    }

                    OutputStream os = new FileOutputStream(keyStoreFile);
                    try {
                        ks.store(os, KeyStores.getPassword());
                    } finally {
                        os.close();
                    }
                    repopulateTables();
                } catch (Exception ex) {
                    // TODO: handle exception
                    OutputController.getLogger().log(OutputController.Level.ERROR_ALL, ex);
                }
            }
        }
    }

    private class ExportButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {

            final JTable table ;
            if (currentKeyStoreLevel == Level.USER) {
                table = userTable;
            } else {
                table = systemTable;
            }

            //For now, let's just export in -rfc mode as keytool does.
            //we'll write to a file the exported certificate.

            try {
                int selectedRow = table.getSelectedRow();
                if (selectedRow != -1) {
                    JFileChooser chooser = new JFileChooser();
                    int returnVal = chooser.showOpenDialog(parent);
                    if (returnVal == JFileChooser.APPROVE_OPTION) {
                        String alias = keyStore.getCertificateAlias(certs
                                                        .get(selectedRow));
                        if (alias != null) {
                            if (currentKeyStoreType == KeyStores.Type.CLIENT_CERTS) {
                                char[] password = getPassword(R("CVExportPasswordMessage"));
                                if (password != null) {
                                    CertificateUtils.dumpPKCS12(alias, chooser.getSelectedFile(), keyStore, password);
                                }
                            } else {
                                Certificate c = keyStore.getCertificate(alias);
                                PrintStream ps = new PrintStream(chooser.getSelectedFile().getAbsolutePath());
                                CertificateUtils.dump(c, ps);
                            }
                            repopulateTables();
                        }
                    }
                }
            } catch (Exception ex) {
                OutputController.getLogger().log(OutputController.Level.ERROR_ALL, ex);
            }
        }
    }

    private class RemoveButtonListener implements ActionListener {

        /**
         * Removes a certificate from the keyStore and writes changes to disk.
         */
        @Override
        public void actionPerformed(ActionEvent e) {

            final JTable table;
            if (currentKeyStoreLevel == Level.USER) {
                table = userTable;
            } else {
                table = systemTable;
            }
            try {
                int selectedRow = table.getSelectedRow();

                if (selectedRow != -1) {
                    String alias = keyStore.getCertificateAlias(certs.get(selectedRow));
                    if (alias != null) {

                        int i = JOptionPane.showConfirmDialog(parent,
                                                        R("CVRemoveConfirmMessage"),
                                                        R("CVRemoveConfirmTitle"),
                                                        JOptionPane.YES_NO_OPTION);
                        if (i == 0) {
                            keyStore.deleteEntry(alias);
                            File keyStoreFile = new File(KeyStores
                                                        .getKeyStoreLocation(currentKeyStoreLevel, currentKeyStoreType));
                            if (!keyStoreFile.isFile()) {
                                FileUtils.createRestrictedFile(keyStoreFile, true);
                            }
                            FileOutputStream fos = new FileOutputStream(keyStoreFile);
                            keyStore.store(fos, KeyStores.getPassword());
                            fos.close();
                        }
                    }
                    repopulateTables();
                }
            } catch (Exception ex) {
                OutputController.getLogger().log(OutputController.Level.ERROR_ALL, ex);
            }

        }
    }

    private class DetailsButtonListener implements ActionListener {

        /**
         * Shows the details of a trusted certificate.
         */
        @Override
        public void actionPerformed(ActionEvent e) {

            final JTable table;
            if (currentKeyStoreLevel == Level.USER) {
                table = userTable;
            } else {
                table = systemTable;
            }

            int selectedRow = table.getSelectedRow();
            if (selectedRow != -1 && selectedRow >= 0) {
                X509Certificate c = certs.get(selectedRow);
                SecurityDialog.showSingleCertInfoDialog(c, parent);
            }
        }
    }

    private class CloseButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            parent.dispose();
        }
    }

}
