/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.openjpa.jdbc.kernel;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.MappingRepository;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.sql.LogicalUnion;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.SelectExecutor;
import org.apache.openjpa.jdbc.sql.SelectImpl;
import org.apache.openjpa.jdbc.sql.Union;
import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.PreparedQuery;
import org.apache.openjpa.kernel.PreparedQueryCache.Exclusion;
import org.apache.openjpa.kernel.Query;
import org.apache.openjpa.kernel.QueryImpl;
import org.apache.openjpa.kernel.QueryLanguages;
import org.apache.openjpa.kernel.StoreQuery;
import org.apache.openjpa.kernel.exps.Parameter;
import org.apache.openjpa.kernel.exps.QueryExpressions;
import org.apache.openjpa.lib.rop.RangeResultObjectProvider;
import org.apache.openjpa.lib.rop.ResultList;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.UserException;

/**
 * Implements {@link PreparedQuery} for SQL queries.
 * PreparedQuery holds the post-compilation and post-execution state of a kernel Query.
 * The post-execution internal state of a query is appended as a <em>user object</em>
 * to the user-visible result to maintain the API contract.
 *
 * @author Pinaki Poddar
 *
 */
public class PreparedQueryImpl implements PreparedQuery {
    private static Localizer _loc =
        Localizer.forPackage(PreparedQueryImpl.class);

    private final String _id;
    private String _sql;
    private boolean _initialized;

    // Post-compilation state of an executable query, populated on construction
    private Class<?> _candidate;
    private boolean _subclasses;

    // post-execution state of a query
    private QueryExpressions[] _exps;
    private Class<?>[] _projTypes;

    // Position of the user defined parameters in the _params list
    private Map<Object, Integer[]>    _userParamPositions;
    private Map<Integer, Object> _template;
    private SelectImpl select;

    /**
     * Construct.
     *
     * @param id an identifier for this query to be used as cache key
     * @param compiled a compiled query
     */
    public PreparedQueryImpl(String id, Query compiled) {
        this(id, null, compiled);
    }

    /**
     * Construct.
     *
     * @param id an identifier for this query to be used as cache key
     * @param corresponding data store language query string
     * @param compiled a compiled query
     */
    public PreparedQueryImpl(String id, String sql, Query compiled) {
        this._id = id;
        this._sql = sql;
        if (compiled != null) {
            _candidate    = compiled.getCandidateType();
            _subclasses   = compiled.hasSubclasses();
        }
    }

    @Override
    public String getIdentifier() {
        return _id;
    }

    @Override
    public String getLanguage() {
        return QueryLanguages.LANG_PREPARED_SQL;
    }

    /**
     * Get the original query string which is same as the identifier of this
     * receiver.
     */
    @Override
    public String getOriginalQuery() {
        return getIdentifier();
    }

    @Override
    public String getTargetQuery() {
        return _sql;
    }

    void setTargetQuery(String sql) {
        _sql = sql;
    }

    @Override
    public boolean isInitialized() {
        return _initialized;
    }

    public QueryExpressions[] getQueryExpressions() {
        return _exps;
    }

    public Class[] getProjectionTypes() {
        return _projTypes;
    }

    /**
     * Pours the post-compilation state held by this receiver to the given
     * query.
     */
    @Override
    public void setInto(Query q) {
    	q.setQuery(_id);
        q.setCandidateType(_candidate, _subclasses);
    }

    /**
     * Initialize this receiver with post-execution result.
     * The input argument is processed only if it is a {@link ResultList} with
     * an attached {@link SelectResultObjectProvider} as its
     * {@link ResultList#getUserObject() user object}.
     *
     * @return an exclusion if can not be initialized for some reason.
     * null if initialization is successful.
     */
    @Override
    public Exclusion initialize(Object result) {
        if (isInitialized())
            return null;
        Object[] extract = extractSelectExecutor(result);
        SelectExecutor selector = (SelectExecutor)extract[0];
        if (selector == null)
            return new PreparedQueryCacheImpl.StrongExclusion(_id, ((Localizer.Message)extract[1]).getMessage());
        if (selector == null || selector.hasMultipleSelects()
            || ((selector instanceof Union)
            && (((Union)selector).getSelects().length != 1)))
            return new PreparedQueryCacheImpl.StrongExclusion(_id, _loc.get("exclude-multi-select", _id).getMessage());
        select = extractImplementation(selector);
        if (select == null)
            return new PreparedQueryCacheImpl.StrongExclusion(_id, _loc.get("exclude-no-select", _id).getMessage());
        SQLBuffer buffer = selector.getSQL();
        if (buffer == null)
            return new PreparedQueryCacheImpl.StrongExclusion(_id, _loc.get("exclude-no-sql", _id).getMessage());
        if (isUsingFieldStrategy())
            return new PreparedQueryCacheImpl.StrongExclusion(_id,
                _loc.get("exclude-user-strategy", _id).getMessage());

        if (isPaginated())
            return new PreparedQueryCacheImpl.StrongExclusion(_id,
                _loc.get("exclude-pagination", _id).getMessage());

        setTargetQuery(buffer.getSQL());
        setParameters(buffer.getParameters());
        setUserParameterPositions(buffer.getUserParameters());
        _initialized = true;

        return null;
    }

    /**
     * Extract the underlying SelectExecutor from the given argument, if possible.
     *
     * @return two objects in an array. The element at index 0 is SelectExecutor,
     * if it can be extracted. The element at index 1 is the reason why it can
     * not be extracted.
     */
    private Object[] extractSelectExecutor(Object result) {
        if (!(result instanceof ResultList))
            return new Object[]{null, _loc.get("exclude-not-result", _id)};
        Object userObject = ((ResultList<?>)result).getUserObject();
        if (userObject == null || !userObject.getClass().isArray() || ((Object[])userObject).length != 2)
            return new Object[]{null, _loc.get("exclude-no-user-object", _id)};
        Object provider = ((Object[])userObject)[0];
        Object executor = ((Object[])userObject)[1];
        if (!(executor instanceof StoreQuery.Executor))
            return new Object[]{null, _loc.get("exclude-not-executor", _id)};
        _exps = ((StoreQuery.Executor)executor).getQueryExpressions();
        for (QueryExpressions exp : _exps) {
            if (exp.hasInExpression)
                return new Object[]{null, _loc.get("exclude-in-expression", _id)};
            if (isUsingExternalizedParameter(exp)) {
                return new Object[]{null, _loc.get("exclude-externalized-param", _id)};
            }
        }
        if (_exps[0].projections.length == 0) {
            _projTypes = StoreQuery.EMPTY_CLASSES;
        } else {
            _projTypes = new Class[_exps[0].projections.length];
            for (int i = 0; i < _exps[0].projections.length; i++) {
                _projTypes[i] = _exps[0].projections[i].getType();
            }
        }
        if (provider instanceof QueryImpl.PackingResultObjectProvider) {
            provider = ((QueryImpl.PackingResultObjectProvider)provider).getDelegate();
        }
        if (provider instanceof RangeResultObjectProvider) {
            provider = ((RangeResultObjectProvider)provider).getDelegate();
        }
        if (provider instanceof SelectResultObjectProvider) {
            return new Object[]{((SelectResultObjectProvider)provider).getSelect(), null};
        }
        return new Object[]{null, _loc.get("exclude-not-select-rop", _id, provider.getClass().getName())};
    }

    private SelectImpl extractImplementation(SelectExecutor selector) {
        if (selector == null)
            return null;
        if (selector instanceof SelectImpl)
            return (SelectImpl)selector;
        if (selector instanceof LogicalUnion.UnionSelect)
            return ((LogicalUnion.UnionSelect)selector).getDelegate();
        if (selector instanceof Union)
            return extractImplementation(((Union)selector).getSelects()[0]);

        return null;
    }

    private boolean isUsingExternalizedParameter(QueryExpressions exp) {
        if (exp == null)
            return false;
        List<FieldMetaData> fmds = exp.getParameterizedFields();
        if (fmds == null || fmds.isEmpty())
            return false;
        for (FieldMetaData fmd : fmds) {
            if (fmd.isExternalized())
                return true;
        }
        return false;
    }

    private boolean isPaginated() {
        if (select instanceof SelectImpl) {
            if (((SelectImpl)select).getStartIndex() != 0 ||
                ((SelectImpl)select).getEndIndex() != Long.MAX_VALUE)
                return true;
        }
        return false;
    }
    private boolean isUsingFieldStrategy() {
        for (QueryExpressions exp : _exps) {
            if (isUsingFieldStrategy(exp)) {
                return true;
            }
        }
        return false;
    }

    private boolean isUsingFieldStrategy(QueryExpressions exp) {
        if (exp == null)
            return false;
        List<FieldMetaData> fmds = exp.getParameterizedFields();
        if (fmds == null || fmds.isEmpty())
            return false;
        for (FieldMetaData fmd : fmds) {
            if (((FieldMapping)fmd).getMappingInfo().getStrategy() != null)
                return true;
        }
        return false;
    }

    /**
     * Merge the given user parameters with its own parameter. The given map
     * must be compatible with the user parameters extracted during
     * {@link #initialize(Object) initialization}.
     *
     * @return 0-based parameter index mapped to corresponding values.
     *
     */
    @Override
    public Map<Integer, Object> reparametrize(Map user, Broker broker) {
        if (!isInitialized())
            throw new InternalException("reparameterize() on uninitialized.");
        if (user == null || user.isEmpty()) {
            if (!_userParamPositions.isEmpty()) {
                throw new UserException(_loc.get("uparam-null",
                    _userParamPositions.keySet(), this));
            } else {
                return _template;
            }
        }
        if (!_userParamPositions.keySet().equals(user.keySet())) {
            throw new UserException(_loc.get("uparam-mismatch",
                _userParamPositions.keySet(), user.keySet(), this));
        }
        Map<Integer, Object> result = new HashMap<>(_template);

        Set<Map.Entry<Object,Object>> userSet = user.entrySet();
        for (Map.Entry<Object,Object> userEntry : userSet) {
            Object key = userEntry.getKey();
            Integer[] indices = _userParamPositions.get(key);
            if (indices == null || indices.length == 0)
                throw new UserException(_loc.get("uparam-no-pos", key, this));
            Object val = userEntry.getValue();
            if (ImplHelper.isManageable(val)) {
                setPersistenceCapableParameter(result, val, indices, broker);
            } else if (val instanceof Collection) {
                setCollectionValuedParameter(result, (Collection)val, indices,
                    key, broker);
            } else {
                for (int j : indices) {
                    if (val instanceof Enum) {
                        if (_template.get(j) instanceof Integer) {
                            val = ((Enum)val).ordinal();
                        } else {
                            val = ((Enum)val).name();
                        }
                    }
                    result.put(j, val);
                }
            }
        }
        return result;
    }

    /**
     * Calculate primary key identity value(s) of the given manageable instance
     * and fill in the given map.
     *
     * @param values a map of integer parameter index to parameter value
     * @param pc a manageable instance
     * @param indices the indices of the column values
     * @param broker used to obtain the primary key values
     */
    private void setPersistenceCapableParameter(Map<Integer,Object> result,
        Object pc, Integer[] indices, Broker broker) {
        JDBCStore store = (JDBCStore)broker.getStoreManager()
            .getInnermostDelegate();
        MappingRepository repos = store.getConfiguration()
            .getMappingRepositoryInstance();
        ClassMapping mapping = repos.getMapping(pc.getClass(),
            broker.getClassLoader(), true);
        Column[] pks = mapping.getPrimaryKeyColumns();
        Object cols = mapping.toDataStoreValue(pc, pks, store);
        if (cols instanceof Object[]) {
            Object[] array = (Object[])cols;
            int n = array.length;
            if (n > indices.length || indices.length%n != 0)
                throw new UserException(_loc.get("uparam-pc-key",
                    pc.getClass(), n, Arrays.toString(indices)));
            int k = 0;
            for (int j : indices) {
                result.put(j, array[k%n]);
                k++;
            }
        } else {
            for (int j : indices) {
                result.put(j, cols);
            }
        }
    }

    private void setCollectionValuedParameter(Map<Integer,Object> result,
        Collection values, Integer[] indices, Object param, Broker broker) {
        int n = values.size();
        Object[] array = values.toArray();
        if (n == 0 || n > indices.length || indices.length%n != 0) {
            throw new UserException(_loc.get("uparam-coll-size", param, values,
                Arrays.toString(indices)));
        }
        int k = 0;
        for (int j : indices) {
            Object val = array[k%n];
            if (ImplHelper.isManageable(val))
                setPersistenceCapableParameter(result, val, indices, broker);
            else
                result.put(j, val);
            k++;
        }

    }
    /**
     * Marks the positions and keys of user parameters.
     *
     * @param list even elements are numbers representing the position of a
     * user parameter in the _param list. Odd elements are the user parameter
     * key. A user parameter key may appear more than once.
     */
    void setUserParameterPositions(List list) {
        _userParamPositions = new HashMap<>();
        List<Integer> positions = new ArrayList<>();
        for (int i = 1; list != null && i < list.size(); i += 2) {
            Object key = ((Parameter)list.get(i)).getParameterKey();
            positions.clear();
            for (int j = 1; j < list.size(); j += 2) {
                Object other = ((Parameter)list.get(j)).getParameterKey();
                if (key.equals(other))
                    positions.add((Integer)list.get(j-1));
            }
            _userParamPositions.put(key, positions.toArray(new Integer[positions.size()]));
        }
    }

    void setParameters(List list) {
        Map<Integer, Object> tmp = new HashMap<>();
        for (int i = 0; list != null && i < list.size(); i++) {
            tmp.put(i, list.get(i));
        }
        _template = Collections.unmodifiableMap(tmp);
    }

    SelectImpl getSelect() {
        return select;
    }

    @Override
    public String toString() {
        return "PreparedQuery: [" + getOriginalQuery() + "] --> [" +
               getTargetQuery() + "]";
    }
}
