/*
 * 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.SelectExecutor;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.FinderCache;
import org.apache.openjpa.kernel.FinderQuery;
import org.apache.openjpa.kernel.QueryHints;
import org.apache.openjpa.kernel.QueryStatistics;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.util.StringUtil;

/**
 * Implementation of FinderCache for JDBC.
 *
 * @author Pinaki Poddar
 *
 * @since 2.0.0
 *
 */
public class FinderCacheImpl
    implements FinderCache<ClassMapping, SelectExecutor, Result> {
    private static final String PATTERN_SEPARATOR = "\\;";
    private static final String EXLUDED_BY_USER = "Excluded by user";

    private final Map<ClassMapping, FinderQuery<ClassMapping, SelectExecutor, Result>> _delegate;
    // Key: class name Value: Reason why excluded
    private final Map<String, String> _uncachables;
    private List<String> _exclusionPatterns;
    private QueryStatistics<ClassMapping> _stats;
    private ReentrantLock _lock = new ReentrantLock();
    private boolean _enableStats = false;

    public FinderCacheImpl() {
        _delegate = new HashMap<>();
        _uncachables = new HashMap<>();
        _stats = new QueryStatistics.None<>();
    }

    /**
     * Get a map-oriented view of the cache.
     *
     * @return a map of the query string with class names as key.
     */
    @Override
    public Map<String, String> getMapView() {
        lock();
        try {
            Map<String, String> view = new TreeMap<>();
            for (ClassMapping mapping : _delegate.keySet()) {
                view.put(mapping.getDescribedType().getName(),
                    _delegate.get(mapping).getQueryString());
            }
            return view;
        } finally {
            unlock();
        }
    }

    /**
     * Gets basic statistics of execution and hit count of finder queries.
     */
    @Override
    public QueryStatistics<ClassMapping> getStatistics() {
        return _stats;
    }

    /**
     * Gets the finder query for the given mapping. The get operation can be
     * controlled by FetchConfiguration hints.
     * {@link QueryHints#HINT_IGNORE_FINDER HINT_IGNORE_FINDER} will ignore
     * any cached finder that may exist in this cache and will return null.
     * {@link QueryHints#HINT_INVALIDATE_FINDER HINT_INVALIDATE_FINDER} will
     * invalidate any cached finder that may exist in this cache and will return
     * null.
     *
     */
    @Override
    public FinderQuery<ClassMapping,SelectExecutor,Result>
        get(ClassMapping mapping, FetchConfiguration fetch) {
        if (fetch.getReadLockLevel() != 0) {
            return null;
        }

        if (!fetch.isFetchConfigurationSQLCacheAdmissible()) {
            return null;
        }

        boolean ignore = isHinted(fetch, QueryHints.HINT_IGNORE_FINDER);
        boolean invalidate = isHinted(fetch, QueryHints.HINT_INVALIDATE_FINDER);
        if (invalidate) {
            invalidate(mapping);
        }
        if (ignore) {
            return null;
        }
        FinderQuery<ClassMapping, SelectExecutor, Result> result = _delegate.get(mapping);
        _stats.recordExecution(mapping);
        return result;
    }

    /**
     * Cache a Finder Query for the given mapping and select. The put operation
     * can be controlled by FetchConfiguration hints.
     * If no entry exists for the given mapping then an attempt is made to
     * create a new FinderQuery. The attempt, however, may not be successful
     * because all Selects can not be cached.
     * @see FinderQueryImpl#newFinder(ClassMapping, Select).
     *
     * If a entry for the given mapping exists then the value of
     * {@link QueryHints#HINT_RECACHE_FINDER HINT_RECACHE_FINDER} hint
     * determines whether the existing entry is returned or a new FinderQuery
     * with the given argument overwrites the existing one.
     *
     * @param mapping the class for which the finder is to be cached
     * @param select the finder query
     * @param fetch may contain hints to control cache operation
     */
    @Override
    public FinderQuery<ClassMapping, SelectExecutor, Result> cache
       (ClassMapping mapping, SelectExecutor select, FetchConfiguration fetch) {
        lock();
        try {
            if (fetch.getReadLockLevel() != 0) {
                return null;
            }

            if (!fetch.isFetchConfigurationSQLCacheAdmissible()) {
                return null;
            }

            boolean recache = isHinted(fetch, QueryHints.HINT_RECACHE_FINDER);
            if (isExcluded(mapping)) {
                return recache ? put(mapping, select) : null;
            }
            if (_delegate.containsKey(mapping)) {
                return recache ? put(mapping, select) : _delegate.get(mapping);
            }
            return put(mapping, select);
        } finally {
            unlock();
        }
    }

    /**
     * Creates and puts a FinderQuery in the internal map indexed by the
     * given ClassMapping.
     * If a new FinderQuery can not be created for the given Select (because
     * some Select are not cached), then the mapping is marked invalid.
     *
    */
    private FinderQuery<ClassMapping, SelectExecutor, Result> put(ClassMapping mapping, SelectExecutor select) {
        FinderQuery<ClassMapping, SelectExecutor, Result> finder = FinderQueryImpl.newFinder(mapping, select);
        if (finder != null) {
            _delegate.put(mapping, finder);
        } else {
            invalidate(mapping);
        }
        return finder;
    }

    /**
     * Affirms if the given mapping is excluded from being cached.
     */
    @Override
    public boolean isExcluded(ClassMapping mapping) {
        return mapping != null && isExcluded(mapping.getDescribedType().getName());
    }

    /**
     * Searches the exclusion patterns to find out if the given string matches
     * any element.
     */
    private boolean isExcluded(String target) {
        if (_exclusionPatterns != null && _exclusionPatterns.contains(target))
            return true;
        return getMatchedExclusionPattern(target) != null;
    }

    /**
     * Adds a pattern for exclusion. Any cached finder whose class name
     * matches the given pattern will be marked invalidated as a side-effect.
     */
    @Override
    public void addExclusionPattern(String pattern) {
        lock();
        try {
            if (_exclusionPatterns == null)
                _exclusionPatterns = new ArrayList<>();
            _exclusionPatterns.add(pattern);
            Collection<ClassMapping> invalidMappings = getMatchedKeys(pattern,
                    _delegate.keySet());
            for (ClassMapping invalidMapping : invalidMappings)
                markUncachable(invalidMapping, pattern);
        } finally {
            unlock();
        }
    }
    /**
     * Removes a pattern for exclusion. Any query identifier marked as not
     * cachable due to the given pattern will now be removed from the list of
     * uncachables as a side-effect.
     */
    @Override
    public void removeExclusionPattern(String pattern) {
        lock();
        try {
            if (_exclusionPatterns == null)
                return;
            _exclusionPatterns.remove(pattern);
            Collection<String> reborns = getMatchedKeys(pattern,
                _uncachables.keySet());
            for (String rebornKey : reborns)
                _uncachables.remove(rebornKey);
        } finally {
            unlock();
        }
    }

    /**
     * Gets the pattern that matches the given identifier.
     */
    private String getMatchedExclusionPattern(String id) {
        if (_exclusionPatterns == null || _exclusionPatterns.isEmpty())
            return null;
        for (String pattern : _exclusionPatterns)
            if (matches(pattern, id))
                return pattern;
        return null;
    }

    /**
     * Gets the elements of the given set that match the given pattern.
     */
    private Collection<ClassMapping> getMatchedKeys(String pattern, Set<ClassMapping> set) {
        List<ClassMapping> result = new ArrayList<>();
        for (ClassMapping entry : set) {
            if (matches(pattern, entry)) {
                result.add(entry);
            }
        }
        return result;
    }

    /**
     * Gets the elements of the given list which match the given pattern.
     */
    private Collection<String> getMatchedKeys(String pattern, Collection<String> coll) {
        List<String> result = new ArrayList<>();
        for (String key : coll) {
            if (matches(pattern, key)) {
                result.add(key);
            }
        }
        return result;
    }

    boolean matches(String pattern, ClassMapping mapping) {
        return matches(pattern, mapping.getDescribedType().getName());
    }

    boolean matches(String pattern, String target) {
        return target != null && (target.equals(pattern)
          || target.matches(pattern));
    }

    @Override
    public boolean invalidate(ClassMapping mapping) {
        lock();
        try {
            return _delegate.remove(mapping) != null;
        } finally {
            unlock();
        }
    }

    @Override
    public FinderQuery<ClassMapping, SelectExecutor, Result> markUncachable(ClassMapping mapping) {
        return markUncachable(mapping.getDescribedType().getName());
    }

    public FinderQuery<ClassMapping, SelectExecutor, Result> markUncachable(String id) {
        return markUncachable(id, EXLUDED_BY_USER);
    }

    private FinderQuery<ClassMapping, SelectExecutor, Result> markUncachable(String cls, String reason) {
        lock();
        try {
            boolean excludedByUser = _uncachables.get(cls) == EXLUDED_BY_USER;
            if (!excludedByUser)
                _uncachables.put(cls, reason);
            return _delegate.remove(searchMappingByName(cls));
        } finally {
            unlock();
        }
    }

    private FinderQuery<ClassMapping, SelectExecutor, Result> markUncachable(ClassMapping mapping, String reason) {
        lock();
        try {
            String cls = mapping.getDescribedType().getName();
            boolean excludedByUser = _uncachables.get(cls) == EXLUDED_BY_USER;
            if (!excludedByUser)
                _uncachables.put(cls, reason);
            return _delegate.remove(mapping);
        } finally {
            unlock();
        }
    }

    ClassMapping searchMappingByName(String cls) {
        for (ClassMapping mapping : _delegate.keySet())
            if (matches(cls, mapping))
                return mapping;
        return null;
    }


    public void setExcludes(String excludes) {
        lock();
        try {
            if (StringUtil.isEmpty(excludes))
                return;
            if (_exclusionPatterns == null)
                _exclusionPatterns = new ArrayList<>();
            String[] patterns = excludes.split(PATTERN_SEPARATOR);
            for (String pattern : patterns)
                addExclusionPattern(pattern);
        } finally {
            unlock();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<String> getExcludes() {
        return (List<String>)_exclusionPatterns == null
            ? Collections.EMPTY_LIST
            : Collections.unmodifiableList(_exclusionPatterns);
    }

    boolean isHinted(FetchConfiguration fetch, String hint) {
        if (fetch == null)
            return false;
        Object result = fetch.getHint(hint);
        return result != null && "true".equalsIgnoreCase(result.toString());
    }

    void lock() {
        if (_lock != null)
            _lock.lock();
    }

    void unlock() {
        if (_lock != null && _lock.isLocked())
            _lock.unlock();
    }

    public void setEnableStats(boolean b) {
        _enableStats = b;
        if (_enableStats) {
            _stats = new QueryStatistics.Default<>();
        }
    }

    public boolean getEnableStats() {
        return _enableStats;
    }
    // ----------------------------------------------------
    //  Configuration contract
    // ----------------------------------------------------
    @Override
    public void startConfiguration() {
    }

    @Override
    public void setConfiguration(Configuration conf) {
    }

    @Override
    public void endConfiguration() {
    }
}
