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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.event.RemoteCommitEvent;
import org.apache.openjpa.event.RemoteCommitListener;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.lib.util.concurrent.AbstractConcurrentEventManager;
import org.apache.openjpa.util.GeneralException;


/**
 * Abstract {@link DataCache} implementation that provides various
 * statistics, logging, and timeout functionality common across cache
 * implementations.
 *
 * @author Patrick Linskey
 * @author Abe White
 */
public abstract class AbstractDataCache extends AbstractConcurrentEventManager
    implements DataCache, Configurable {

    private static final long serialVersionUID = 1L;

    protected CacheStatisticsSPI _stats = new CacheStatisticsImpl();

    private static final BitSet EMPTY_BITSET = new BitSet(0);

    private static final Localizer s_loc = Localizer.forPackage(AbstractDataCache.class);


    /**
     * The configuration set by the system.
     */
    protected OpenJPAConfiguration conf;

    /**
     * The log to use.
     */
    protected Log log;

    private String _name = null;
    private boolean _closed = false;
    private String _schedule = null;
    protected Set<String> _includedTypes = new HashSet<>();
    protected Set<String> _excludedTypes = new HashSet<>();
    protected boolean _evictOnBulkUpdate = true;

    @Override
    public String getName() {
        return _name;
    }

    @Override
    public void setName(String name) {
        _name = name;
    }
    public void setEnableStatistics(boolean enable){
        if(enable){
            _stats.enable();
        }
    }
    public void getEnableStatistics(){
        _stats.isEnabled();
    }

    public String getEvictionSchedule() {
        return _schedule;
    }

    public void setEvictionSchedule(String s) {
        _schedule = s;
    }

    @Override
    public void initialize(DataCacheManager manager) {
        if (_schedule != null && !"".equals(_schedule)) {
            ClearableScheduler scheduler = manager.getClearableScheduler();
            if (scheduler != null)
                scheduler.scheduleEviction(this, _schedule);
        }
        // Cast here rather than add to the interface because this is a hack to support an older way of configuring
        if(manager instanceof DataCacheManagerImpl){
            List<String> invalidConfigured = new ArrayList<>();
            // assert that things are configured properly
            if(_includedTypes!=null){
                for(String s : _includedTypes){
                    if(_excludedTypes.contains(s)){
                        invalidConfigured.add(s);
                    }
                }
                if (invalidConfigured.size() > 0) {
                    throw new GeneralException(s_loc.get("invalid-types-excluded-types", invalidConfigured.toString()));
                }
            }
            ((DataCacheManagerImpl)manager).setTypes(_includedTypes, _excludedTypes);
        }
    }

    @Override
    public void commit(Collection<DataCachePCData> additions, Collection<DataCachePCData> newUpdates,
            Collection<DataCachePCData> existingUpdates, Collection<Object> deletes) {
        // remove all objects in deletes list
        removeAllInternal(deletes);

        // next, add all the new additions
        putAllInternal(additions);
        putAllInternal(newUpdates);

        // possibly add the existing updates, depending on the
        // semantics of the cache, as dictated by recacheUpdates()
        if (recacheUpdates())
            putAllInternal(existingUpdates);

        if (log.isTraceEnabled()) {
            Collection<Object> addIds = new ArrayList<>(additions.size());
            Collection<Object> upIds = new ArrayList<>(newUpdates.size());
            Collection<Object> exIds = new ArrayList<>(existingUpdates.size());

            for (DataCachePCData addition : additions)
                addIds.add(addition.getId());
            for (DataCachePCData newUpdate : newUpdates)
                upIds.add(newUpdate.getId());
            for (DataCachePCData existingUpdate : existingUpdates)
                exIds.add(existingUpdate.getId());

            log.trace(s_loc.get("cache-commit", new Object[]{ addIds, upIds, exIds, deletes }));
        }
    }

    @Override
    public boolean contains(Object key) {
        DataCachePCData o = getInternal(key);
        if (o != null && o.isTimedOut()) {
            o = null;
            removeInternal(key);
            if (log.isTraceEnabled())
                log.trace(s_loc.get("cache-timeout", key));
        }
        return o != null;
    }

    @Override
    public BitSet containsAll(Collection<Object> keys) {
        if (keys.isEmpty())
            return EMPTY_BITSET;

        BitSet set = new BitSet(keys.size());
        int i = 0;
        for (Iterator<Object> iter = keys.iterator(); iter.hasNext(); i++)
            if (contains(iter.next()))
                set.set(i);
        return set;
    }

    @Override
    public DataCachePCData get(Object key) {
        DataCachePCData o = getInternal(key);
        if (o != null && o.isTimedOut()) {
            o = null;
            removeInternal(key);
            if (log.isTraceEnabled())
                log.trace(s_loc.get("cache-timeout", key));
        }
        if (log.isTraceEnabled()) {
            if (o == null)
                log.trace(s_loc.get("cache-miss", key));
            else
                log.trace(s_loc.get("cache-hit", key));
        }

        return o;
    }


    /**
     * Returns the objects for the given key List.
     */
    @Override
    public Map<Object,DataCachePCData> getAll(List<Object> keys) {
        Map<Object,DataCachePCData> resultMap = new HashMap<>(keys.size());
        for (Object key : keys)
            resultMap.put(key, get(key));
        return resultMap;
    }

    @Override
    public DataCachePCData put(DataCachePCData data) {
        DataCachePCData o = putInternal(data.getId(), data);
        if (log.isTraceEnabled())
            log.trace(s_loc.get("cache-put", data.getId()));
        return (o == null || o.isTimedOut()) ? null : o;
    }

    @Override
    public void update(DataCachePCData data) {
        if (recacheUpdates()) {
            putInternal(data.getId(), data);
        }
    }

    @Override
    public DataCachePCData remove(Object key) {
        DataCachePCData o = removeInternal(key);
        if (o != null && o.isTimedOut())
            o = null;
        if (log.isTraceEnabled()) {
            if (o == null)
                log.trace(s_loc.get("cache-remove-miss", key));
            else
                log.trace(s_loc.get("cache-remove-hit", key));
        }
        return o;
    }

    @Override
    public BitSet removeAll(Collection<Object> keys) {
        if (keys.isEmpty())
            return EMPTY_BITSET;

        BitSet set = new BitSet(keys.size());
        int i = 0;
        for (Iterator<Object> iter = keys.iterator(); iter.hasNext(); i++)
            if (remove(iter.next()) != null)
                set.set(i);
        return set;
    }

    /**
     * Remove the objects of the given class from the cache.
     */
    @Override
    public void removeAll(Class<?> cls, boolean subClasses) {
        removeAllInternal(cls, subClasses);
    }

    @Override
    public boolean pin(Object key) {
        boolean bool = pinInternal(key);
        if (log.isTraceEnabled()) {
            if (bool)
                log.trace(s_loc.get("cache-pin-hit", key));
            else
                log.trace(s_loc.get("cache-pin-miss", key));
        }
        return bool;
    }

    @Override
    public BitSet pinAll(Collection<Object> keys) {
        if (keys.isEmpty())
            return EMPTY_BITSET;

        BitSet set = new BitSet(keys.size());
        int i = 0;
        for (Iterator<Object> iter = keys.iterator(); iter.hasNext(); i++)
            if (pin(iter.next()))
                set.set(i);
        return set;
    }

    @Override
    public void pinAll(Class<?> cls, boolean subs) {
        if (log.isWarnEnabled())
            log.warn(s_loc.get("cache-class-pin", getName()));
    }

    @Override
    public boolean unpin(Object key) {
        boolean bool = unpinInternal(key);
        if (log.isTraceEnabled()) {
            if (bool)
                log.trace(s_loc.get("cache-unpin-hit", key));
            else
                log.trace(s_loc.get("cache-unpin-miss", key));
        }
        return bool;
    }

    @Override
    public BitSet unpinAll(Collection<Object> keys) {
        if (keys.isEmpty())
            return EMPTY_BITSET;

        BitSet set = new BitSet(keys.size());
        int i = 0;
        for (Iterator<Object> iter = keys.iterator(); iter.hasNext(); i++)
            if (unpin(iter.next()))
                set.set(i);
        return set;
    }

    @Override
    public void unpinAll(Class<?> cls, boolean subs) {
        if (log.isWarnEnabled())
            log.warn(s_loc.get("cache-class-unpin", getName()));
    }

    @Override
    public void clear() {
        clearInternal();
        if (log.isTraceEnabled())
            log.trace(s_loc.get("cache-clear", getName()));
    }

    @Override
    public void close() {
        close(true);
    }

    protected void close(boolean clear) {
        if (!_closed) {
            if (clear)
                clearInternal();
            _closed = true;
        }
    }

    public boolean isClosed() {
        return _closed;
    }

    @Override
    public void addExpirationListener(ExpirationListener listen) {
        addListener(listen);
    }

    @Override
    public boolean removeExpirationListener(ExpirationListener listen) {
        return removeListener(listen);
    }

    @Override
    public String toString() {
        return "[" + super.toString() + ":" + _name + "]";
    }

    /**
     * This method is part of the {@link RemoteCommitListener} interface. If
     * your cache subclass relies on OpenJPA for clustering support, make it
     * implement <code>RemoteCommitListener</code>. This method will take
     * care of invalidating entries from remote commits.
     */
    public void afterCommit(RemoteCommitEvent event) {
        if (_closed)
            return;

        if (event.getPayloadType() == RemoteCommitEvent.PAYLOAD_EXTENTS) {
            removeAllTypeNamesInternal(event.getUpdatedTypeNames());
            removeAllTypeNamesInternal(event.getDeletedTypeNames());
        } else {
            // drop all the committed OIDs, excepting brand
            // new OIDs. brand new OIDs either won't be in
            // the cache, or if they are, will be more up to date
            removeAllInternal(event.getUpdatedObjectIds());
            removeAllInternal(event.getDeletedObjectIds());
        }
    }

    /**
     * Invoke when a key is removed from this cache. Propagates the
     * expiration event on to all expiration listeners registered
     * with this class.
     */
    protected void keyRemoved(Object key, boolean expired) {
        // Notify any expiration listeners of the expiration.
        if (hasListeners())
            fireEvent(new ExpirationEvent(this, key, expired));

        if (expired && log.isTraceEnabled())
            log.trace(s_loc.get("cache-expired", key));
    }

    /**
     * Return <code>true</code> if updates to data already in the
     * cache (either in {@link #commit} or the {@link #update})
     * should be put back into the cache. Returns false by default.
     */
    protected boolean recacheUpdates() {
        return false;
    }

    /**
     * Return the object for the given oid.
     */
    protected abstract DataCachePCData getInternal(Object oid);

    /**
     * Add the given object to the cache, returning the old object under the
     * given oid.
     */
    protected abstract DataCachePCData putInternal(Object oid,
            DataCachePCData pc);

    /**
     * Add all of the given objects to the cache.
     */
    protected void putAllInternal(Collection<DataCachePCData> pcs) {
        for (DataCachePCData pc : pcs) {
            putInternal(pc.getId(), pc);
        }
    }

    /**
     * Remove the object under the given oid from the cache.
     */
    protected abstract DataCachePCData removeInternal(Object oid);

    /**
     * Evict objects in cache by class.
     */
    protected abstract void removeAllInternal(Class<?> cls, boolean subclasses);

    /**
     * Remove all objects under the given oids from the cache.
     */
    protected void removeAllInternal(Collection<Object> oids) {
        for (Object oid : oids)
            removeInternal(oid);
    }

    /**
     * Remove all objects of the given class names from the cache.
     */
    protected void removeAllTypeNamesInternal(Collection<String> classNames) {
        Collection<Class<?>> classes = Caches.addTypesByName(conf, classNames, null);
        if (classes == null)
            return;

        for (Class<?> cls : classes) {
            if (log.isTraceEnabled())
                log.trace(s_loc.get("cache-removeclass", cls.getName()));
            removeAllInternal(cls, false);
        }
    }

    /**
     * Clear the cache.
     */
    protected abstract void clearInternal();

    /**
     * Pin an object to the cache.
     */
    protected abstract boolean pinInternal(Object oid);

    /**
     * Unpin an object from the cache.
     */
    protected abstract boolean unpinInternal(Object oid);

    
    @Override
    public DataCache getPartition(String name, boolean create) {
        if (Objects.equals(_name, name))
            return this;
        return null;
    }

    
    @Override
    public Set<String> getPartitionNames() {
        return Collections.emptySet();
    }

    @Override
    public boolean isPartitioned() {
        return false;
    }

     @Override
    public CacheStatistics getStatistics() {
    	return _stats;
    }

    // ---------- Configurable implementation ----------

    @Override
    public void setConfiguration(Configuration conf) {
        this.conf = (OpenJPAConfiguration) conf;
        this.log = conf.getLog(OpenJPAConfiguration.LOG_DATACACHE);
    }

    @Override
    public void startConfiguration() {
    }

    @Override
    public void endConfiguration() {
        if (_name == null)
            setName(NAME_DEFAULT);
    }

    // ---------- AbstractEventManager implementation ----------

    @Override
    protected void fireEvent(Object event, Object listener) {
        ExpirationListener listen = (ExpirationListener) listener;
        ExpirationEvent ev = (ExpirationEvent) event;
        try {
            listen.onExpire(ev);
        } catch (Exception e) {
            if (log.isWarnEnabled())
                log.warn(s_loc.get("exp-listener-ex"), e);
		}
	}

    public Set<String> getTypes() {
        return _includedTypes;
    }

    public Set<String> getExcludedTypes() {
        return _excludedTypes;
    }

    public void setTypes(Set<String> types) {
        _includedTypes = types;
        if (log.isWarnEnabled())
            log.warn(s_loc.get("recommend_jpa2_caching", new Object[]{"Types", DataCacheMode.ENABLE_SELECTIVE.toString()}));
    }

    public void setTypes(String types) {
        _includedTypes =
            StringUtil.isEmpty(types) ? null : new HashSet<>(Arrays.asList(StringUtil.split(types, ";", 0)));
        if (log.isWarnEnabled())
            log.warn(s_loc.get("recommend_jpa2_caching", new Object[]{"Types", DataCacheMode.ENABLE_SELECTIVE.toString()}));
    }

    public void setExcludedTypes(Set<String> types) {
        _excludedTypes = types;
        if (log.isWarnEnabled())
            log.warn(s_loc.get("recommend_jpa2_caching", new Object[]{"ExcludeTypes", DataCacheMode.DISABLE_SELECTIVE.toString()}));
    }

    public void setExcludedTypes(String types) {
        _excludedTypes =
            StringUtil.isEmpty(types) ? null : new HashSet<>(Arrays.asList(StringUtil.split(types, ";", 0)));
        if (log.isWarnEnabled())
            log.warn(s_loc.get("recommend_jpa2_caching", new Object[]{"ExcludeTypes", DataCacheMode.DISABLE_SELECTIVE.toString()}));
    }

    public DataCache selectCache(OpenJPAStateManager sm) {
        return this;
    }

    @Override
    public boolean getEvictOnBulkUpdate(){
        return _evictOnBulkUpdate;
    }

    public void setEvictOnBulkUpdate(boolean b){
        _evictOnBulkUpdate = b;
    }
}
