/*
 * 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.persistence;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.enhance.Reflection;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.Filters;
import org.apache.openjpa.kernel.QueryHints;
import org.apache.openjpa.kernel.exps.AggregateListener;
import org.apache.openjpa.kernel.exps.FilterListener;
import org.apache.openjpa.lib.conf.ProductDerivation;
import org.apache.openjpa.lib.conf.ProductDerivations;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringDistance;

/**
 * Manages query hint keys and handles their values on behalf of a owning
 * {@link QueryImpl}. Uses specific knowledge of hint keys declared in
 * different parts of the system.
 *
 * This receiver collects hint keys from different parts of the system. The
 * keys are implicitly or explicitly declared by several different mechanics.
 * This receiver sets the values on behalf of a owning {@link QueryImpl}
 * based on the its specific knowledge of these keys.
 *
 * The hint keys from following sources are collected and handled:
 *
 * 1. {@link org.apache.openjpa.kernel.QueryHints} interface declares hint keys
 *    as public static final fields. These fields are collected by reflection.
 *    The values are handled by invoking methods on the owning {@link QueryImpl}
 *
 * 2. Some hint keys are collected from bean-style property names of {@link
 *    JDBCFetchPlan} by {@link Reflection#getBeanStylePropertyNames(Class)
 *    reflection} and prefixed with <code>openjpa.FetchPlan</code>.
 *    Their values are used to set the corresponding property of {@link
 *    FetchPlan} via {@link #hintToSetter(FetchPlan, String, Object) reflection}
 *
 * 3. Currently defined <code>javax.persistence.*</code> hint keys have
 *    a equivalent counterpart to one of these FetchPlan keys.
 *    The JPA keys are mapped to equivalent FetchPlan hint keys.
 *
 * 4. Some keys directly invoke setters or add listeners to the owning
 *    {@link QueryImpl}. These hint keys are statically declared in
 *    this receiver itself.
 *
 * 5. ProductDerivation may introduce their own query hint keys via {@link
 *    ProductDerivation#getSupportedQueryHints()}. Their values are set in the
 *    {@link FetchConfiguration#setHint(String, Object)}
 *
 *  A hint key is classified into one of the following three categories:
 *
 *  1. Supported: A key is known to this receiver as collected from different
 *     parts of the system. The value of a supported key is recorded and
 *     available via {@link #getHints()} method.
 *  2. Recognized: A key is not known to this receiver but has a prefix which
 *     is known to this receiver. The value of a recognized key is not recorded
 *     but its value is available via {@link FetchConfiguration#getHint(String)}
 *  3. Unrecognized: A key is neither supported nor recognized. The value of a
 *     unrecognized key is neither recorded nor set anywhere.
 *
 *  If an incompatible value is supplied for a supported key, a non-fatal
 *  {@link ArgumentException} is raised.
 *
 * @author Pinaki Poddar
 *
 * @since 2.0.0
 *
 */
public class HintHandler  {
  protected final QueryImpl<?> owner;

    private static final Localizer _loc = Localizer.forPackage(HintHandler.class);
    protected static Set<String> _supportedHints = ProductDerivations.getSupportedQueryHints();

    protected static final String PREFIX_OPENJPA = "openjpa.";
    protected static final String PREFIX_JDBC = PREFIX_OPENJPA + "jdbc.";
    protected static final String PREFIX_FETCHPLAN = PREFIX_OPENJPA + "FetchPlan.";
    private Map<String, Object> _hints;


    HintHandler(QueryImpl<?> impl) {
        super();
        owner = impl;
    }

    /**
     * Record a key-value pair only only if the given key is supported.
     *
     * @return FALSE if the key is unrecognized.
     *         null (i.e. MAY BE) if the key is recognized, but not supported.
     *         TRUE if the key is supported.
     */
    protected Boolean record(String hint, Object value) {
        if (hint == null)
            return Boolean.FALSE;
        if (_supportedHints.contains(hint)) {
            if (_hints == null)
                _hints = new TreeMap<>();
            _hints.put(hint, value);
            return Boolean.TRUE;
        }
        if (isKnownPrefix(hint)) {
            Log log = owner.getDelegate().getBroker().getConfiguration().getLog(OpenJPAConfiguration.LOG_RUNTIME);
            String possible = StringDistance.getClosestLevenshteinDistance(hint, getSupportedHints());
            if (log.isWarnEnabled())
                log.warn(_loc.get("bad-query-hint", hint, possible));
            return null; // possible but not registered
        }
        return Boolean.FALSE; // not possible
    }

    public void setHint(String key, Object value) {
        Boolean status = record(key, value);
        if (Boolean.FALSE.equals(status))
            return;
        FetchPlan plan = owner.getFetchPlan();
        if (status == null) {
            plan.setHint(key, value);
            return;
        }

        ClassLoader loader = owner.getDelegate().getBroker().getClassLoader();
        if (QueryHints.HINT_SUBCLASSES.equals(key)) {
            if (value instanceof String)
                value = Boolean.valueOf((String) value);
            owner.setSubclasses((Boolean) value);
        } else if (QueryHints.HINT_RELAX_BIND_PARAM_TYPE_CHECK.equals(key)) {
            owner.setRelaxBindParameterTypeChecking(value);
        } else if (QueryHints.HINT_FILTER_LISTENER.equals(key)) {
            owner.addFilterListener(Filters.hintToFilterListener(value, loader));
        } else if (QueryHints.HINT_FILTER_LISTENERS.equals(key)) {
            FilterListener[] arr = Filters.hintToFilterListeners(value, loader);
            for (FilterListener filterListener : arr) {
                owner.addFilterListener(filterListener);
            }
        } else if (QueryHints.HINT_AGGREGATE_LISTENER.equals(key)) {
            owner.addAggregateListener(Filters.hintToAggregateListener(value, loader));
        } else if (QueryHints.HINT_AGGREGATE_LISTENERS.equals(key)) {
            AggregateListener[] arr = Filters.hintToAggregateListeners(value, loader);
            for (AggregateListener aggregateListener : arr) {
                owner.addAggregateListener(aggregateListener);
            }
        } else if (QueryHints.HINT_RESULT_COUNT.equals(key)) {
            int v = (Integer) Filters.convert(value, Integer.class);
            if (v < 0) {
                throw new IllegalArgumentException(_loc.get("bad-query-hint-value", key, value).toString());
            }
            plan.setHint(key, v);
        } else if (QueryHints.HINT_INVALIDATE_PREPARED_QUERY.equals(key)) {
            plan.setHint(key, Filters.convert(value, Boolean.class));
            owner.invalidatePreparedQuery();
        } else if (QueryHints.HINT_IGNORE_PREPARED_QUERY.equals(key)) {
            plan.setHint(key, Filters.convert(value, Boolean.class));
            owner.ignorePreparedQuery();
        } else if (QueryHints.HINT_USE_LITERAL_IN_SQL.equals(key)) {
            Boolean convertedValue = (Boolean)Filters.convert(value, Boolean.class);
            plan.setHint(key, convertedValue);
        } else { // default
            plan.setHint(key, value);
        }
    }

    /**
     * Affirms if the given key starts with any of the known prefix.
     * @param key
     */
    protected boolean isKnownPrefix(String key) {
        if (key == null)
            return false;
        for (String prefix : ProductDerivations.getConfigurationPrefixes()) {
            if (key.startsWith(prefix))
                return true;
        }
        return false;
    }



//    protected boolean hasPrecedent(String key) {
//        boolean hasPrecedent = true;
//        String[] list = precedenceMap.get(key);
//        if (list != null) {
//            for (String hint : list) {
//                if (hint.equals(key))
//                    break;
//                // stop if a higher precedence hint has already defined
//                if (getHints().containsKey(hint)) {
//                    hasPrecedent = false;
//                    break;
//                }
//            }
//        }
//        return hasPrecedent;
//    }

//    private Integer toLockLevel(Object value) {
//        Object origValue = value;
//        if (value instanceof String) {
//            // to accommodate alias name input in relationship with enum values
//            //  e.g. "optimistic-force-increment" == LockModeType.OPTIMISTIC_FORCE_INCREMENT
//            String strValue = ((String) value).toUpperCase().replace('-', '_');
//            value = Enum.valueOf(LockModeType.class, strValue);
//        }
//        if (value instanceof LockModeType)
//            value = MixedLockLevelsHelper.toLockLevel((LockModeType) value);
//
//        Integer intValue = null;
//        if (value instanceof Integer)
//            intValue = (Integer) value;
//        if (intValue == null
//            || (intValue != MixedLockLevels.LOCK_NONE
//                && intValue != MixedLockLevels.LOCK_READ
//                && intValue != MixedLockLevels.LOCK_OPTIMISTIC
//                && intValue != MixedLockLevels.LOCK_WRITE
//                && intValue != MixedLockLevels.LOCK_OPTIMISTIC_FORCE_INCREMENT
//                && intValue != MixedLockLevels.LOCK_PESSIMISTIC_READ
//                && intValue != MixedLockLevels.LOCK_PESSIMISTIC_WRITE
//                && intValue != MixedLockLevels.LOCK_PESSIMISTIC_FORCE_INCREMENT)
//                )
//            throw new IllegalArgumentException(_loc.get("bad-lock-level", origValue).getMessage());
//        return intValue;
//    }

    /**
     * Gets all the supported hint keys. The set of supported hint keys is
     * statically determined by collecting hint keys from the ProductDerivations.
     */
    public Set<String> getSupportedHints() {
        return _supportedHints;
    }

    /**
     * Gets all the recorded hint keys and their values.
     */
    public Map<String, Object> getHints() {
        if (_hints == null)
            return Collections.emptyMap();
        return Collections.unmodifiableMap(_hints);
    }

}

