/*
 * 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.cassandra.utils.btree;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Nullable;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;

import static java.util.Comparator.naturalOrder;

public class BTreeMultimap<K, V> implements Multimap<K, V>
{
    private final BTreeMap<K, Collection<V>> map;
    private final Comparator<K> comparator;
    private final Comparator<V> valueComparator;
    private final int size;

    private BTreeMultimap(BTreeMap<K, Collection<V>> map, Comparator<K> comparator, Comparator<V> valueComparator, int size)
    {
        this.map = map;
        this.comparator = comparator;
        this.valueComparator = valueComparator;
        this.size = size;
    }

    public static <K extends Comparable<K>, V extends Comparable<V>> BTreeMultimap<K, V> empty()
    {
        return new BTreeMultimap<K, V>(BTreeMap.empty(), naturalOrder(), naturalOrder(), 0);
    }

    public BTreeMultimap<K, V> with(K key, V value)
    {
        if (map.containsKey(key))
        {
            BTreeSet<V> oldSet = (BTreeSet<V>) map.get(key);
            BTreeSet<V> newSet = oldSet.with(value);
            int newSize = size + newSet.size() - oldSet.size();
            return new BTreeMultimap<>(map.without(key).with(key, newSet), comparator, valueComparator, newSize);
        }
        else
        {
            BTreeSet<V> newSet = BTreeSet.of(valueComparator, value);
            return new BTreeMultimap<>(map.with(key, newSet), comparator, valueComparator, size + 1);
        }
    }

    public BTreeMultimap<K, V> without(K key)
    {
        Collection<V> oldSet = map.get(key);
        if (oldSet == null)
            return this;
        int newSize = size - oldSet.size();
        return new BTreeMultimap<>(map.without(key), comparator, valueComparator, newSize);
    }

    public BTreeMultimap<K, V> without(K key, V value)
    {
        BTreeSet<V> values = (BTreeSet<V>) map.get(key);
        if (values == null)
            return this;
        if (!values.contains(value))
            return this;
        BTreeSet<V> newValues = BTreeSet.wrap(BTreeRemoval.remove(values.tree, valueComparator, value), valueComparator);
        BTreeMap<K, Collection<V>> newMap = map.without(key);
        if (newValues.isEmpty())
            return new BTreeMultimap<>(newMap, comparator, valueComparator, size - 1);

        return new BTreeMultimap<>(newMap.with(key, newValues), comparator, valueComparator, size - 1);
    }

    @Override
    public int size()
    {
        return size;
    }

    @Override
    public boolean isEmpty()
    {
        return map.isEmpty();
    }

    @Override
    public boolean containsKey(@Nullable Object o)
    {
        if (o == null)
            return false;
        return map.containsKey(o);
    }

    @Override
    public boolean containsValue(@Nullable Object o)
    {
        if (o == null)
            return false;
        for (Map.Entry<K, Collection<V>> e : map.entrySet())
            if (e.getValue().contains(o))
                return true;
        return false;
    }

    @Override
    public boolean containsEntry(@Nullable Object key, @Nullable Object value)
    {
        if (key == null || value == null)
            throw new NullPointerException();
        return map.containsKey(key) && map.get(key).contains(value);
    }

    @Override
    public Collection<V> get(@Nullable K k)
    {
        if (k == null)
            return null;
        Collection<V> value = map.get(k);
        if (value == null)
            return Collections.emptySet();
        return value;
    }

    @Override
    public Set<K> keySet()
    {
        return map.keySet();
    }

    private transient Multiset<K> keys = null;
    @Override
    public Multiset<K> keys()
    {
        Multiset<K> res = keys;
        return res == null ? keys = createKeys() : res;
    }

    private Multiset<K> createKeys()
    {
        ImmutableMultiset.Builder<K> keys = ImmutableMultiset.builder();
        keys.addAll(map.keySet());
        return keys.build();
    }

    private transient Collection<V> values = null;
    @Override
    public Collection<V> values()
    {
        Collection<V> res = values;
        return res == null ? values = createValues() : res;
    }

    private Collection<V> createValues()
    {
        ImmutableList.Builder<V> builder = ImmutableList.builder();
        for (Map.Entry<K, Collection<V>> entry : map.entrySet())
            builder.addAll(entry.getValue());
        return builder.build();
    }

    private transient Collection<Map.Entry<K,V>> entries = null;
    @Override
    public Collection<Map.Entry<K, V>> entries()
    {
        Collection<Map.Entry<K, V>> res = entries;
        return res == null ? entries = createEntries() : res;
    }

    public Collection<Map.Entry<K, V>> createEntries()
    {
        Set<Map.Entry<K, V>> entries = new HashSet<>();
        for (Map.Entry<K, Collection<V>> entry : map.entrySet())
            for (V v : entry.getValue())
                entries.add(new AbstractBTreeMap.Entry<>(entry.getKey(), v));
        return entries;
    }

    @Override
    public Map<K, Collection<V>> asMap()
    {
        return map;
    }

    public boolean put(@Nullable K k, @Nullable V v) { throw new UnsupportedOperationException();}
    public boolean remove(@Nullable Object o, @Nullable Object o1) {throw new UnsupportedOperationException();}
    public boolean putAll(@Nullable K k, Iterable<? extends V> iterable) {throw new UnsupportedOperationException();}
    public boolean putAll(Multimap<? extends K, ? extends V> multimap) {throw new UnsupportedOperationException();}
    public Collection<V> replaceValues(@Nullable K k, Iterable<? extends V> iterable) {throw new UnsupportedOperationException();}
    public Collection<V> removeAll(@Nullable Object o) {throw new UnsupportedOperationException();}
    public void clear() { throw new UnsupportedOperationException(); }

    @Override
    public String toString()
    {
        return map.toString();
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (!(o instanceof BTreeMultimap)) return false;
        BTreeMultimap<?, ?> that = (BTreeMultimap<?, ?>) o;
        return size == that.size &&
               Objects.equals(map, that.map) &&
               Objects.equals(comparator, that.comparator) &&
               Objects.equals(valueComparator, that.valueComparator);
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(map, comparator, valueComparator, size);
    }
}
