/*
 * Copyright 2014, Stratio.
 *
 * 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 com.stratio.deep.jdbc.config;

import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.ComboCondition;
import com.healthmarketscience.sqlbuilder.SelectQuery;
import com.healthmarketscience.sqlbuilder.dbspec.Column;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSchema;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSpec;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable;
import com.stratio.deep.commons.config.DeepJobConfig;
import com.stratio.deep.commons.config.ExtractorConfig;
import com.stratio.deep.commons.entity.Cells;
import com.stratio.deep.commons.exception.DeepGenericException;
import com.stratio.deep.commons.filter.Filter;
import com.stratio.deep.commons.filter.FilterType;
import com.stratio.deep.jdbc.extractor.JdbcNativeCellExtractor;
import com.stratio.deep.jdbc.extractor.JdbcNativeEntityExtractor;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

import static com.stratio.deep.commons.extractor.utils.ExtractorConstants.*;

/**
 * Configuration class for Jdbc-Spark integration
 *
 * @param <T> Type of returned objects.
 */
public class JdbcDeepJobConfig<T> extends DeepJobConfig<T, JdbcDeepJobConfig<T>>
        implements IJdbcDeepJobConfig<T, JdbcDeepJobConfig<T>>, Serializable{

    private static final long serialVersionUID = -3772553194634727039L;

    /**
     * JDBC Driver class.
     */
    private Class driverClass;

    /**
     * JDBC connection url.
     */
    private String connectionUrl;

    /**
     * Database root object.
     */
    private  DbSpec dbSpec = new DbSpec();

    /**
     * Query to be executed.
     */
    private  SelectQuery query;

    /**
     * Database schema.
     */
    private  DbSchema dbSchema;

    /**
     * Database table.
     */
    private  DbTable dbTable;

    /**
     * Column used for sorting (optional).
     */
    private  DbColumn sort;

    /**
     * Column used for partitioning (must be numeric).
     */
    private  DbColumn partitionKey;

    /**
     * Partitioning upper bound.
     */
    private int upperBound = Integer.MAX_VALUE - 1;

    /**
     * Partitioning lower bound.
     */
    private int lowerBound = 0;

    /**
     * Number of partitions.
     */
    private int numPartitions = 1;

    /**
     * Quote or not schema and tables names.
     */
    private boolean quoteSql;

    /**
     * Default constructor.
     */
    public JdbcDeepJobConfig() {

    }

    /**
     * Constructor for Entity class-based configuration.
     *
     * @param entityClass
     */
    public JdbcDeepJobConfig(Class<T> entityClass) {
        super(entityClass);
        if (Cells.class.isAssignableFrom(entityClass)) {
            extractorImplClass = JdbcNativeCellExtractor.class;
        } else {
            extractorImplClass = JdbcNativeEntityExtractor.class;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> initialize() throws IllegalStateException {
        this.validate();
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> initialize(ExtractorConfig extractorConfig) {
        Map<String, Serializable> values = extractorConfig.getValues();
        if (values.get(CATALOG) != null) {
            this.database(extractorConfig.getString(CATALOG));
        }

        if (values.get(TABLE) != null) {
            this.table(extractorConfig.getString(TABLE));
        }
        super.initialize(extractorConfig);
        if (values.get(FILTER_QUERY) != null) {
            this.filters(extractorConfig.getFilterArray(FILTER_QUERY));
        }

        if (values.get(JDBC_DRIVER_CLASS) != null) {
           driverClass(extractorConfig.getString(JDBC_DRIVER_CLASS));

        }

        if (values.get(JDBC_CONNECTION_URL) != null) {
            connectionUrl(extractorConfig.getString(JDBC_CONNECTION_URL));
        }

        if (values.get(JDBC_QUOTE_SQL) != null) {
            quoteSql(extractorConfig.getBoolean(JDBC_QUOTE_SQL));
        }
        if (values.get(JDBC_PARTITION_KEY) != null) {
            partitionKey(extractorConfig.getString(JDBC_PARTITION_KEY));
        }

        if (values.get(JDBC_NUM_PARTITIONS) != null) {
            numPartitions(extractorConfig.getInteger(JDBC_NUM_PARTITIONS));
        }

        if (values.get(JDBC_PARTITIONS_LOWER_BOUND) != null) {
            lowerBound(extractorConfig.getInteger(JDBC_PARTITIONS_LOWER_BOUND));
        }

        if (values.get(JDBC_PARTITIONS_UPPER_BOUND) != null) {
            upperBound(extractorConfig.getInteger(JDBC_PARTITIONS_UPPER_BOUND));
        }
        this.initialize();

        return this;
    }

    /**
     * Validates configuration object.
     */
    private void validate() {
        if(driverClass == null) {
            throw new IllegalArgumentException("Driver class must be specified");
        }
        if(catalog == null || catalog.isEmpty()) {
            throw new IllegalArgumentException("Schema name must be specified");
        }
        if(table == null || table.isEmpty()) {
            throw new IllegalArgumentException("Table name must be specified");
        }
        if(connectionUrl == null || connectionUrl.isEmpty()) {
            if((host != null && !host.isEmpty()) && (port > 0)) {
                connectionUrl(getJdbcUrl());
            } else {
                throw new IllegalArgumentException("You must specify at least one of connectionUrl or host and port properties");
            }
        }
        if(partitionKey == null && numPartitions > 1) {
            throw new IllegalArgumentException("You must define a valid partition key for using more than one partition.");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> filters(Filter[] filters) {
        this.filters = filters;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SelectQuery getQuery() {
        SelectQuery selectQuery = new SelectQuery();
        List<DbColumn> columns = dbTable.getColumns();
        if(!columns.isEmpty()) {
            selectQuery.addColumns(columns.toArray(new Column[columns.size()]));
        } else {
            selectQuery.addAllTableColumns(dbTable);
        }
        selectQuery.addFromTable(dbTable);
        if(sort != null) {
            selectQuery.addOrderings(sort);
        }
        applyFilters(selectQuery);
        query = selectQuery;
        return query;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> connectionUrl(String connectionUrl) {
        this.connectionUrl = connectionUrl;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getConnectionUrl() {
        if(connectionUrl == null || connectionUrl.isEmpty()) {
            return getJdbcUrl();
        }
        return this.connectionUrl;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> driverClass(String driverClass) {
        try {
            this.driverClass = Class.forName(driverClass);
        } catch(ClassNotFoundException e) {
            throw new DeepGenericException("Class " + driverClass + "not found", e);
        }
        return this;
    }

    @Override
    public String getDriverClass() {
        return this.driverClass.getCanonicalName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> database(String database) {
        this.catalog = database;
        dbSchema = new DbSchema(dbSpec, catalog);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getDatabase() {
        return this.catalog;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> table(String table) {
        this.table = table;
        if(dbSchema != null) {
            dbTable = new DbTable(dbSchema, table, table);
        }
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> inputColumns(String... columns) {
        if(dbTable != null) {
            for(String column:columns) {
                dbTable.addColumn(column);
            }
        }
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> sort(String sort) {
        if(dbTable != null) {
            this.sort = new DbColumn(dbTable, sort, "",null,null);
        }
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DbColumn getSort() {
        return this.sort;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> partitionKey(String partitionKey) {
        if(dbTable != null) {
            this.partitionKey = new DbColumn(dbTable, partitionKey, "",null,null);
        }
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DbColumn getPartitionKey() {
        return this.partitionKey;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> upperBound(int upperBound) {
        this.upperBound = upperBound;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getUpperBound() {
        return this.upperBound;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> lowerBound(int lowerBound) {
        this.lowerBound = lowerBound;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getLowerBound() {
        return this.lowerBound;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> numPartitions(int numPartitions) {
        this.numPartitions = numPartitions;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getNumPartitions() {
        return this.numPartitions;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JdbcDeepJobConfig<T> quoteSql(boolean quoteSql) {
        this.quoteSql = quoteSql;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getQuoteSql() {
        return this.quoteSql;
    }

    private String getJdbcUrl() {
        StringBuilder sb = new StringBuilder();
        sb.append("jdbc:");
        sb.append(getJdbcProvider());
        sb.append("://");
        sb.append(host.get(0));
        sb.append(":");
        sb.append(port);
        sb.append("/");
        sb.append(catalog);
        sb.append("?");
        return sb.toString();
    }

    private String getJdbcProvider(){
        int firstIndex = driverClass.toString().indexOf(".");
        int secondIndex = driverClass.toString().indexOf(".", ++firstIndex);
        return driverClass.toString().substring(firstIndex, secondIndex);
    }

    private void applyFilters(SelectQuery query) {
        if(this.filters != null && this.filters.length > 0) {
            ComboCondition comboCondition = new ComboCondition(ComboCondition.Op.AND);
            if (filters.length > 0) {
                for(int i=0; i<filters.length; i++) {
                    Filter filter = filters[i];
                    FilterType filterType = filter.getFilterType();
                    DbColumn filterColumn = new DbColumn(dbTable, filter.getField(), "",null,null);
                    if(filterType.equals(FilterType.EQ)) {
                        comboCondition.addCondition(BinaryCondition.equalTo(filterColumn, filter.getValue()));
                    } else if(filterType.equals(FilterType.GT)) {
                        comboCondition.addCondition(BinaryCondition.greaterThan(filterColumn, filter.getValue(), false));
                    } else if(filterType.equals(FilterType.LT)) {
                        comboCondition.addCondition(BinaryCondition.lessThan(filterColumn, filter.getValue(), false));
                    } else if(filterType.equals(FilterType.GTE)) {
                        comboCondition.addCondition(BinaryCondition.greaterThan(filterColumn, filter.getValue(), true));
                    } else if(filterType.equals(FilterType.LTE)) {
                        comboCondition.addCondition(BinaryCondition.lessThan(filterColumn, filter.getValue(), true));
                    } else if(filterType.equals(FilterType.NEQ)) {
                        comboCondition.addCondition(BinaryCondition.notEqualTo(filterColumn, filter.getValue()));
                    } else if(filterType.equals(FilterType.IN)) {
                        ComboCondition comboConditionOR = new ComboCondition(ComboCondition.Op.OR);
                        String[] condicion =filter.getValue().toString().split(",");
                        for (int z=0; z < condicion.length ; z++) {
                            comboConditionOR.addCondition(BinaryCondition.equalTo(filterColumn, condicion[z]));
                        }
                        comboCondition.addCondition(comboConditionOR);
                    }
                    else {
                        throw new UnsupportedOperationException("Currently, the filter operation " + filterType + " is not supported");
                    }
                }
            }
            query.addCondition(comboCondition);
        }
    }

}