/*
   Copyright (c) 2009, 2011, Oracle and/or its affiliates. All rights reserved.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is also distributed with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have included with MySQL.

   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 General Public License, version 2.0, for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
*/

package com.mysql.clusterj.core.query;

import com.mysql.clusterj.ClusterJException;
import com.mysql.clusterj.ClusterJFatalInternalException;
import com.mysql.clusterj.ClusterJUserException;
import com.mysql.clusterj.Query;

import com.mysql.clusterj.core.query.PredicateImpl.ScanType;
import com.mysql.clusterj.core.spi.DomainFieldHandler;
import com.mysql.clusterj.core.spi.DomainTypeHandler;
import com.mysql.clusterj.core.spi.QueryExecutionContext;
import com.mysql.clusterj.core.spi.SessionSPI;
import com.mysql.clusterj.core.spi.ValueHandler;

import com.mysql.clusterj.core.store.Index;
import com.mysql.clusterj.core.store.IndexOperation;
import com.mysql.clusterj.core.store.IndexScanOperation;
import com.mysql.clusterj.core.store.Operation;
import com.mysql.clusterj.core.store.ResultData;
import com.mysql.clusterj.core.store.ScanOperation;

import com.mysql.clusterj.core.util.I18NHelper;
import com.mysql.clusterj.core.util.Logger;
import com.mysql.clusterj.core.util.LoggerFactoryService;

import com.mysql.clusterj.query.Predicate;
import com.mysql.clusterj.query.PredicateOperand;
import com.mysql.clusterj.query.QueryDefinition;
import com.mysql.clusterj.query.QueryDomainType;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class QueryDomainTypeImpl<T> implements QueryDomainType<T> {

    /** My message translator */
    static final I18NHelper local = I18NHelper.getInstance(QueryDomainTypeImpl.class);

    /** My logger */
    static final Logger logger = LoggerFactoryService.getFactory().getInstance(QueryDomainTypeImpl.class);

    /** My class. */
    protected Class<T> cls;

    /** My DomainTypeHandler. */
    protected DomainTypeHandler<T> domainTypeHandler;

    /** My where clause. */
    protected PredicateImpl where;

    /** My parameters. These encapsulate the parameter names not the values. */
    protected Map<String, ParameterImpl> parameters =
            new HashMap<String, ParameterImpl>();

    /** My properties. These encapsulate the property names not the values. */
    protected Map<String, PropertyImpl> properties =
            new HashMap<String, PropertyImpl>();

    public QueryDomainTypeImpl(DomainTypeHandler<T> domainTypeHandler, Class<T> cls) {
        this.cls = cls;
        this.domainTypeHandler = domainTypeHandler;
    }

    public QueryDomainTypeImpl(DomainTypeHandler<T> domainTypeHandler) {
        this.domainTypeHandler = domainTypeHandler;
    }

    public PredicateOperand get(String propertyName) {
        // if called multiple times for the same property,
        // return the same PropertyImpl instance
        PropertyImpl property = properties.get(propertyName);
        if (property != null) {
            return property;
        } else {
            DomainFieldHandler fmd = domainTypeHandler.getFieldHandler(propertyName);
            property = new PropertyImpl(this, fmd);
            properties.put(propertyName, property);
            return property;
        }
    }

    /** Set the where clause. Mark parameters used by this query.
     * @param predicate the predicate
     * @return the query definition (this)
     */
    public QueryDefinition<T> where(Predicate predicate) {
        if (predicate == null) {
            throw new ClusterJUserException(
                    local.message("ERR_Query_Where_Must_Not_Be_Null"));
        }
        if (!(predicate instanceof PredicateImpl)) {
            throw new UnsupportedOperationException(
                    local.message("ERR_NotImplemented"));
        }
        // if a previous where clause, unmark the parameters
        if (where != null) {
            where.unmarkParameters();
            where = null;
        }
        this.where = (PredicateImpl)predicate;
        where.markParameters();
        return this;
    }

    public PredicateOperand param(String parameterName) {
        final ParameterImpl parameter = parameters.get(parameterName);
        if (parameter != null) {
            return parameter;
        } else {
            ParameterImpl result = new ParameterImpl(this, parameterName);
            parameters.put(parameterName, result);
            return result;
        }
    }

    /** Convenience method to negate a predicate.
     * @param predicate the predicate to negate
     * @return the inverted predicate
     */
    public Predicate not(Predicate predicate) {
        return predicate.not();
    }

    /** Query.getResultList delegates to this method.
     * 
     * @return the results of executing the query
     */
    public List<T> getResultList(QueryExecutionContext context) {
        assertAllParametersBound(context);

        SessionSPI session = context.getSession();
        session.startAutoTransaction();
        // set up results and table information
        List<T> resultList = new ArrayList<T>();
        try {
            // execute the query
            ResultData resultData = getResultData(context);
            // put the result data into the result list
            while (resultData.next()) {
                T row = (T) session.newInstance(cls);
                ValueHandler handler =domainTypeHandler.getValueHandler(row);
                // set values from result set into object
                domainTypeHandler.objectSetValues(resultData, handler);
                resultList.add(row);
            }
            session.endAutoTransaction();
            return resultList;
        } catch (ClusterJException ex) {
            session.failAutoTransaction();
            throw ex;
        } catch (Exception ex) {
            session.failAutoTransaction();
            throw new ClusterJException(
                    local.message("ERR_Exception_On_Query"), ex);
        }
    }

    /** Execute the query and return the result data. The type of operation
     * (primary key lookup, unique key lookup, index scan, or table scan) 
     * depends on the where clause and the bound parameter values.
     * 
     * @param context the query context, including the bound parameters
     * @return the raw result data from the query
     * @throws ClusterJUserException if not all parameters are bound
     */
    public ResultData getResultData(QueryExecutionContext context) {
	SessionSPI session = context.getSession();
        // execute query based on what kind of scan is needed
        // if no where clause, scan the entire table
        CandidateIndexImpl index = where==null?
            CandidateIndexImpl.getIndexForNullWhereClause():
            where.getBestCandidateIndex(context);
        ScanType scanType = index.getScanType();
        Map<String, Object> explain = newExplain(index, scanType);
        context.setExplain(explain);
        ResultData result = null;
        Index storeIndex;

        switch (scanType) {

            case PRIMARY_KEY: {
                // perform a select operation
                Operation op = session.getSelectOperation(domainTypeHandler.getStoreTable());
                // set key values into the operation
                index.operationSetKeys(context, op);
                // set the expected columns into the operation
                domainTypeHandler.operationGetValues(op);
                // execute the select and get results
                result = op.resultData();
                break;
            }

            case INDEX_SCAN: {
                storeIndex = index.getStoreIndex();
                if (logger.isDetailEnabled()) logger.detail("Using index scan with index " + index.getIndexName());
                IndexScanOperation op;
                // perform an index scan operation
                if (index.isMultiRange()) {
                    op = session.getIndexScanOperationMultiRange(storeIndex, domainTypeHandler.getStoreTable());
                    
                } else {
                    op = session.getIndexScanOperation(storeIndex, domainTypeHandler.getStoreTable());
                    
                }
                // set the expected columns into the operation
                domainTypeHandler.operationGetValues(op);
                // set the bounds into the operation
                index.operationSetBounds(context, op);
                // set additional filter conditions
                where.filterCmpValue(context, op);
                // execute the scan and get results
                result = op.resultData();
                break;
            }

            case TABLE_SCAN: {
                if (logger.isDetailEnabled()) logger.detail("Using table scan");
                // perform a table scan operation
                ScanOperation op = session.getTableScanOperation(domainTypeHandler.getStoreTable());
                // set the expected columns into the operation
                domainTypeHandler.operationGetValues(op);
                // set the bounds into the operation
                if (where != null) {
                    where.filterCmpValue(context, op);
                }
                // execute the scan and get results
                result = op.resultData();
                break;
            }

            case UNIQUE_KEY: {
                storeIndex = index.getStoreIndex();
                if (logger.isDetailEnabled()) logger.detail("Using unique lookup with index " + index.getIndexName());
                // perform a unique lookup operation
                IndexOperation op = session.getUniqueIndexOperation(storeIndex, domainTypeHandler.getStoreTable());
                // set the keys of the indexName into the operation
                where.operationEqual(context, op);
                // set the expected columns into the operation
                //domainTypeHandler.operationGetValuesExcept(op, indexName);
                domainTypeHandler.operationGetValues(op);
                // execute the select and get results
                result = op.resultData();
                break;
            }

            default:
                session.failAutoTransaction();
                throw new ClusterJFatalInternalException(
                        local.message("ERR_Illegal_Scan_Type", scanType));
        }
        context.deleteFilters();
        return result;
    }

    /** Delete the instances that satisfy the query and return the number
     * of instances deleted. The type of operation used to find the instances
     * (primary key lookup, unique key lookup, index scan, or table scan) 
     * depends on the where clause and the bound parameter values.
     * 
     * @param context the query context, including the bound parameters
     * @return the number of instances deleted
     * @throws ClusterJUserException if not all parameters are bound
     */
    public int deletePersistentAll(QueryExecutionContext context) {
                                SessionSPI session = context.getSession();
        // calculate what kind of scan is needed
        // if no where clause, scan the entire table
        CandidateIndexImpl index = where==null?
            CandidateIndexImpl.getIndexForNullWhereClause():
            where.getBestCandidateIndex(context);
        ScanType scanType = index.getScanType();
        Map<String, Object> explain = newExplain(index, scanType);
        context.setExplain(explain);
        int result = 0;
        int errorCode = 0;
        Index storeIndex;
        session.startAutoTransaction();

        try {
            switch (scanType) {

                case PRIMARY_KEY: {
                    // perform a delete by primary key operation
                    if (logger.isDetailEnabled()) logger.detail("Using delete by primary key.");
                    Operation op = session.getDeleteOperation(domainTypeHandler.getStoreTable());
                    // set key values into the operation
                    index.operationSetKeys(context, op);
                    // execute the delete operation
                    session.executeNoCommit(false, true);
                    errorCode = op.errorCode();
                    // a non-zero result means the row was not deleted
                    result = (errorCode == 0?1:0);
                    break;
                }

                case UNIQUE_KEY: {
                    storeIndex = index.getStoreIndex();
                    if (logger.isDetailEnabled()) logger.detail(
                            "Using delete by unique key  " + index.getIndexName());
                    // perform a delete by unique key operation
                    IndexOperation op = session.getUniqueIndexDeleteOperation(storeIndex,
                            domainTypeHandler.getStoreTable());
                    // set the keys of the indexName into the operation
                    where.operationEqual(context, op);
                    // execute the delete operation
                    session.executeNoCommit(false, true);
                    errorCode = op.errorCode();
                    // a non-zero result means the row was not deleted
                    result = (errorCode == 0?1:0);
                    break;
                }

                case INDEX_SCAN: {
                    storeIndex = index.getStoreIndex();
                    if (logger.isDetailEnabled()) logger.detail(
                            "Using delete by index scan with index " + index.getIndexName());
                    // perform an index scan operation
                    IndexScanOperation op = session.getIndexScanDeleteOperation(storeIndex,
                            domainTypeHandler.getStoreTable());
                    // set the expected columns into the operation
                    domainTypeHandler.operationGetValues(op);
                    // set the bounds into the operation
                    index.operationSetBounds(context, op);
                    // set additional filter conditions
                    where.filterCmpValue(context, op);
                    // delete results of the scan; don't abort if no row found
                    result = session.deletePersistentAll(op, false);
                    break;
                }

                case TABLE_SCAN: {
                    if (logger.isDetailEnabled()) logger.detail("Using delete by table scan");
                    // perform a table scan operation
                    ScanOperation op = session.getTableScanDeleteOperation(domainTypeHandler.getStoreTable());
                    // set the expected columns into the operation
                    domainTypeHandler.operationGetValues(op);
                    // set the bounds into the operation
                    if (where != null) {
                        where.filterCmpValue(context, op);
                    }
                    // delete results of the scan; don't abort if no row found
                    result = session.deletePersistentAll(op, false);
                    break;
                }

                default:
                    throw new ClusterJFatalInternalException(
                            local.message("ERR_Illegal_Scan_Type", scanType));
            }
            context.deleteFilters();
            session.endAutoTransaction();
            return result;
        } catch (ClusterJException e) {
            session.failAutoTransaction();
            throw e;
        } catch (Exception e) {
            session.failAutoTransaction();
            throw new ClusterJException(local.message("ERR_Exception_On_Query"), e);
        } 
    }

    protected CandidateIndexImpl[] createCandidateIndexes() {
        return domainTypeHandler.createCandidateIndexes();
    }

    /** Explain how this query will be or was executed and store
     * the result in the context.
     * 
     * @param context the context, including bound parameters
     */
    public void explain(QueryExecutionContext context) {
        assertAllParametersBound(context);
        CandidateIndexImpl index = where==null?
                CandidateIndexImpl.getIndexForNullWhereClause():
                where.getBestCandidateIndex(context);
        ScanType scanType = index.getScanType();
        Map<String, Object> explain = newExplain(index, scanType);
        context.setExplain(explain);
    }

    /** Create a new explain for this query.
     * @param index the index used
     * @param scanType the scan type
     * @return the explain
     */
    protected Map<String, Object> newExplain(CandidateIndexImpl index,
            ScanType scanType) {
        Map<String, Object> explain = new HashMap<String, Object>();
        explain.put(Query.SCAN_TYPE, scanType.toString());
        explain.put(Query.INDEX_USED, index.getIndexName());
        return explain;
    }

    /** Assert that all parameters used by this query are bound.
     * @param context the context, including the parameter map
     * @throws ClusterJUserException if not all parameters are bound
     */
    protected void assertAllParametersBound(QueryExecutionContext context) {
        if (where != null) {
            // Make sure all marked parameters (used in the query) are bound.
            for (ParameterImpl param: parameters.values()) {
                if (param.isMarkedAndUnbound(context)) {
                    throw new ClusterJUserException(
                            local.message("ERR_Parameter_Not_Bound", param.getName()));
                }
            }
        }
    }

    public Class<T> getType() {
        return cls;
    }

}
