/*
 * ARX Data Anonymization Tool
 * Copyright 2014 - 2015 Karol Babioch, Fabian Prasser, Florian Kohlmayer
 * 
 * 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.io;

import java.io.IOException;
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 adapter for JDBC
 * 
 * This adapter can import data from JDBC sources. The source itself is
 * described by an appropriate {@link ImportConfigurationJDBC} object.
 * 
 * @author Karol Babioch
 * @author Fabian Prasser
 */
public class ImportAdapterJDBC extends ImportAdapter {
    
    /** The configuration describing the CSV file being used. */
    private ImportConfigurationJDBC config;
    
    /**
     * ResultSet containing rows to return.
     *
     * @see {@link #next()}
     */
    private ResultSet               resultSet;
    
    /** JDBC statement. */
    private Statement               statement;
    
    /** Indicates whether there is another row to return. */
    private boolean                 hasNext;
    
    /**
     * Indicates whether the first row has already been returned
     * 
     * The first row contains the name of the columns. Depending upon whether
     * the name of the column has been assigned explicitly, this is either the
     * value of the table itself, the value defined by the user.
     */
    private boolean                 headerReturned;
    
    /**
     * Number of rows that need to be processed in total.
     *
     * @see {@link #getProgress()}
     */
    private int                     totalRows;
    
    /**
     * Creates a new instance of this object with given configuration.
     *
     * @param config {@link #config}
     * @throws IOException In case of communication errors with JDBC
     * @todo Fix IOException
     */
    protected ImportAdapterJDBC(ImportConfigurationJDBC config) throws IOException {
        
        super(config);
        this.config = config;
        
        /* Preparation work */
        indexes = getIndexesToImport();
        dataTypes = getColumnDatatypes();
        
        try {
            
            /* Used to keep track of progress */
            statement = config.getConnection().createStatement();
            statement.execute("SELECT COUNT(*) FROM " + config.getTable());
            resultSet = statement.getResultSet();
            
            if (resultSet.next()) {
                
                totalRows = resultSet.getInt(1);
                if (totalRows == 0) {
                    closeResources();
                    throw new IOException("Table doesn't contain any rows");
                }
                
            } else {
                closeResources();
                throw new IOException("Couldn't determine number of rows");
            }
            
            /* Query for actual data */
            statement = config.getConnection().createStatement();
            statement.execute("SELECT * FROM " + config.getTable());
            resultSet = statement.getResultSet();
            hasNext = resultSet.next();
            
        } catch (SQLException e) {
            closeResources();
            throw new IOException(e.getMessage());
        }
        
        // Create header
        header = createHeader();
    }
    
    @Override
    public Integer getLength() {
        return totalRows;
    }
    
    /**
     * Returns the percentage of data that has already been returned
     * 
     * This divides the number of rows that have already been returned by the
     * number of total rows and casts the result into a percentage. In case of
     * an {@link SQLException} 0 will be returned.
     *
     * @return
     */
    @Override
    public int getProgress() {
        
        try {
            return (int) (((double) resultSet.getRow() / (double) totalRows) * 100d);
        } catch (SQLException e) {
            return 0;
        }
    }
    
    /**
     * Indicates whether there is another element to return
     * 
     * This returns true when there is another element in the result set {@link #resultSet}.
     *
     * @return
     */
    @Override
    public boolean hasNext() {
        return hasNext;
    }
    
    /*
     * (non-Javadoc)
     * 
     * @see java.util.Iterator#next()
     */
    @Override
    public String[] next() {
        
        /* Return header in first iteration */
        if (!headerReturned) {
            headerReturned = true;
            return header;
        }
        
        try {
            
            /* Create regular row */
            String[] result = new String[indexes.length];
            for (int i = 0; i < indexes.length; i++) {
                
                result[i] = IOUtil.trim(resultSet.getString(indexes[i]));
                if (!dataTypes[i].isValid(result[i])) {
                    if (config.columns.get(i).isCleansing()) {
                        result[i] = DataType.NULL_VALUE;
                    } else {
                        closeResources();
                        throw new IllegalArgumentException("Data value does not match data type");
                    }
                }
            }
            
            /* Move cursor forward and assign result to {@link #hasNext} */
            hasNext = resultSet.next();
            
            if (!hasNext) {
                closeResources();
            }
            
            return result;
            
        } catch (SQLException e) {
            closeResources();
            throw new RuntimeException("Couldn't retrieve data from database");
        }
    }
    
    /**
     * Dummy.
     */
    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
    
    /**
     * Closes the JDBC resources.
     */
    private void closeResources() {
        try {
            if (resultSet != null) {
                resultSet.close();
            }
        } catch (Exception e) {
            /* Ignore silently */
        }
        try {
            if (statement != null) {
                statement.close();
            }
        } catch (Exception e) {
            /* Ignore silently */
        }
        try {
            if (config.isManageConnection()) {
                config.getConnection().close();
            }
        } catch (Exception e) {
            /* Die silently */
        }
    }
    
    /**
     * Creates the header row
     * 
     * This returns a string array with the names of the columns that will be
     * returned later on by iterating over this object. Depending upon whether
     * or not names have been assigned explicitly either the appropriate values
     * will be returned, or names from the JDBC metadata will be used.
     *
     * @return
     */
    private String[] createHeader() {
        
        /* Initialization */
        String[] header = new String[config.getColumns().size()];
        List<ImportColumn> columns = config.getColumns();
        
        /* Create header */
        for (int i = 0, len = columns.size(); i < len; i++) {
            
            ImportColumn column = columns.get(i);
            
            /* Check whether name has been assigned explicitly or is nonempty */
            if ((column.getAliasName() != null) &&
                !column.getAliasName().equals("")) {
                
                header[i] = column.getAliasName();
                
            } else {
                
                /* Assign name from JDBC metadata */
                try {
                    /* +1 offset, because counting in JDBC starts at 1 */
                    header[i] = IOUtil.trim(resultSet.getMetaData().getColumnName(((ImportColumnJDBC) column).getIndex() + 1));
                } catch (SQLException e) {
                    throw new IllegalArgumentException("Index for column '" + ((ImportColumnJDBC) column).getIndex() + "' couldn't be found");
                }
            }
            column.setAliasName(header[i]);
        }
        
        /* Return header */
        return header;
        
    }

    /**
     * Returns an array with indexes of columns that should be imported
     * 
     * Only columns listed within {@link #column} will be imported. This
     * iterates over the list of columns and returns an array with indexes of
     * columns that should be imported.
     * 
     * @return Array containing indexes of columns that should be imported
     */
    protected int[] getIndexesToImport() {
        
        /* Get indexes to import from */
        ArrayList<Integer> indexes = new ArrayList<Integer>();
        for (ImportColumn column : config.getColumns()) {
            indexes.add(((ImportColumnJDBC) column).getIndex());
        }
        
        int[] result = new int[indexes.size()];
        for (int i = 0; i < result.length; i++) {
            result[i] = indexes.get(i) + 1;
        }
        
        return result;
    }
}
