/**
 * Copyright (C) 2014-2017 Xavier Witdouck
 *
 * 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 com.zavtech.morpheus.index;

import java.util.function.Predicate;

import com.zavtech.morpheus.array.Array;
import com.zavtech.morpheus.array.ArrayBuilder;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;

/**
 * An Index implementation designed to store any object type.
 *
 * <p>This is open source software released under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 License</a></p>
 *
 * @author  Xavier Witdouck
 */
class IndexOfObjects<K> extends IndexBase<K> {

    private static final long serialVersionUID = 1L;

    private TObjectIntMap<K> indexMap;

    /**
     * Constructor
     * @param type      the element type
     * @param initialSize   the initial size for this index
     */
    IndexOfObjects(Class<K> type, int initialSize) {
        super(Array.of(type, initialSize));
        this.indexMap = new TObjectIntHashMap<>(initialSize, 0.75f, -1);
    }

    /**
     * Constructor
     * @param iterable      the keys for index
     */
    IndexOfObjects(Iterable<K> iterable) {
        super(iterable);
        this.indexMap = new TObjectIntHashMap<>(keyArray().length(), 0.75f, -1);
        this.keyArray().sequential().forEachValue(v -> {
            final int index = v.index();
            final K key = v.getValue();
            final int existing = indexMap.put(key, index);
            if (existing >= 0) {
                throw new IndexException("Cannot have duplicate keys in index: " + v.getValue());
            }
        });
    }

    /**
     * Constructor
     * @param iterable  the keys for index
     * @param parent    the parent index to initialize from
     */
    private IndexOfObjects(Iterable<K> iterable, IndexOfObjects<K> parent) {
        super(iterable, parent);
        this.indexMap = new TObjectIntHashMap<>(keyArray().length(), 0.75f, -1);
        this.keyArray().sequential().forEachValue(v -> {
            final K key = v.getValue();
            final int index = parent.indexMap.get(key);
            if (index < 0) throw new IndexException("No match for key: " + v.getValue());
            final int existing = indexMap.put(key, index);
            if (existing >= 0) {
                throw new IndexException("Cannot have duplicate keys in index: " + v.getValue());
            }
        });
    }

    @Override()
    public final Index<K> filter(Iterable<K> keys) {
        return new IndexOfObjects<>(keys, isFilter() ? (IndexOfObjects<K>)parent() : this);
    }

    @Override
    public Index<K> filter(Predicate<K> predicate) {
        final int count = size();
        final Class<K> type = type();
        final ArrayBuilder<K> builder = ArrayBuilder.of(count / 2, type);
        for (int i=0; i<count; ++i) {
            final K value = keyArray().getValue(i);
            if (predicate.test(value)) {
                builder.add(value);
            }
        }
        final Array<K> filter = builder.toArray();
        return new IndexOfObjects<>(filter, isFilter() ? (IndexOfObjects<K>)parent() : this);
    }

    @Override
    public final boolean add(K key) {
        if (isFilter()) {
            throw new IndexException("Cannot add keys to an filter on another index");
        } else {
            if (indexMap.containsKey(key)) {
                return false;
            } else {
                final int index = indexMap.size();
                this.ensureCapacity(index + 1);
                this.keyArray().setValue(index, key);
                this.indexMap.put(key, index);
                return true;
            }
        }
    }

    @Override
    public int addAll(Iterable<K> keys, boolean ignoreDuplicates) {
        if (isFilter()) {
            throw new IndexException("Cannot add keys to an filter on another index");
        } else {
            final int[] count = new int[1];
            keys.forEach(key -> {
                if (!indexMap.containsKey(key)) {
                    final int index = indexMap.size();
                    this.ensureCapacity(index + 1);
                    this.keyArray().setValue(index, key);
                    final int existing = indexMap.put(key, index);
                    if (!ignoreDuplicates && existing >= 0) {
                        throw new IndexException("Attempt to add duplicate key to index: " + key);
                    }
                    ++count[0];
                }
            });
            return count[0];
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public final Index<K> copy() {
        try {
            final IndexOfObjects<K> clone = (IndexOfObjects<K>)super.copy();
            clone.indexMap = new TObjectIntHashMap<>(indexMap);
            return clone;
        } catch (Exception ex) {
            throw new IndexException("Failed to clone index", ex);
        }
    }

    @Override
    public final int size() {
        return indexMap.size();
    }

    @Override
    public final int getIndexForKey(K key) {
        final int index = indexMap.get(key);
        if (index < 0) {
            throw new IndexException("No match for key in index: " + key);
        } else {
            return index;
        }
    }

    @Override
    public final boolean contains(K key) {
        return indexMap.containsKey(key);
    }

    @Override
    public final int replace(K existing, K replacement) {
        final int index = indexMap.remove(existing);
        if (index == -1) {
            throw new IndexException("No match key for " + existing);
        } else {
            if (indexMap.containsKey(replacement)) {
                throw new IndexException("The replacement key already exists in index " + replacement);
            } else {
                final int ordinal = getOrdinalForIndex(index);
                this.indexMap.put(replacement, index);
                this.keyArray().setValue(ordinal, replacement);
                return index;
            }
        }
    }

    @Override()
    public final void forEachEntry(IndexConsumer<K> consumer) {
        final int size = size();
        for (int i=0; i<size; ++i) {
            final K key = keyArray().getValue(i);
            final int index = indexMap.get(key);
            consumer.accept(key, index);
        }
    }

}