/*
 * 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.ignite.internal.util;

import java.util.Comparator;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.lang.IgniteBiInClosure;
import org.jetbrains.annotations.Nullable;

/**
 * Concurrent ordered map that automatically manages its maximum size.
 * Once it exceeds its maximum, it will start removing smallest elements
 * until the maximum is reached again. If optional listener is set
 * via {@link #evictionListener(org.apache.ignite.lang.IgniteBiInClosure)} method, then listener
 * will be notified for every eviction.
 * <p>
 * Note that due to concurrent nature of this map, it may grow slightly
 * larger than its maximum allowed size, but in this case it will quickly
 * readjust back to allowed size.
 * <p>
 * Note that {@link #remove(Object)} and {@link #remove(Object, Object)} methods
 * are not supported for this kind of map.
 */
public class GridBoundedConcurrentOrderedMap<K, V> extends ConcurrentSkipListMap<K, V> {
    /** */
    private static final long serialVersionUID = 0L;

    /** Element count. */
    private final AtomicInteger cnt = new AtomicInteger();

    /** Maximum size. */
    private int max;

    /** Listener. */
    private volatile IgniteBiInClosure<K, V> lsnr;

    /**
     * Constructs a new, empty map that orders its elements according to
     * their {@linkplain Comparable natural ordering}.
     *
     * @param max Upper bound of this map.
     */
    public GridBoundedConcurrentOrderedMap(int max) {
        A.ensure(max > 0, "max > 0");

        this.max = max;
    }

    /**
     * Constructs a new, empty set that orders its elements according to
     * the specified comparator.
     *
     * @param max Upper bound of this map.
     * @param comparator The comparator that will be used to order this map.
     *      If <tt>null</tt>, the {@linkplain Comparable natural
     *      ordering} of the elements will be used.
     */
    public GridBoundedConcurrentOrderedMap(int max, Comparator<? super K> comparator) {
        super(comparator);

        A.ensure(max > 0, "max > 0");

        this.max = max;
    }

    /**
     * Constructs a new map containing the elements in the specified
     * map, that orders its elements according to their
     * {@linkplain Comparable natural ordering}.
     *
     * @param max Upper bound of this map.
     * @param map The elements that will comprise the new map.
     * @throws ClassCastException if the elements in <tt>map</tt> are
     *      not {@link Comparable}, or are not mutually comparable.
     * @throws NullPointerException if the specified map or any
     *      of its elements are {@code null}.
     */
    public GridBoundedConcurrentOrderedMap(int max, Map<? extends K, ? extends V> map) {
        super(map);

        A.ensure(max > 0, "max > 0");

        this.max = max;
    }

    /**
     * Constructs a new map containing the same elements and using the
     * same ordering as the specified sorted map.
     *
     * @param max Upper bound of this map.
     * @param map Sorted map whose elements will comprise the new map.
     * @throws NullPointerException if the specified sorted map or any
     *      of its elements are {@code null}.
     */
    public GridBoundedConcurrentOrderedMap(int max, SortedMap<K, V> map) {
        super(map);

        A.ensure(max > 0, "max > 0");

        this.max = max;
    }

    /**
     * Sets closure listener to be called for every eviction event.
     *
     * @param lsnr Closure to be called for every eviction event.
     */
    public void evictionListener(IgniteBiInClosure<K, V> lsnr) {
        this.lsnr = lsnr;
    }

    /**
     * Gets closure listener to be called for every eviction event.
     *
     * @return Closure to be called for every eviction event.
     */
    public IgniteBiInClosure<K, V> evictionListener() {
        return lsnr;
    }

    /** {@inheritDoc} */
    @Nullable @Override public V put(K k, V v) {
        A.notNull(k, "k", v, "v");

        V ret = super.put(k, v);

        onPut();

        return ret;
    }

    /** {@inheritDoc} */
    @Nullable @Override public V putIfAbsent(K k, V v) {
        A.notNull(k, "k", v, "v");

        V ret = super.putIfAbsent(k, v);

        // Handle size only if put succeeded.
        if (ret == null)
            onPut();

        return ret;
    }

    /**
     * Handles size after put.
     */
    private void onPut() {
        cnt.incrementAndGet();

        IgniteBiInClosure<K, V> lsnr = this.lsnr;

        int delta = cnt.get() - max;

        for (int i = 0; i < delta && cnt.get() > max; i++) {
            Entry<K, V> e = pollFirstEntry();

            if (e == null)
                return;

            cnt.decrementAndGet();

            // Listener notification.
            if (lsnr != null)
                lsnr.apply(e.getKey(), e.getValue());
        }
    }

    /**
     * Approximate size at this point of time. Note, that unlike {@code size}
     * methods on other {@code concurrent} collections, this method executes
     * in constant time without traversal of the elements.
     *
     * @return Approximate set size at this point of time.
     */
    @Override public int size() {
        return cnt.get();
    }

    /** {@inheritDoc} */
    @Override public GridBoundedConcurrentOrderedMap<K, V> clone() {
        GridBoundedConcurrentOrderedMap<K, V> map = (GridBoundedConcurrentOrderedMap<K, V>)super.clone();

        map.max = max;

        return map;
    }

    /**
     * This method is not supported and always throws {@link UnsupportedOperationException}.
     *
     * @param o {@inheritDoc}
     * @return {@inheritDoc}
     */
    @Override public V remove(Object o) {
        V old = super.remove(o);

        if (old != null)
            cnt.decrementAndGet();

        return old;
    }

    /**
     * This method is not supported and always throws {@link UnsupportedOperationException}.
     *
     * @param key {@inheritDoc}
     * @param val {@inheritDoc}
     * @return {@inheritDoc}
     */
    @Override public boolean remove(Object key, Object val) {
        boolean rmvd = super.remove(key, val);

        if (rmvd)
            cnt.decrementAndGet();

        return rmvd;
    }

    /** {@inheritDoc} */
    @Override public void clear() {
        throw new UnsupportedOperationException();
    }
}
