/*
    Websocket Smartcard Signer
    Copyright (C) 2017  Damiano Falcioni ([email protected])
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero 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 Affero General Public License for more details.
    
    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. 
 */
package df.sign;

import java.awt.Choice;
import java.awt.Dimension;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
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.JTable;
import javax.swing.JTextArea;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;

import df.sign.datastructure.Data;
import df.sign.pkcs11.CertificateData;
import df.sign.utils.IOUtils;
import df.sign.utils.X509Utils;

public class SignUI {

    public SignEngine signEngine = null;
    
    public boolean readAllCertificates = false;
    public String dnRestrictedSignatureName = "";
    
    public SignUI(SignEngine signEngine){
        this.signEngine = signEngine;
    }

    public CertificateData showCertificateDialog(){
        
        final Choice certificateComboBox = new Choice();
        final JOptionPane optionPane = new JOptionPane();
        optionPane.setMessageType(JOptionPane.PLAIN_MESSAGE);
        
        JPanel panel = new JPanel();
        
        JButton signButton = new JButton("Sign");
        signButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if(isCertificateCorrect(certificateComboBox))
                    optionPane.setValue(JOptionPane.OK_OPTION);
            }
        });
        
        JButton terminateButton = new JButton("Cancel");
        terminateButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                optionPane.setValue(JOptionPane.CLOSED_OPTION);
            }
        });
        
        JButton refreshCertificateButton = new JButton();
        refreshCertificateButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                updateComboBox(certificateComboBox);
            }
        });
        refreshCertificateButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("refresh.png")));  
        refreshCertificateButton.setBorderPainted(false);  
        refreshCertificateButton.setFocusPainted(true);  
        refreshCertificateButton.setContentAreaFilled(false);
        refreshCertificateButton.setPreferredSize(new java.awt.Dimension(20,20));
        
        panel.add(certificateComboBox);
        panel.add(refreshCertificateButton);
        
        updateComboBox(certificateComboBox);
        
        optionPane.setMessage(panel);
        optionPane.setOptions(new Object[] { signButton, terminateButton});
        
        SignUtils.playBeeps(1);
        
        JDialog dialog = optionPane.createDialog(null, "Certificate selection");
        dialog.setAlwaysOnTop(true);
        dialog.setVisible(true);
        
        int retval = (optionPane.getValue() instanceof Integer)?((Integer)optionPane.getValue()).intValue():-1;
        dialog.dispose();
        
        if(retval == JOptionPane.OK_OPTION){
            CertificateData certData = SignUtils.getCertificateDataByID((String)certificateComboBox.getSelectedItem(), signEngine.certificateList);
            return certData;
        }
        return null;
    }
    
    private void updateComboBox(Choice certificateComboBox){
        certificateComboBox.removeAll();
        certificateComboBox.addItem("Loading Certificates...");
        certificateComboBox.select(0);
       
        ArrayList<CertificateData> certList = new ArrayList<CertificateData>();
        try {
            certList = signEngine.loadSmartCardCertificateList(readAllCertificates).certificateList;
        } catch (Exception e) {
            e.printStackTrace();
            SignUtils.playBeeps(1);
            JOptionPane.showMessageDialog(null, "ERROR LOADING CERTIFICATES:\n"+e.getMessage(), "ERROR", JOptionPane.ERROR_MESSAGE);
        }
        
        certificateComboBox.removeAll();
        certificateComboBox.addItem("--Select Certificate--");
        for(int i=0;i<certList.size();i++)
            certificateComboBox.addItem(certList.get(i).id);
        
        if(certificateComboBox.getItemCount()==1){
            certificateComboBox.removeAll();
            certificateComboBox.addItem("--No Certificates Available!--");
            SignUtils.playBeeps(2);
        }
        else{
            if(certificateComboBox.getItemCount()==2){
                certificateComboBox.remove(0);
            }
            SignUtils.playBeeps(1);
        }
    }
    
    private boolean isCertificateCorrect(Choice certificateComboBox){
        CertificateData certData = SignUtils.getCertificateDataByID((String)certificateComboBox.getSelectedItem(), signEngine.certificateList);
        
        if(certData == null){
            SignUtils.playBeeps(2);
            JOptionPane.showMessageDialog(null, "CERTIFICATE NOT SELECTED", "ERRORE", JOptionPane.ERROR_MESSAGE);
            return false;
        }
        
        if(dnRestrictedSignatureName.length() != 0){
            String cfCert = X509Utils.getCFFromCertSubject(certData.cert.getSubjectDN().getName());
            if(!cfCert.equals(dnRestrictedSignatureName.toUpperCase()))
                if(!certData.cert.getSubjectDN().getName().contains(dnRestrictedSignatureName.toUpperCase())){
                    SignUtils.playBeeps(2);
                    JOptionPane.showMessageDialog(null, "SIGNATURE AVAILABLE ONLY FOR USER " + dnRestrictedSignatureName +"\nThe selected certificate is valid for user " + cfCert, "ERROR", JOptionPane.ERROR_MESSAGE);
                    return false;
            }
        }
        
        if(!X509Utils.checkValidity(certData.cert, null)){
            SignUtils.playBeeps(2);
            int ret = JOptionPane.showConfirmDialog(null, "THE CERTIFICATE IS EXPIRED\nPROCEEDS ANYWAY?", "WARNING", JOptionPane.YES_NO_OPTION);
            if(ret != JOptionPane.YES_OPTION)
                return false;
        }
        
        if(X509Utils.checkIsSelfSigned(certData.cert)){
            SignUtils.playBeeps(2);
            int ret = JOptionPane.showConfirmDialog(null, "THE CERTIFICATE IS SELF SIGNED\nPROCEEDS ANYWAY?", "WARNING", JOptionPane.YES_NO_OPTION);
            if(ret != JOptionPane.YES_OPTION)
                return false;
        }
        
        if(X509Utils.checkIsRevoked(certData.cert)){
            SignUtils.playBeeps(2);
            int ret = JOptionPane.showConfirmDialog(null, "THE CERTIFICATE IS REVOKED\nPROCEEDS ANYWAY?", "WARNING", JOptionPane.YES_NO_OPTION);
            if(ret != JOptionPane.YES_OPTION){
                return false;
            }
        }
        
        return true;
    }
    
    public void sign(CertificateData certData, String pin){
        if(signEngine.getNumDataToSign() == 0){
            SignUtils.playBeeps(2);
            JOptionPane.showMessageDialog(null, "NO DATA TO SIGN", "ERROR", JOptionPane.ERROR_MESSAGE);
            return;
        }
 
        try {
            signEngine.sign(certData, pin);
        } catch (Exception e) {
            e.printStackTrace();
            SignUtils.playBeeps(1);
            JOptionPane.showMessageDialog(null, "ERROR DURING THE SIGNING PROCESS:\n"+e.getMessage(), "ERROR", JOptionPane.ERROR_MESSAGE);
        }
    }
    /*
    public static String askForPIN(){
        final JPasswordField txt = new JPasswordField(8);

        txt.addAncestorListener(new AncestorListener(){
            @Override
            public void ancestorAdded(AncestorEvent arg0) {
                arg0.getComponent().requestFocusInWindow();
            }
            @Override
            public void ancestorMoved(AncestorEvent arg0) {}
            @Override
            public void ancestorRemoved(AncestorEvent arg0) {}
        });
        
        JLabel lbl = new JLabel("INSERT THE SMARTCARD PIN");
        JPanel pan = new JPanel();
        pan.add(lbl);
        pan.add(txt);
        
        SignUtils.playBeeps(1);
        int retval = JOptionPane.showOptionDialog(null, pan, "PIN", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null);        
        if(retval == JOptionPane.OK_OPTION) 
            return new String(txt.getPassword());
        return null;
    }
    */
    public static String askForPIN() {
        final JOptionPane optionPane = new JOptionPane();
        optionPane.setMessageType(JOptionPane.PLAIN_MESSAGE);
        
        JPanel panel = new JPanel();
        
        final JPasswordField txt = new JPasswordField(8);

        txt.addAncestorListener(new AncestorListener(){
            @Override
            public void ancestorAdded(AncestorEvent arg0) {
                arg0.getComponent().requestFocusInWindow();
            }
            @Override
            public void ancestorMoved(AncestorEvent arg0) {}
            @Override
            public void ancestorRemoved(AncestorEvent arg0) {}
        });
        
        JLabel lbl = new JLabel("Insert the PIN for the selected certificate: ");
        panel.add(lbl);
        panel.add(txt);
        
        JButton okButton = new JButton("OK");
        okButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                optionPane.setValue(JOptionPane.OK_OPTION);
            }
        });
        
        JButton cancelButton = new JButton("Cancel");
        cancelButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                optionPane.setValue(JOptionPane.CLOSED_OPTION);
            }
        });
        
        optionPane.setMessage(panel);
        optionPane.setOptions(new Object[] { okButton, cancelButton});
        
        SignUtils.playBeeps(1);
        
        JDialog dialog = optionPane.createDialog(null, "PIN");
        dialog.setAlwaysOnTop(true);
        dialog.setVisible(true);
        
        int retval = (optionPane.getValue() instanceof Integer)?((Integer)optionPane.getValue()).intValue():-1;
        dialog.dispose();
        
        if(retval == JOptionPane.OK_OPTION){
            return new String(txt.getPassword());
        }
        return null;
    }
    
    private void showHelp(){
        String[] dllList = signEngine.dllList;
        
        final JOptionPane optionPane = new JOptionPane();
        
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
        JTextArea txtConflicts = new JTextArea();
        txtConflicts.setColumns(60);
        txtConflicts.setRows(3);
        txtConflicts.setEditable(false);
        JScrollPane txtConflictsScroll = new JScrollPane (txtConflicts, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        panel.add(new JLabel(" "));
        panel.add(new JLabel("JAR CONFLICTS: "));
        panel.add(new JLabel(" "));
        panel.add(txtConflictsScroll);

        String conflicts = "No JAR conflicts identified";
        String[] conflictJARList = SignUtils.checkJarConflicts();
        if(conflictJARList.length != 0){
            conflicts = "";
            for(String conflictJAR:conflictJARList)
                conflicts += "- " + conflictJAR + "\n";
        }
        txtConflicts.setText(conflicts);
        
        String[] tableColumnNames = {"LIBRARY NAME", "STATUS", "SMARTCARD TYPE"};
        Object[][] tableData = new Object[dllList.length][3];
        JTable table = new JTable(tableData, tableColumnNames);
        JScrollPane tableScroll = new JScrollPane (table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        tableScroll.setPreferredSize(new Dimension(60, 200));
        panel.add(new JLabel(" "));
        panel.add(new JLabel("LIBRARIES STATUS: "));
        panel.add(new JLabel(" "));
        panel.add(tableScroll);
        
        for(int i=0; i<dllList.length;i++){
            tableData[i][0] = dllList[i];
            tableData[i][1] = (SignUtils.getLibraryFullPath(dllList[i])!=null)?"INSTALLED":"NOT INSTALLED";
            tableData[i][2] = (SignUtils.getCardTypeFromDLL(dllList[i])!="")?SignUtils.getCardTypeFromDLL(dllList[i]):"NOT MANAGED";
        }
        
        JTextArea txtSmartcardInfo = new JTextArea();
        txtSmartcardInfo.setColumns(60);
        txtSmartcardInfo.setRows(3);
        txtSmartcardInfo.setEditable(false);
        JScrollPane txtSmartcardInfoScroll = new JScrollPane (txtSmartcardInfo, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        panel.add(new JLabel(" "));
        panel.add(new JLabel("CONNECTED SMARTCARD INFOS: "));
        panel.add(new JLabel(" "));
        panel.add(txtSmartcardInfoScroll);
        
        String smartcardInfo = "";
        ArrayList<String> cardATRList = SignUtils.getConnectedCardATR();
        if(cardATRList.size()==0){
            smartcardInfo = "SMARTCARD NOT CONNECTED\n";
        }else{
            smartcardInfo = "CONNECTED SMARTCARDS:\n";
            for(String cardATR:cardATRList){
                String[] cardInfo = SignUtils.getCardInfo(cardATR);
                if(cardInfo == null)
                    smartcardInfo += "- UNKNOWN. ATR: " + cardATR + "\n";
                else{
                    smartcardInfo += "- " + cardInfo[0] + "\tPKCS11: ";
                    String[] cardInfoDllList = cardInfo[1].split("%");
                    String urlDllInstaller = cardInfo[3];
                    String correctLibrary = "";
                    for(String cardInfoDll : cardInfoDllList){
                        String dllFullPath = SignUtils.getLibraryFullPath(cardInfoDll);
                        if(dllFullPath != ""){
                            correctLibrary = dllFullPath;
                            break;
                        }
                    }
                    if(correctLibrary == "")
                        smartcardInfo += "NOT INSTALLED-> " + dllList[0] + " DOWNLOAD URL: " + urlDllInstaller + "\n";
                    else
                        smartcardInfo += "INSTALLED->" + correctLibrary + "\n";
                }
            }
        }
        
        txtSmartcardInfo.setText(smartcardInfo);
        
        JTextArea txtLog = new JTextArea();
        txtLog.setColumns(60);
        txtLog.setRows(10);
        txtLog.setEditable(false);
        JScrollPane txtLogScroll = new JScrollPane (txtLog, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        panel.add(new JLabel(" "));
        panel.add(new JLabel("LOGS: "));
        panel.add(new JLabel(" "));
        panel.add(txtLogScroll);
        try {
            txtLog.setText(new String(IOUtils.readFile(SignUtils.logFilePath)));
        } catch(Exception e) {}
        optionPane.setMessageType(JOptionPane.PLAIN_MESSAGE);
        optionPane.setMessage(panel);
        optionPane.setOptions(new Object[] {"OK"});
        
        JDialog dialog = optionPane.createDialog(null, "Diagnostic");
        dialog.setVisible(true);
        dialog.setAlwaysOnTop(true);
        
        optionPane.getValue();
        dialog.dispose();
    }
    
    public static List<Data> showFileSelection() throws Exception{
        List<Data> ret = new ArrayList<Data>();
        
        JFileChooser jfc = new JFileChooser();
        jfc.setMultiSelectionEnabled(true);
        
        jfc.setDialogTitle("Choose the files to sign");
        SignUtils.playBeeps(1);
        if(jfc.showOpenDialog(null) != JFileChooser.APPROVE_OPTION)
            return null;
        
        File[] choosedFileList = jfc.getSelectedFiles();
        for(File file:choosedFileList){
            String id = file.getAbsolutePath();
            byte[] fileContent = IOUtils.readFile(file);
            ret.add(new Data(id, fileContent));
        }
        return ret;
    }
    
    public static void showFileSave(List<Data> dataSignedList) throws Exception{
        for(Data dataSigned : dataSignedList){
            JFileChooser jfc = new JFileChooser();
            jfc.setMultiSelectionEnabled(false);
            jfc.setSelectedFile(new File(dataSigned.id+(dataSigned.id.toLowerCase().endsWith(".pdf") || dataSigned.id.toLowerCase().endsWith(".p7m")?"":(dataSigned.config.saveAsPDF?".pdf":".p7m"))));
            jfc.setDialogTitle("Save file");
            SignUtils.playBeeps(1);
            if(jfc.showSaveDialog(null) != JFileChooser.APPROVE_OPTION)
                continue;
            String fileName = jfc.getSelectedFile().getAbsolutePath();
            if(new File(fileName).exists()){
                SignUtils.playBeeps(1);
                if(JOptionPane.showConfirmDialog(null, "Overwrite the file " + fileName + " ?", "WARNING", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION)
                    continue;
            }
            
            IOUtils.writeFile(dataSigned.data, fileName, false);
        }
    }
    
    public static void showErrorMessage(String message){
        SignUtils.playBeeps(2);
        JOptionPane.showMessageDialog(null, message, "ERROR", JOptionPane.ERROR_MESSAGE);
    }
    
    public void createTrayIcon() throws Exception{
        if(!SystemTray.isSupported())
            throw new Exception("SystemTray is not supported");
        
        SignFactory.getUniqueWebSocketServer().waitStart();
        
        final PopupMenu popup = new PopupMenu();
        final TrayIcon trayIcon = new TrayIcon(new ImageIcon(SignUI.class.getResource((SignFactory.getUniqueWebSocketServer().isTerminating())?"smTrayR.png":"smTrayG.png")).getImage());
        trayIcon.setImageAutoSize(true);

        final SystemTray tray = SystemTray.getSystemTray();
        
        MenuItem aboutItem = new MenuItem("About");
        MenuItem statusItem = new MenuItem("Server Status");
        MenuItem restartItem = new MenuItem("Restart Server");
        MenuItem locallyItem = new MenuItem("Sign Locally");
        MenuItem checkItem = new MenuItem("Check Problems");
        MenuItem exitItem = new MenuItem("Exit");
        popup.add(aboutItem);
        popup.addSeparator();
        popup.add(locallyItem);
        popup.addSeparator();
        popup.add(statusItem);
        popup.addSeparator();
        popup.add(restartItem);
        popup.addSeparator();
        popup.add(checkItem);
        popup.addSeparator();
        popup.addSeparator();
        popup.add(exitItem);
        trayIcon.setPopupMenu(popup);
        
        tray.add(trayIcon);
        
        ActionListener statusAL = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if(SignFactory.getUniqueWebSocketServer().isTerminated())
                    JOptionPane.showMessageDialog(null, "WebSocket Server status: TERMINATED");
                else if(SignFactory.getUniqueWebSocketServer().isTerminating())
                    JOptionPane.showMessageDialog(null, "WebSocket Server status: TERMINATING");
                else if(SignFactory.getUniqueWebSocketServer().isStarted())
                    JOptionPane.showMessageDialog(null, "WebSocket Server status: STARTED on port " + SignFactory.getUniqueWebSocketServer().getPort());
                else
                    JOptionPane.showMessageDialog(null, "WebSocket Server status: NOT STARTED");
            }
        };
        
        final ActionListener onServerStatusChanged = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if(e.getID() == 0)
                    trayIcon.setImage(new ImageIcon(SignUI.class.getResource("smTrayG.png")).getImage());
                else if(e.getID() == 1)
                    trayIcon.setImage(new ImageIcon(SignUI.class.getResource("smTrayR.png")).getImage());
                else
                    JOptionPane.showMessageDialog(null, "Event " + e.getActionCommand() + " not recognized");
            }
        };
        
        SignFactory.getUniqueWebSocketServer().onStatusChanged(onServerStatusChanged);
        
        trayIcon.addActionListener(statusAL);
        
        aboutItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(null, "WEBSOCKET SMARTCARD SIGNER\n\nCreated by: Damiano Falcioni ([email protected])");
            }
        });
        
        statusItem.addActionListener(statusAL);
        
        locallyItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                SignFactory.performSignLocally();
            }
        });
        
        restartItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                SignFactory.getNewWebSocketServer().serverThreadStart();
                SignFactory.getUniqueWebSocketServer().waitStart();
                SignFactory.getUniqueWebSocketServer().onStatusChanged(onServerStatusChanged);
                trayIcon.setImage(new ImageIcon(SignUI.class.getResource((SignFactory.getUniqueWebSocketServer().isTerminating())?"smTrayR.png":"smTrayG.png")).getImage());
            }
        });
        
        checkItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                showHelp();
            }
        });
        
        exitItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                tray.remove(trayIcon);
                System.exit(0);
            }
        });
    }
}