/*
 * 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.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import org.deidentifier.arx.DataType;
import org.deidentifier.arx.gui.resources.Resources;
import org.deidentifier.arx.gui.view.SWTUtil;
import org.deidentifier.arx.io.IOUtil;
import org.deidentifier.arx.io.ImportColumnJDBC;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;

/**
 * Table overview page
 * 
 * This pages gives the user an overview of the detected tables and allows him
 * to select the desired one by clicking on it. The tables itself are retrieved
 * from {@link ImportData#getJdbcTables()()}. The selected one will be assigned
 * via {@link ImportWizardModel#setSelectedJdbcTable(String)} along with the
 * detected columns for this table using {@link ImportWizardModel#setWizardColumns(List)} and its preview data {@link ImportWizardModel#setPreviewData(List)}.
 * 
 * @author Karol Babioch
 * @author Fabian Prasser
 */
public class ImportWizardPageTable extends WizardPage {
    
    /**
     * Returns a human readable string representation of <code>rows</code>
     *
     * This converts rows into a human readable string, e.g. 1000000 gets
     * converted to 1M.
     *
     * The code is based upon <a href="http://bit.ly/1m4UetX">this</a> snippet.
     *
     * @param rows The number of rows to be converted
     *
     * @return Human readable string representation of <code>rows</code>
     */
    private static String getHumanReadableCount(long rows) {
        
        int unit = 1000;
        if (rows < unit) {
            return String.valueOf(rows);
        } else {
            int exp = (int) (Math.log(rows) / Math.log(unit));
            char pre = "kMGTPE".charAt(exp - 1); //$NON-NLS-1$
            return String.format("%.1f%s", rows / Math.pow(unit, exp), pre); //$NON-NLS-1$
        }
    }
    
    /** Reference to the wizard containing this page. */
    private ImportWizard wizardImport;
    
    /** SWT Widgets */
    private Table        table;
    
    /** SWT Widgets */
    private TableViewer  tableViewer;
    
    /**
     * Creates a new instance of this page and sets its title and description.
     *
     * @param wizardImport Reference to wizard containing this page
     */
    public ImportWizardPageTable(ImportWizard wizardImport) {
        
        super("WizardImportTablePage"); //$NON-NLS-1$
        this.wizardImport = wizardImport;
        this.setTitle(Resources.getMessage("ImportWizardPageTable.3")); //$NON-NLS-1$
        this.setDescription(Resources.getMessage("ImportWizardPageTable.4")); //$NON-NLS-1$
    }
    
    /**
     * Creates the design of this page along with the appropriate listeners.
     *
     * @param parent
     */
    public void createControl(Composite parent) {
        
        Composite container = new Composite(parent, SWT.NULL);
        
        setControl(container);
        container.setLayout(new GridLayout(1, false));
        
        /* TableViewer for the detected tables */
        tableViewer = SWTUtil.createTableViewer(container, SWT.BORDER | SWT.FULL_SELECTION);
        tableViewer.setContentProvider(new ArrayContentProvider());
        ColumnViewerToolTipSupport.enableFor(tableViewer, ToolTip.NO_RECREATE);
        tableViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            
            /**
             * Reads in the columns and preview data for selected table
             */
            @Override
            public void selectionChanged(SelectionChangedEvent arg0) {
                
                /* Save selected table */
                int index = table.getSelectionIndex();
                String selectedTable = wizardImport.getData()
                                                   .getJdbcTables()
                                                   .get(index);
                wizardImport.getData().setSelectedJdbcTable(selectedTable);
                
                readColumns();
                setPageComplete(readPreview());
            }
        });
        
        /* Table for {@link #tableViewer} */
        table = tableViewer.getTable();
        table.setHeaderVisible(true);
        table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
        
        /* Column for table names */
        TableViewerColumn tableViewerColumnName = new TableViewerColumn(tableViewer,
                                                                        SWT.NONE);
        tableViewerColumnName.setLabelProvider(new ColumnLabelProvider() {
            
            /** Returns table name */
            @Override
            public String getText(Object element) {
                return (String) element;
            }
        });
        
        TableColumn tblclmnColumnName = tableViewerColumnName.getColumn();
        tblclmnColumnName.setToolTipText(Resources.getMessage("ImportWizardPageTable.5")); //$NON-NLS-1$
        tblclmnColumnName.setWidth(300);
        tblclmnColumnName.setText(Resources.getMessage("ImportWizardPageTable.6")); //$NON-NLS-1$
        
        TableViewerColumn tableViewerColumnColumns = new TableViewerColumn(tableViewer,
                                                                           SWT.NONE);
        tableViewerColumnColumns.setLabelProvider(new ColumnLabelProvider() {
            
            /**
             * Returns number of columns for table
             * 
             * If the number of columns couldn't be determined, three question
             * marks are returned.
             */
            @Override
            public String getText(Object element) {
                
                int columns = getNumberOfColumns((String) element);
                if (columns != -1) {
                    return "" + columns; //$NON-NLS-1$
                } else {
                    return "???"; //$NON-NLS-1$
                }
            }
            
        });
        
        TableColumn tblclmnColumns = tableViewerColumnColumns.getColumn();
        tblclmnColumns.setToolTipText(Resources.getMessage("ImportWizardPageTable.9")); //$NON-NLS-1$
        tblclmnColumns.setWidth(100);
        tblclmnColumns.setText(Resources.getMessage("ImportWizardPageTable.10")); //$NON-NLS-1$
        
        TableViewerColumn tableViewerColumnRows = new TableViewerColumn(tableViewer, SWT.NONE);
        tableViewerColumnRows.setLabelProvider(new ColumnLabelProvider() {
            
            /**
             * Returns number of rows for table
             * 
             * If the number of rows couldn't be determined, three question
             * marks are returned.
             */
            @Override
            public String getText(Object element) {
                
                long rows = getNumberOfRows((String) element);
                if (rows != -1) {
                    return " ~ " + getHumanReadableCount(rows); //$NON-NLS-1$
                } else {
                    return "???"; //$NON-NLS-1$
                }
            }
            
            /**
             * Returns the exact number of rows as tooltip
             *
             * This will return the exact number of rows for tables with a
             * row count greater than thousand, as the column itself will
             * only show a human readable string.
             *
             * @see #getText(Object)
             * @see #getNumberOfRows(String)
             */
            @Override
            public String getToolTipText(Object element) {
                
                long rows = getNumberOfRows((String) element);
                if (rows > 1000) {
                    return "" + rows; //$NON-NLS-1$
                } else {
                    return null;
                }
            }
        });
        
        TableColumn tblclmnRows = tableViewerColumnRows.getColumn();
        tblclmnRows.setToolTipText(Resources.getMessage("ImportWizardPageTable.14")); //$NON-NLS-1$
        tblclmnRows.setWidth(100);
        tblclmnRows.setText(Resources.getMessage("ImportWizardPageTable.15")); //$NON-NLS-1$
        
        setPageComplete(false);
    }
    
    /**
     * Applies previously detected tables to {@link #tableViewer}.
     *
     * @param visible
     */
    @Override
    public void setVisible(boolean visible) {
        
        super.setVisible(visible);
        
        if (visible) {
            tableViewer.setInput(wizardImport.getData().getJdbcTables());
            tableViewer.getControl().redraw();
            /* Mark page as complete when table has been selected before */
            if (wizardImport.getData().getSelectedJdbcTable() != null) {
                setPageComplete(true);
            }
        } else {
            setPageComplete(false);
        }
    }
    
    /**
     * Gets the number of columns for given table
     *
     * This uses the JDBC connection {@link ImportWizardModel#getJdbcConnection()} to determine the number of
     * columns for given table.
     *
     * @param table
     *            Table number of rows should be returned for
     *
     * @return Number of rows for given table, -1 in case of error
     */
    private int getNumberOfColumns(String selectedTable) {
        ResultSet rs = null;
        int i = 0;
        
        try {
            String table = selectedTable;
            String schema = null;
            if (selectedTable.contains(".")) {
                table = selectedTable.split("\\.")[1];
                schema = selectedTable.split("\\.")[0];
            }
            
            
            Connection connection = wizardImport.getData().getJdbcConnection();
            rs = connection.getMetaData().getColumns(null,
                                                     schema,
                                                     table,
                                                     null);
            while (rs.next()) {
                i++;
            }

            return i;
            
        } catch (SQLException e) {
            /* Ignore silently */
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException e) {
                /* Ignore silently */
            }
        }
        
        return -1;
    }
    
    /**
     * Gets the number of rows for given table
     * 
     * This uses the JDBC connection {@link ImportWizardModel#getJdbcConnection()} to determine the number of
     * rows for given table.
     * 
     * @param table
     *            Table number of rows should be returned for
     * 
     * @return Number of rows for given table, -1 in case of error
     */
    private long getNumberOfRows(String table) {
        
        Statement statement = null;
        ResultSet resultSet = null;
        
        try {
            statement = wizardImport.getData()
                                    .getJdbcConnection()
                                    .createStatement();
            statement.execute("SELECT COUNT(*) FROM " + table); //$NON-NLS-1$
            resultSet = statement.getResultSet();
            
            if (resultSet.next()) {
                return resultSet.getLong(1);
            }
            
        } catch (SQLException e) {
            /* Ignore silently */
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
            } catch (SQLException e) {
                /* Ignore silently */
            }
            try {
                if (statement != null) {
                    statement.close();
                }
            } catch (SQLException e) {
                /* Ignore silently */
            }
        }
        
        return -1L;
    }
    
    /**
     * Reads in the columns of currently selected table
     * 
     * If this can be performed successful, the columns will be made available
     * for the next page by {@link ImportWizardModel#setWizardColumns(List)}.
     * Otherwise an appropriate error message is set.
     */
    private void readColumns() {
        
        String selectedTable = wizardImport.getData().getSelectedJdbcTable();
        Connection connection = wizardImport.getData().getJdbcConnection();
        List<ImportWizardModelColumn> columns = new ArrayList<ImportWizardModelColumn>();
        
        int i = 0;
        ResultSet rs = null;
        try {
            
            String table = selectedTable;
            String schema = null;
            if (selectedTable.contains(".")) {
                table = selectedTable.split("\\.")[1];
                schema = selectedTable.split("\\.")[0];
            }
            
            rs = connection.getMetaData().getColumns(null,
                                                     schema,
                                                     table,
                                                     null);
            
            while (rs.next()) {
                ImportColumnJDBC column = new ImportColumnJDBC(i++,
                                                               IOUtil.trim(rs.getString("COLUMN_NAME")), //$NON-NLS-1$
                                                               DataType.STRING);
                columns.add(new ImportWizardModelColumn(column));
            }
            
        } catch (SQLException e) {
            setErrorMessage(Resources.getMessage("ImportWizardPageTable.17")); //$NON-NLS-1$
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException e) {
                /* Ignore silently */
            }
        }
        
        wizardImport.getData().setWizardColumns(columns);
    }
    
    /**
     * Reads in the preview data for currently selected table
     * 
     * If this can be performed successful, the preview data will be made
     * available for the following pages by {@link ImportWizardModel#setPreviewData(List)}.
     * Otherwise an appropriate error message is set.
     */
    private boolean readPreview() {
        
        String selectedTable = wizardImport.getData().getSelectedJdbcTable();
        Connection connection = wizardImport.getData().getJdbcConnection();
        Statement statement = null;
        ResultSet rs = null;
        
        try {
            
            statement = connection.createStatement();
            statement.setMaxRows(ImportWizardModel.PREVIEW_MAX_LINES);
            statement.execute("SELECT * FROM " + selectedTable); //$NON-NLS-1$
            rs = statement.getResultSet();
            
            List<String[]> previewData = new ArrayList<String[]>();
            while (rs.next()) {
                String[] previewRow = new String[rs.getMetaData().getColumnCount()];
                for (int j = 0; j < previewRow.length; j++) {
                    previewRow[j] = IOUtil.trim(rs.getString(j + 1));
                }
                previewData.add(previewRow);
            }
            wizardImport.getData().setPreviewData(previewData);
            if (previewData.isEmpty()) {
                setErrorMessage(Resources.getMessage("ImportWizardPageTable.22")); //$NON-NLS-1$
                return false;
            } else {
                setErrorMessage(null);
                setMessage(Resources.getMessage("ImportWizardPageTable.23"), INFORMATION); //$NON-NLS-1$
                return true;
            }
        } catch (SQLException e) {
            wizardImport.getData().setPreviewData(new ArrayList<String[]>());
            setErrorMessage(Resources.getMessage("ImportWizardPageTable.21")); //$NON-NLS-1$
            return false;
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException e) {
                /* Ignore silently */
            }
            try {
                if (statement != null) {
                    statement.close();
                }
            } catch (SQLException e) {
                /* Ignore silently */
            }
        }
    }
}
