/*
 * 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.jackrabbit.oak.plugins.document;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.jackrabbit.oak.cache.CacheStats;
import org.apache.jackrabbit.oak.plugins.document.cache.CacheInvalidationStats;
import org.jetbrains.annotations.NotNull;

public class CountingDocumentStore implements DocumentStore {

    private DocumentStore delegate;

    //TODO: remove mec
    boolean printStacks;

    class Stats {

        private int numFindCalls;
        private int numQueryCalls;
        private int numRemoveCalls;
        private int numCreateOrUpdateCalls;

    }

    private Map<Collection, Stats> collectionStats = new HashMap<Collection, Stats>();

    public CountingDocumentStore(DocumentStore delegate) {
        this.delegate = delegate;
    }

    public void resetCounters() {
        collectionStats.clear();
    }

    public int getNumFindCalls(Collection collection) {
        return getStats(collection).numFindCalls;
    }

    public int getNumQueryCalls(Collection collection) {
        return getStats(collection).numQueryCalls;
    }

    public int getNumRemoveCalls(Collection collection) {
        return getStats(collection).numRemoveCalls;
    }

    public int getNumCreateOrUpdateCalls(Collection collection) {
        return getStats(collection).numCreateOrUpdateCalls;
    }

    private Stats getStats(Collection collection) {
        if (!collectionStats.containsKey(collection)) {
            Stats s = new Stats();
            collectionStats.put(collection, s);
            return s;
        } else {
            return collectionStats.get(collection);
        }
    }

    @Override
    public <T extends Document> T find(Collection<T> collection, String key) {
        getStats(collection).numFindCalls++;
        if (printStacks) {
            new Exception("find [" + getStats(collection).numFindCalls + "] (" + collection + ") " + key).printStackTrace();
        }
        return rewrap(collection, delegate.find(collection, key));
    }

    @Override
    public <T extends Document> T find(Collection<T> collection,
                                       String key,
                                       int maxCacheAge) {
        getStats(collection).numFindCalls++;
        if (printStacks) {
            new Exception("find [" + getStats(collection).numFindCalls + "] (" + collection + ") " + key + " [max: " + maxCacheAge + "]").printStackTrace();
        }
        return rewrap(collection, delegate.find(collection, key, maxCacheAge));
    }

    @NotNull
    @Override
    public <T extends Document> List<T> query(Collection<T> collection,
                                              String fromKey,
                                              String toKey,
                                              int limit) {
        getStats(collection).numQueryCalls++;
        if (printStacks) {
            new Exception("query1 [" + getStats(collection).numQueryCalls + "] (" + collection + ") " + fromKey + ", to " + toKey + ". limit " + limit).printStackTrace();
        }
        return rewrap(collection, delegate.query(collection, fromKey, toKey, limit));
    }

    @NotNull
    @Override
    public <T extends Document> List<T> query(Collection<T> collection,
                                              String fromKey,
                                              String toKey,
                                              String indexedProperty,
                                              long startValue,
                                              int limit) {
        getStats(collection).numQueryCalls++;
        if (printStacks) {
            new Exception("query2 [" + getStats(collection).numQueryCalls + "] (" + collection + ") " + fromKey + ", to " + toKey + ". limit " + limit).printStackTrace();
        }
        return rewrap(collection, delegate.query(collection, fromKey, toKey, indexedProperty, startValue, limit));
    }

    @Override
    public <T extends Document> void remove(Collection<T> collection,
                                            String key) {
        getStats(collection).numRemoveCalls++;
        delegate.remove(collection, key);
    }

    @Override
    public <T extends Document> void remove(Collection<T> collection,
                                            List<String> keys) {
        getStats(collection).numRemoveCalls++;
        delegate.remove(collection, keys);
    }

    @Override
    public <T extends Document> int remove(Collection<T> collection,
                                           Map<String, Long> toRemove) {
        getStats(collection).numRemoveCalls++;
        return delegate.remove(collection, toRemove);
    }

    @Override
    public <T extends Document> int remove(Collection<T> collection,
                                           String indexedProperty, long startValue, long endValue)
            throws DocumentStoreException {
        getStats(collection).numRemoveCalls++;
        return delegate.remove(collection, indexedProperty, startValue, endValue);
    }

    @Override
    public <T extends Document> boolean create(Collection<T> collection,
                                               List<UpdateOp> updateOps) {
        getStats(collection).numCreateOrUpdateCalls++;
        return delegate.create(collection, updateOps);
    }

    @Override
    public <T extends Document> T createOrUpdate(Collection<T> collection,
                                                 UpdateOp update) {
        getStats(collection).numCreateOrUpdateCalls++;
        return rewrap(collection, delegate.createOrUpdate(collection, update));
    }

    @Override
    public <T extends Document> List<T> createOrUpdate(Collection<T> collection,
                                                       List<UpdateOp> updateOps) {
        getStats(collection).numCreateOrUpdateCalls++;
        return rewrap(collection, delegate.createOrUpdate(collection, updateOps));
    }

    @Override
    public <T extends Document> T findAndUpdate(Collection<T> collection,
                                                UpdateOp update) {
        getStats(collection).numCreateOrUpdateCalls++;
        return rewrap(collection, delegate.findAndUpdate(collection, update));
    }

    @Override
    public CacheInvalidationStats invalidateCache() {
        return delegate.invalidateCache();
    }

    @Override
    public CacheInvalidationStats invalidateCache(Iterable<String> keys) {
        return delegate.invalidateCache(keys);
    }

    @Override
    public <T extends Document> void invalidateCache(Collection<T> collection,
                                                     String key) {
        delegate.invalidateCache(collection, key);
    }

    @Override
    public void dispose() {
        delegate.dispose();
    }

    @Override
    public <T extends Document> T getIfCached(Collection<T> collection,
                                              String key) {
        return rewrap(collection, delegate.getIfCached(collection, key));
    }

    @Override
    public void setReadWriteMode(String readWriteMode) {
        delegate.setReadWriteMode(readWriteMode);
    }

    @Override
    public Iterable<CacheStats> getCacheStats() {
        return delegate.getCacheStats();
    }

    @Override
    public Map<String, String> getMetadata() {
        return delegate.getMetadata();
    }

    @NotNull
    @Override
    public Map<String, String> getStats() {
        return delegate.getStats();
    }

    @Override
    public long determineServerTimeDifferenceMillis() {
        return delegate.determineServerTimeDifferenceMillis();
    }

    private <T extends Document> T rewrap(Collection<T> collection, T doc) {
        if (doc == null) {
            return null;
        }
        T d = collection.newDocument(this);
        doc.deepCopy(d);
        return d;
    }

    private <T extends Document> List<T> rewrap(Collection<T> collection,
                                                List<T> docs) {
        List<T> docList = new ArrayList<>(docs.size());
        for (T d : docs) {
            docList.add(rewrap(collection, d));
        }
        return docList;
    }
}
