/*
 * ARX Data Anonymization Tool
 * Copyright 2012 - 2024 Fabian Prasser and contributors
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.deidentifier.arx.gui.view.impl.wizard;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.deidentifier.arx.gui.resources.Resources;
import org.deidentifier.arx.gui.view.impl.common.DelayedChangeListener;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;

/**
 * JDBC page
 *
 * This page offers means to specify connection details for a database. For
 * now MS SQL, MySQL, PostgreSQL and SQLite is supported. In case of remote database
 * types (i.e. MS SQL, MySQL and PostgreSQL) the user is asked for the server and a
 * username and password. In case of SQLite the user can select any *.db file.
 *
 * After ther user specified the details a connection is established and
 * passed on to {@link ImportWizardModel}.
 *
 * This includes:
 *
 * <ul>
 *  <li>{@link ImportWizardModel#setJdbcConnection(Connection)<li>
 *  <li>{@link ImportWizardModel#setJdbcTables(List)<li>
 * </ul>
 * 
 * @author Karol Babioch
 * @author Fabian Prasser
 */
public class ImportWizardPageJDBC extends WizardPage {
    
    /** Reference to the wizard containing this page. */
    private ImportWizard        wizardImport;
    
    /** Widget */
    private Label               lblType;
    
    /** Widget */
    private Combo               comboType;
    
    /** Widget */
    private Composite           compositeSwap;
    
    /** Widget */
    private Text                txtServer;
    
    /** Widget */
    private StackLayout         layout;
    
    /** Widget */
    private Composite           compositeRemote;
    
    /** Widget */
    private Composite           compositeLocal;
    
    /** Widget */
    private Text                txtPort;
    
    /** Widget */
    private Text                txtUsername;
    
    /** Widget */
    private Text                txtPassword;
    
    /** Widget */
    private Text                txtDatabase;
    
    /** Widget */
    private Label               lblLocation;
    
    /** Widget */
    private Combo               comboLocation;
    
    /** Widget */
    private Button              btnChoose;
    
    /** Widget */
    private Composite           container;
    
    /** Widget */
    private static final String ORACLE     = "Oracle";    //$NON-NLS-1$
                                                           
    /** Widget */
    private static final String MSSQL      = "MS SQL";    //$NON-NLS-1$
                                                           
    /** Widget */
    private static final String MYSQL      = "MySQL";     //$NON-NLS-1$
                                                           
    /** Widget */
    private static final String POSTGRESQL = "PostgreSQL"; //$NON-NLS-1$
                                                           
    /** Widget */
    private static final String SQLITE     = "SQLite";    //$NON-NLS-1$
                                                           
    /**
     * Creates a new instance of this page and sets its title and description.
     *
     * @param wizardImport Reference to wizard containing this page
     */
    public ImportWizardPageJDBC(ImportWizard wizardImport) {
        
        super("WizardImportJdbcPage"); //$NON-NLS-1$
        setTitle("JDBC"); //$NON-NLS-1$
        setDescription(Resources.getMessage("ImportWizardPageJDBC.6")); //$NON-NLS-1$
        this.wizardImport = wizardImport;
    }
    
    /**
     * Creates the design of this page
     * 
     * This adds all the controls to the page along with their listeners.
     *
     * @param parent
     * @note {@link #compositeSwap} contains the actual text fields. Depending
     *       upon the status of {@link #comboType}, it will either display {@link #compositeRemote} or {@link #compositeLocal}.
     */
    public void createControl(Composite parent) {
        
        container = new Composite(parent, SWT.NULL);
        
        setControl(container);
        container.setLayout(new GridLayout(2, false));
        
        /* Type label + combobox */
        lblType = new Label(container, SWT.NONE);
        lblType.setText(Resources.getMessage("ImportWizardPageJDBC.7")); //$NON-NLS-1$
        
        /* Combo for choosing database type */
        comboType = new Combo(container, SWT.READ_ONLY);
        comboType.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        comboType.setItems(new String[] { ORACLE, MSSQL, POSTGRESQL, MYSQL, SQLITE });
        comboType.addSelectionListener(new SelectionAdapter() {
            
            /**
             * Swaps the composites, resets it and triggers a relayout
             */
            @Override
            public void widgetSelected(SelectionEvent e) {
                
                setMessage(null);
                setErrorMessage(null);
                setPageComplete(false);
                
                /* Display compositeLocal in case of SQLite */
                if (comboType.getText().equals(SQLITE)) {
                    comboLocation.removeAll();
                    layout.topControl = compositeLocal;
                    
                /* Display compositeRemote otherwise */
                } else {
                    
                    layout.topControl = compositeRemote;
                    
                    /* Set default ports*/
                    if (comboType.getText().equals(MSSQL)) {
                        txtPort.setText("1433"); //$NON-NLS-1$
                    } else if (comboType.getText().equals(MYSQL)) {
                        txtPort.setText("3306"); //$NON-NLS-1$
                    } else if (comboType.getText().equals(POSTGRESQL)) {
                        txtPort.setText("5432"); //$NON-NLS-1$
                    } else if (comboType.getText().equals(ORACLE)) {
                        txtPort.setText("1521"); //$NON-NLS-1$
                    }
                }
                
                /* Trigger relayout */
                compositeSwap.layout();
            }
        });
        
        /* Placeholder */
        new Label(container, SWT.NONE);
        new Label(container, SWT.NONE);
        
        /* Swap composite */
        compositeSwap = new Composite(container, SWT.NONE);
        layout = new StackLayout();
        compositeSwap.setLayout(layout);
        compositeSwap.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1));
        
        /* Remote composite */
        createCompositeRemote();
        
        /* Local composite */
        createCompositeLocal();
        
        /* Mark page as incomplete by default */
        setPageComplete(false);
    }
    
    /**
     * Creates the content of {@link #compositeLocal}
     *
     * This adds a file chooser and an appropriate combo to select files.
     * Selecting a file from the combo will trigger a read of the tables. If
     * everything is fine, the tables from the database will be read.
     *
     * @see {@link #readTables()}
     */
    private void createCompositeLocal() {
        
        compositeLocal = new Composite(compositeSwap, SWT.NONE);
        compositeLocal.setLayout(new GridLayout(3, false));
        
        /* Location label */
        lblLocation = new Label(compositeLocal, SWT.NONE);
        lblLocation.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblLocation.setText(Resources.getMessage("ImportWizardPageJDBC.11")); //$NON-NLS-1$
        
        /* Combo box for selection of file */
        comboLocation = new Combo(compositeLocal, SWT.READ_ONLY);
        comboLocation.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        comboLocation.addSelectionListener(new SelectionAdapter() {
            
            /* Read tables from file */
            @Override
            public void widgetSelected(SelectionEvent e) {
                setErrorMessage(null);
                connect();
                boolean ok = readTables();
                setPageComplete(ok);
                if (ok) {
                    setMessage(Resources.getMessage("ImportWizardPageJDBC.21"), INFORMATION); //$NON-NLS-1$
                }
            }
        });
        
        /* Button to open file selection dialog */
        btnChoose = new Button(compositeLocal, SWT.NONE);
        btnChoose.setText(Resources.getMessage("ImportWizardPageJDBC.12")); //$NON-NLS-1$
        btnChoose.addSelectionListener(new SelectionAdapter() {
            
            /**
             * Opens a file selection dialog for "*.db" files
             *
             * If a valid file was selected, it is added to {@link #comboLocation} when it wasn't already there. It is then
             * preselected within {@link #comboLocation}.
             */
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                
                /* Open file dialog */
                final String path = wizardImport.getController().actionShowOpenFileDialog(getShell(), "*.db"); //$NON-NLS-1$
                
                if (path == null) {
                    return;
                }
                
                /* Check whether path was already added */
                if (comboLocation.indexOf(path) == -1) {
                    comboLocation.add(path, 0);
                }
                
                /* Select path and notify comboLocation about change */
                comboLocation.select(comboLocation.indexOf(path));
                comboLocation.notifyListeners(SWT.Selection, null);
            }
        });
    }
    
    /**
     * Creates the content of {@link #compositeRemote}
     *
     * This adds all of the labels and text fields necessary to connect to a
     * remote database server. If everything is fine, the tables from the
     * database will be read.
     *
     * @see {@link #readTables()}
     */
    private void createCompositeRemote() {
        
        compositeRemote = new Composite(compositeSwap, SWT.NONE);
        compositeRemote.setLayout(new GridLayout(2, false));
        
        // Tries to connect to database on changes
        DelayedChangeListener connectionTester = new DelayedChangeListener(1000) {
            @Override
            public void delayedEvent() {
                tryToConnect();
            }
        };
        
        Label lblServer = new Label(compositeRemote, SWT.NONE);
        lblServer.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblServer.setText(Resources.getMessage("ImportWizardPageJDBC.14")); //$NON-NLS-1$
        
        txtServer = new Text(compositeRemote, SWT.BORDER);
        txtServer.setText("localhost"); //$NON-NLS-1$
        txtServer.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        txtServer.addModifyListener(connectionTester);
        
        Label lblPort = new Label(compositeRemote, SWT.NONE);
        lblPort.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblPort.setText(Resources.getMessage("ImportWizardPageJDBC.16")); //$NON-NLS-1$
        
        txtPort = new Text(compositeRemote, SWT.BORDER);
        txtPort.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        txtPort.addModifyListener(connectionTester);
        
        Label lblUsername = new Label(compositeRemote, SWT.NONE);
        lblUsername.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblUsername.setText(Resources.getMessage("ImportWizardPageJDBC.0")); //$NON-NLS-1$
        
        txtUsername = new Text(compositeRemote, SWT.BORDER);
        txtUsername.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        txtUsername.addModifyListener(connectionTester);
        
        Label lblPassword = new Label(compositeRemote, SWT.NONE);
        lblPassword.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblPassword.setText(Resources.getMessage("ImportWizardPageJDBC.1")); //$NON-NLS-1$
        
        txtPassword = new Text(compositeRemote, SWT.BORDER | SWT.PASSWORD);
        txtPassword.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        txtPassword.addModifyListener(connectionTester);
        
        Label lblDatabase = new Label(compositeRemote, SWT.NONE);
        lblDatabase.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblDatabase.setText(Resources.getMessage("ImportWizardPageJDBC.19")); //$NON-NLS-1$
        
        txtDatabase = new Text(compositeRemote, SWT.BORDER);
        txtDatabase.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        txtDatabase.addModifyListener(connectionTester);
    }
    
    /**
     * Tries to establish a remote JDBC connection
     *
     * Unless all mandatory fields (everything besides the password) are
     * not empty this will try to connect to the database. It sets the message
     * and errors of this page accordingly and will also try to read in the
     * tables once a successful connection has been established.
     *
     * @see {@link #readTables()}
     */
    private void tryToConnect() {
        
        setErrorMessage(null);
        setMessage(null);
        
        String server = txtServer.getText();
        String port = txtPort.getText();
        String username = txtUsername.getText();
        String database = txtDatabase.getText();
        
        if (server.isEmpty() || port.isEmpty() || username.isEmpty() || database.isEmpty()) {
            setMessage(Resources.getMessage("ImportWizardPageJDBC.6")); //$NON-NLS-1$
            setPageComplete(false);
            return;
        }
        
        setMessage(Resources.getMessage("ImportWizardPageJDBC.20"), INFORMATION); //$NON-NLS-1$
        if (connect()) {
            setMessage(Resources.getMessage("ImportWizardPageJDBC.21"), INFORMATION); //$NON-NLS-1$
            setPageComplete(readTables());
        } else {
            setPageComplete(false);
        }
    }
    
    /**
     * Connects to the database
     *
     * This tries to establish an JDBC connection. In case of an error
     * appropriate error messages are set. Otherwise the connection is passed
     * on to {@link ImportWizardModel}. The return value indicates whether a
     * connection has been established.
     *
     * @return True if successfully connected, false otherwise
     *
     * @see {@link ImportWizardModel#setJdbcConnection(Connection)}
     */
    private boolean connect() {
        
        try {
            
            Connection connection = null;
            
            if (comboType.getText().equals(SQLITE)) {
                
                Class.forName("org.sqlite.JDBC"); //$NON-NLS-1$
                connection = DriverManager.getConnection("jdbc:sqlite:" + comboLocation.getText()); //$NON-NLS-1$
                
            } else if (comboType.getText().equals(POSTGRESQL)) {
                
                Class.forName("org.postgresql.Driver"); //$NON-NLS-1$
                connection = DriverManager.getConnection("jdbc:postgresql://" + txtServer.getText() + ":" + txtPort.getText() + "/" + txtDatabase.getText(), txtUsername.getText(), txtPassword.getText()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                
            } else if (comboType.getText().equals(MSSQL)) {
                
                Class.forName("net.sourceforge.jtds.jdbc.Driver"); //$NON-NLS-1$
                connection = DriverManager.getConnection("jdbc:jtds:sqlserver://" + txtServer.getText() + ":" + txtPort.getText() + "/" + txtDatabase.getText(), txtUsername.getText(), txtPassword.getText()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                
            } else if (comboType.getText().equals(MYSQL)) {
                
                Class.forName("com.mysql.jdbc.Driver"); //$NON-NLS-1$
                connection = DriverManager.getConnection("jdbc:mysql://" + txtServer.getText() + ":" + txtPort.getText() + "/" + txtDatabase.getText(), txtUsername.getText(), txtPassword.getText()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            } else if (comboType.getText().equals(ORACLE)) {
                
                Class.forName("oracle.jdbc.driver.OracleDriver"); //$NON-NLS-1$
                connection = DriverManager.getConnection("jdbc:oracle:thin:@" + txtServer.getText() + ":" + txtPort.getText() + ":" + txtDatabase.getText(), txtUsername.getText(), txtPassword.getText()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
            
            try {
                if (!wizardImport.getData().getJdbcConnection().isClosed()) {
                    wizardImport.getData().getJdbcConnection().close();
                }
            } catch (Exception e) {
                /* Die silently */
            }
            wizardImport.getData().setJdbcConnection(connection);
            return true;
            
        } catch (ClassNotFoundException e) {
            setErrorMessage(Resources.getMessage("ImportWizardPageJDBC.36")); //$NON-NLS-1$
            return false;
        } catch (SQLException e) {
            /* Database connection error */
            setErrorMessage(e.getLocalizedMessage());
            return false;
        }
    }
    
    /**
     * Reads in the tables
     *
     * If successful, the page is marked as complete and a list of tables is
     * assigned to {@link ImportWizardModel}. Otherwise an appropriate error message
     * is set.
     *
     * @see {@link ImportWizardModel#setJdbcTables(List)}
     */
    private boolean readTables() {
        ResultSet rs = null;
        try {
            Connection connection = wizardImport.getData().getJdbcConnection();
            String[] tableTypes = { "TABLE", "VIEW" }; //$NON-NLS-1$ //$NON-NLS-2$
            rs = connection.getMetaData().getTables(null, null, "%", tableTypes); //$NON-NLS-1$
            List<String> tables = new ArrayList<String>();
            
            while (rs.next()) {
                String name = rs.getString("TABLE_NAME"); //$NON-NLS-1$
                String schema = rs.getString("TABLE_SCHEM"); //$NON-NLS-1$
                if (schema != null) {
                    name = schema + "." + name; //$NON-NLS-1$
                }
                tables.add(name); 
            }
            
            wizardImport.getData().setJdbcTables(tables);
            return true;
        } catch (SQLException e) {
            setErrorMessage(Resources.getMessage("ImportWizardPageJDBC.41")); //$NON-NLS-1$
            return false;
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException e) {
                /* Ignore silently */
            }
        }
    }
}
