/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr.index.local;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
import javax.jcr.query.qom.Constraint;
import org.mapdb.BTreeKeySerializer;
import org.mapdb.DB;
import org.modeshape.jcr.index.local.IndexValues.Converter;
import org.modeshape.jcr.spi.index.IndexConstraints;

/**
 * An index for enumerated values. This index only supports string-based values, since all enumerated values are discrete.
 *
 * @author Randall Hauch (rhauch@redhat.com)
 */
final class LocalEnumeratedIndex extends LocalIndex<String> {

    static LocalEnumeratedIndex create( String name,
                                        String workspaceName,
                                        DB db,
                                        Converter<String> converter,
                                        BTreeKeySerializer<String> valueSerializer,
                                        Set<String> enumeratedValues ) {
        return new LocalEnumeratedIndex(name, workspaceName, db, converter, valueSerializer, enumeratedValues);
    }

    static LocalEnumeratedIndex create( String name,
                                        String workspaceName,
                                        DB db,
                                        Converter<String> converter,
                                        BTreeKeySerializer<String> valueSerializer ) {
        return new LocalEnumeratedIndex(name, workspaceName, db, converter, valueSerializer, null);
    }

    protected final ConcurrentNavigableMap<String, Set<String>> nodeKeySetsByValue;
    private final Converter<String> converter;
    private final Set<String> possibleValues;
    private final boolean isNew;
    private final AtomicLong totalCount;

    LocalEnumeratedIndex( String name,
                          String workspaceName,
                          DB db,
                          Converter<String> converter,
                          BTreeKeySerializer<String> valueSerializer,
                          Set<String> possibleValues ) {
        super(name, workspaceName, db);

        this.totalCount = new AtomicLong(0);
        this.converter = converter;
        this.possibleValues = possibleValues != null ? new HashSet<String>(possibleValues) : new HashSet<String>();
        this.nodeKeySetsByValue = new ConcurrentSkipListMap<>();
        // Read all of the existing collections ...
        boolean foundContent = false;
        for (String collectionName : db.getAll().keySet()) {
            String prefix = this.name + "/enumerated/";
            if (collectionName.startsWith(prefix)) {
                foundContent = true;
                if (collectionName.length() > prefix.length()) {
                    String valueString = collectionName.substring(prefix.length());
                    Set<String> keysForValue = createOrGetKeySet(valueString);
                    nodeKeySetsByValue.put(valueString, keysForValue);
                    totalCount.addAndGet(keysForValue.size());
                }
            }
        }
        // Add any that were not found in the DB ...
        for (String possibleValue : this.possibleValues) {
            if (!nodeKeySetsByValue.containsKey(possibleValue)) {
                Set<String> keysForValue = createOrGetKeySet(possibleValue);
                nodeKeySetsByValue.put(possibleValue, keysForValue);
                totalCount.addAndGet(keysForValue.size());
            }
        }
        this.isNew = !foundContent;
    }

    private Set<String> createOrGetKeySet( String value ) {
        String collectionName = collectionName(value);
        if (logger.isDebugEnabled()) {
            if (db.exists(collectionName)) {
                logger.debug("Reopening enum storage '{0}' for '{1}' index in workspace '{2}'", collectionName, name,
                             workspace);
            } else {
                logger.debug("Creating enum storage '{0}' for '{1}' index in workspace '{2}'", collectionName, name, workspace);
            }
        }
        // Try to create the set ...
        Set<String> keySet = db.createHashSet(collectionName).counterEnable().makeOrGet(); // make sure this is ATOMIC !   
        Set<String> previous = nodeKeySetsByValue.putIfAbsent(value, keySet);
        if (previous != null) keySet = previous;
        return keySet;
    }

    private String collectionName( String value ) {
        return name + "/enumerated/" + value;
    }

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

    public String getWorkspaceName() {
        return workspace;
    }
    
    @Override
    public boolean requiresReindexing() {
        return isNew;
    }

    protected final Converter<String> converter() {
        return converter;
    }

    @Override
    public long estimateTotalCount() {
        return totalCount.get();
    }

    @Override
    public Results filter(IndexConstraints filter, long cardinalityEstimate) {
        // Find all sets that match the name pattern ...
        return Operations.createEnumeratedFilter(nodeKeySetsByValue, converter, filter.getConstraints(), filter.getVariables())
                         .getResults();
    }

    @Override
    public long estimateCardinality( List<Constraint> andedConstraints,
                                     Map<String, Object> variables ) {
        return Operations.createEnumeratedFilter(nodeKeySetsByValue, converter, andedConstraints, variables)
                         .estimateCount();
    }

    @Override
    public void add( String nodeKey,
                     String propertyName, 
                     String value ) {
        // Find the set ...
        Set<String> keySet = nodeKeySetsByValue.computeIfAbsent(value, this::createOrGetKeySet);
        if (keySet.add(nodeKey)) {
            totalCount.incrementAndGet();
        }
    }

    @Override
    public void remove( String nodeKey ) {
        for (Set<String> nodeKeySet : nodeKeySetsByValue.values()) {
            if (nodeKeySet.remove(nodeKey)) {
                totalCount.decrementAndGet();    
            }
        }
    }

    @Override
    public void remove( String nodeKey,
                        String propertyName, 
                        String value ) {
        Set<String> nodeKeySet = nodeKeySetsByValue.get(value);
        if (nodeKeySet != null) {
            if (nodeKeySet.remove(nodeKey)) {
                totalCount.decrementAndGet();
            }
        }
    }

    @Override
    public synchronized void clearAllData() {
        for (Map.Entry<String, Set<String>> entry : nodeKeySetsByValue.entrySet()) {
            entry.getValue().clear();
            String collectionName = collectionName(entry.getKey());
            if (db.exists(collectionName)) {
                db.delete(collectionName);
            }
        }
        nodeKeySetsByValue.clear();
        totalCount.set(0);
    }

    @Override
    public synchronized void shutdown( boolean destroyed ) {
        if (destroyed) {
            // Remove the database since the index was destroyed ...
            for (String value : nodeKeySetsByValue.keySet()) {
                String collectionName = collectionName(value);
                if (db.exists(collectionName)) {
                    db.delete(collectionName);
                }
            }
            nodeKeySetsByValue.clear();
            totalCount.set(0);
        }
    }

}
