/*
 * 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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.LockModeType;
import javax.persistence.PessimisticLockScope;

import org.apache.openjpa.kernel.DataCacheRetrieveMode;
import org.apache.openjpa.kernel.DataCacheStoreMode;
import org.apache.openjpa.kernel.DelegatingFetchConfiguration;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.QueryFlushModes;

/**
 * Implements FetchPlan via delegation to FetchConfiguration.
 *
 * @author Abe White
 * @author Pinaki Poddar
 * @since 0.4.1
 */
public class FetchPlanImpl
	implements FetchPlan {

    private final DelegatingFetchConfiguration _fetch;

    /**
     * Structure holds ranking of equivalent hint keys. Each entry value is a list of other keys that are higher rank
     * than the entry key.
     */
    protected static Map<String, List<String>> _precedence = new HashMap<>();

    /**
     * Structure holds one or more converters for a user-specified hint value.
     */
    protected static Map<String,HintValueConverter[]> _hints = new HashMap<>();

    /**
     * Statically registers supported hint keys with their ranking and converters.
     */
    static {
        registerHint(new String[]{"openjpa.FetchPlan.ExtendedPathLookup"},
                new HintValueConverter.StringToBoolean());
        registerHint(new String[]{"openjpa.FetchBatchSize", "openjpa.FetchPlan.FetchBatchSize"},
                new HintValueConverter.StringToInteger());
        registerHint(new String[]{"openjpa.MaxFetchDepth", "openjpa.FetchPlan.MaxFetchDepth"},
                new HintValueConverter.StringToInteger());
        registerHint(new String[]{"openjpa.LockTimeout", "openjpa.FetchPlan.LockTimeout",
                "javax.persistence.lock.timeout"}, new HintValueConverter.StringToInteger());
        registerHint(new String[]{"openjpa.QueryTimeout", "openjpa.FetchPlan.QueryTimeout",
                "javax.persistence.query.timeout"}, new HintValueConverter.StringToInteger());
        registerHint(new String[]{"openjpa.FlushBeforeQueries", "openjpa.FetchPlan.FlushBeforeQueries"},
                new HintValueConverter.StringToInteger(
                   new String[] {"0", "1", "2"},
                   new int[]{QueryFlushModes.FLUSH_TRUE, QueryFlushModes.FLUSH_FALSE,
                           QueryFlushModes.FLUSH_WITH_CONNECTION}));
        registerHint(new String[]{"openjpa.ReadLockMode", "openjpa.FetchPlan.ReadLockMode"},
                new MixedLockLevelsHelper());
        registerHint(new String[]{"openjpa.ReadLockLevel", "openjpa.FetchPlan.ReadLockLevel"},
                new MixedLockLevelsHelper());
        registerHint(new String[]{"openjpa.WriteLockMode", "openjpa.FetchPlan.WriteLockMode"},
                new MixedLockLevelsHelper());
        registerHint(new String[]{"openjpa.WriteLockLevel", "openjpa.FetchPlan.WriteLockLevel"},
                new MixedLockLevelsHelper());
    }

    /**
     * Registers a hint key with its value converters.
     *
     * @param keys a set of keys in increasing order of ranking. Can not be null or empty.
     *
     * @param converters array of converters that are attempts in order to convert a user-specified hint value
     * to a value that is consumable by the kernel.
     */
    protected static void registerHint(String[] keys, HintValueConverter... converters) {
        for (String key : keys) {
            _hints.put(key, converters);
        }
        if (keys.length > 1) {
            for (int i = 0; i < keys.length-1; i++) {
                List<String> list = new ArrayList<>(keys.length-i-1);
                for (int j = i+1; j < keys.length; j++) {
                    list.add(keys[j]);
                }
                _precedence.put(keys[i], list);
            }
        }
    }

    /**
     * Constructor; supply delegate.
     */
    public FetchPlanImpl(FetchConfiguration fetch) {
        _fetch = newDelegatingFetchConfiguration(fetch);
    }

    /**
     * Create a new exception-translating delegating fetch configuration.
     */
    protected DelegatingFetchConfiguration newDelegatingFetchConfiguration(FetchConfiguration fetch) {
        return new DelegatingFetchConfiguration(fetch, PersistenceExceptions.TRANSLATOR);
    }

    /**
     * Delegate.
     */
    @Override
    public FetchConfiguration getDelegate() {
        return _fetch.getDelegate();
    }

    @Override
    public int getMaxFetchDepth() {
        return _fetch.getMaxFetchDepth();
    }

    @Override
    public FetchPlan setMaxFetchDepth(int depth) {
        _fetch.setMaxFetchDepth(depth);
        return this;
    }

    @Override
    public int getFetchBatchSize() {
        return _fetch.getFetchBatchSize();
    }

    @Override
    public FetchPlan setFetchBatchSize(int fetchBatchSize) {
        _fetch.setFetchBatchSize(fetchBatchSize);
        return this;
    }

    @Override
    public boolean getQueryResultCacheEnabled() {
        return _fetch.getQueryCacheEnabled();
    }

    @Override
    public FetchPlan setQueryResultCacheEnabled(boolean cache) {
        _fetch.setQueryCacheEnabled(cache);
        return this;
    }

    @Override
    public boolean getQueryResultCache() {
        return getQueryResultCacheEnabled();
    }

    @Override
    public FetchPlan setQueryResultCache(boolean cache) {
        return setQueryResultCacheEnabled(cache);
    }

    @Override
    public Collection<String> getFetchGroups() {
        return _fetch.getFetchGroups();
    }

    @Override
    public FetchPlan addFetchGroup(String group) {
        _fetch.addFetchGroup(group);
        return this;
    }

    @Override
    public FetchPlan addFetchGroups(String... groups) {
        return addFetchGroups(Arrays.asList(groups));
    }

    @Override
    public FetchPlan addFetchGroups(Collection groups) {
        _fetch.addFetchGroups(groups);
        return this;
    }

    @Override
    public FetchPlan removeFetchGroup(String group) {
        _fetch.removeFetchGroup(group);
        return this;
    }

    @Override
    public FetchPlan removeFetchGroups(String... groups) {
        return removeFetchGroups(Arrays.asList(groups));
    }

    @Override
    public FetchPlan removeFetchGroups(Collection groups) {
        _fetch.removeFetchGroups(groups);
        return this;
    }

    @Override
    public FetchPlan clearFetchGroups() {
        _fetch.clearFetchGroups();
        return this;
    }

    @Override
    public FetchPlan resetFetchGroups() {
        _fetch.resetFetchGroups();
        return this;
    }

    @Override
    public Collection<String> getFields() {
        return (Collection<String>) _fetch.getFields();
    }

    @Override
    public boolean hasField(String field) {
        return _fetch.hasField(field);
    }

    @Override
    public boolean hasField(Class cls, String field) {
        return hasField(toFieldName(cls, field));
    }

    @Override
    public FetchPlan addField(String field) {
        _fetch.addField(field);
        return this;
    }

    @Override
    public FetchPlan addField(Class cls, String field) {
        return addField(toFieldName(cls, field));
    }

    @Override
    public FetchPlan addFields(String... fields) {
        return addFields(Arrays.asList(fields));
    }

    @Override
    public FetchPlan addFields(Class cls, String... fields) {
        return addFields(cls, Arrays.asList(fields));
    }

    @Override
    public FetchPlan addFields(Collection fields) {
        _fetch.addFields(fields);
        return this;
    }

    @Override
    public FetchPlan addFields(Class cls, Collection fields) {
        return addFields(toFieldNames(cls, fields));
    }

    @Override
    public FetchPlan removeField(String field) {
        _fetch.removeField(field);
        return this;
    }

    @Override
    public FetchPlan removeField(Class cls, String field) {
        return removeField(toFieldName(cls, field));
    }

    @Override
    public FetchPlan removeFields(String... fields) {
        return removeFields(Arrays.asList(fields));
    }

    @Override
    public FetchPlan removeFields(Class cls, String... fields) {
        return removeFields(cls, Arrays.asList(fields));
    }

    @Override
    public FetchPlan removeFields(Collection fields) {
        _fetch.removeFields(fields);
        return this;
    }

    @Override
    public FetchPlan removeFields(Class cls, Collection fields) {
        return removeFields(toFieldNames(cls, fields));
    }

    @Override
    public FetchPlan clearFields() {
        _fetch.clearFields();
        return this;
    }

    private static String toFieldName(Class cls, String field) {
        return cls.getName() + "." + field;
    }

    private static Collection toFieldNames(Class cls, Collection fields) {
        if (fields.isEmpty())
            return fields;
        Collection names = new ArrayList(fields);
        for (Object field : fields) {
            names.add(toFieldName(cls, (String) field));
        }
        return names;
    }

    @Override
    public int getLockTimeout() {
        return _fetch.getLockTimeout();
    }

    @Override
    public FetchPlan setLockTimeout(int timeout) {
        _fetch.setLockTimeout(timeout);
        return this;
    }

    @Override
    public PessimisticLockScope getLockScope() {
        return LockScopesHelper.fromLockScope(_fetch.getLockScope());
    }

    @Override
    public FetchPlan setLockScope(PessimisticLockScope scope) {
        _fetch.setLockScope(LockScopesHelper.toLockScope(scope));
        return this;
    }

    @Override
    public int getQueryTimeout() {
        return _fetch.getQueryTimeout();
    }

    @Override
    public FetchPlan setQueryTimeout(int timeout) {
        _fetch.setQueryTimeout(timeout);
        return this;
    }

    @Override
    public LockModeType getReadLockMode() {
        return MixedLockLevelsHelper.fromLockLevel(_fetch.getReadLockLevel());
    }

    @Override
    public FetchPlan setReadLockMode(LockModeType mode) {
        _fetch.setReadLockLevel(MixedLockLevelsHelper.toLockLevel(mode));
        return this;
    }

    @Override
    public LockModeType getWriteLockMode() {
        return MixedLockLevelsHelper.fromLockLevel(_fetch.getWriteLockLevel());
    }

    @Override
    public FetchPlan setWriteLockMode(LockModeType mode) {
        _fetch.setWriteLockLevel(MixedLockLevelsHelper.toLockLevel(mode));
        return this;
    }

    @Override
    public boolean getExtendedPathLookup() {
        return _fetch.getExtendedPathLookup();
    }

    @Override
    public FetchPlan setExtendedPathLookup(boolean flag) {
        _fetch.setExtendedPathLookup(flag);
        return this;
    }

    @Override
    public Object getHint(String key) {
        return _fetch.getHint(key);
    }

    /**
     * Sets the hint after converting the value appropriately.
     * If a higher ranking equivalent hint is already set, then bypasses this hint.
     */
    @Override
    public void setHint(String key, Object value) {
        if (!isRecognizedHint(key))
            return;
        if (_precedence.containsKey(key)) {
            List<String> higherKeys = _precedence.get(key);
            for (String higherKey : higherKeys) {
                if (_fetch.isHintSet(higherKey))
                    return;
            }
        }
        Object newValue = convertHintValue(key, value);
        _fetch.setHint(key, newValue, value);
    }

    public void setHints(Map<String, Object> hints) {
        if (hints == null || hints.isEmpty()) {
            return;
        }
        for (Map.Entry<String,Object> hint : hints.entrySet()) {
            setHint(hint.getKey(), hint.getValue());
        }
    }

    @Override
    public Map<String, Object> getHints() {
        return _fetch.getHints();
    }

    @Override
    public int hashCode() {
        return ((_fetch == null) ? 0  : _fetch.hashCode());
    }

    @Override
    public boolean equals(Object other) {
        if (other == this)
            return true;
        if ((other == null) || (other.getClass() != this.getClass()))
            return false;
        if (_fetch == null)
        	return false;

        return _fetch.equals(((FetchPlanImpl) other)._fetch);
    }

    @Override
    public DataCacheRetrieveMode getCacheRetrieveMode() {
        return _fetch.getCacheRetrieveMode();
    }

    @Override
    public DataCacheStoreMode getCacheStoreMode() {
        return _fetch.getCacheStoreMode();
    }

    @Override
    public FetchPlan setCacheStoreMode(DataCacheStoreMode mode) {
        _fetch.setCacheStoreMode(mode);
        return this;
    }

    @Override
    public FetchPlan setCacheRetrieveMode(DataCacheRetrieveMode mode) {
        _fetch.setCacheRetrieveMode(mode);
        return this;
    }

    Object convertHintValue(String key, Object value) {
        if (value == null)
            return null;
        HintValueConverter[] converters = _hints.get(key);
        if (converters == null)
            return value;
        for (HintValueConverter converter : converters) {
            if (converter.canConvert(value.getClass())) {
                return converter.convert(value);
            }
        }
        return value;
    }

    boolean isRecognizedHint(String key) {
        if (key == null)
            return false;
        if (_hints.containsKey(key))
            return true;
        return key.startsWith("openjpa.");
    }

    boolean intersects(Collection<String> keys, Collection<String> b) {
        if (keys == null || keys.isEmpty() || b == null || b.isEmpty())
            return false;
        for (String key : keys) {
            if (b.contains(key))
                return true;
        }
        return false;
    }
}
