/*
 * 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.ignite.internal.util.tostring.GridToStringInclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.lang.IgnitePredicate;
import org.jetbrains.annotations.Nullable;

/**
 * Controls key to node affinity using consistent hash algorithm. This class is thread-safe
 * and does not have to be externally synchronized.
 * <p>
 * For a good explanation of what consistent hashing is, you can refer to
 * <a href="http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html">Tom White's Blog</a>.
 */
public class GridConsistentHash<N> {
    /** Prime number. */
    private static final int PRIME = 15485857;

    /** Random generator. */
    private static final Random RAND = new Random();

    /** Affinity seed. */
    private final Object affSeed;

    /** Map of hash assignments. */
    @GridToStringInclude
    private final NavigableMap<Integer, SortedSet<N>> circle = new TreeMap<>();

    /** Read/write lock. */
    private final ReadWriteLock rw = new ReentrantReadWriteLock();

    /** Distinct nodes in the hash. */
    @GridToStringInclude
    private final Collection<N> nodes = new HashSet<>();

    /** Nodes comparator to resolve hash codes collisions. */
    private final Comparator<N> nodesComp;

    /**
     * Constructs consistent hash using given affinity seed and {@code MD5} hasher function.
     *
     * @param affSeed Affinity seed (will be used as key prefix for hashing).
     */
    public GridConsistentHash(@Nullable Object affSeed) {
        this(null, affSeed);
    }

    /**
     * Constructs consistent hash using default parameters.
     */
    public GridConsistentHash() {
        this(null, null);
    }

    /**
     * Constructs consistent hash using given affinity seed and hasher function.
     *
     * @param nodesComp Nodes comparator to resolve hash codes collisions.
     * @param affSeed Affinity seed (will be used as key prefix for hashing).
     */
    public GridConsistentHash(@Nullable Comparator<N> nodesComp, @Nullable Object affSeed) {
        this.nodesComp = nodesComp;
        this.affSeed = affSeed == null ? new Integer(PRIME) : affSeed;
    }

    /**
     * Adds nodes to consistent hash algorithm (if nodes are {@code null} or empty, then no-op).
     *
     * @param nodes Nodes to add.
     * @param replicas Number of replicas for every node.
     */
    public void addNodes(@Nullable Collection<N> nodes, int replicas) {
        if (F.isEmpty(nodes))
            return;

        assert nodes != null;

        rw.writeLock().lock();

        try {
            for (N node : nodes)
                addNode(node, replicas);
        }
        finally {
            rw.writeLock().unlock();
        }
    }

    /**
     * Adds a node to consistent hash algorithm.
     *
     * @param node New node (if {@code null} then no-op).
     * @param replicas Number of replicas for the node.
     * @return {@code True} if node was added, {@code false} if it is {@code null} or
     *      is already contained in the hash.
     */
    public boolean addNode(@Nullable N node, int replicas) {
        if (node == null)
            return false;

        long seed = affSeed.hashCode() * 31 + hash(node);

        rw.writeLock().lock();

        try {
            if (!nodes.add(node))
                return false;

            int hash = hash(seed);

            SortedSet<N> set = circle.get(hash);

            if (set == null)
                circle.put(hash, set = new TreeSet<>(nodesComp));

            set.add(node);

            for (int i = 1; i <= replicas; i++) {
                seed = seed * affSeed.hashCode() + i;

                hash = hash(seed);

                set = circle.get(hash);

                if (set == null)
                    circle.put(hash, set = new TreeSet<>(nodesComp));

                set.add(node);
            }

            return true;
        }
        finally {
            rw.writeLock().unlock();
        }
    }

    /**
     * Removes given nodes and all their replicas from consistent hash algorithm
     * (if nodes are {@code null} or empty, then no-op).
     *
     * @param nodes Nodes to remove.
     */
    public void removeNodes(@Nullable Collection<N> nodes) {
        if (F.isEmpty(nodes))
            return;

        rw.writeLock().lock();

        try {
            if (!this.nodes.removeAll(nodes))
                return;

            for (Iterator<SortedSet<N>> it = circle.values().iterator(); it.hasNext(); ) {
                SortedSet<N> set = it.next();

                if (!set.removeAll(nodes))
                    continue;

                if (set.isEmpty())
                    it.remove();
            }
        }
        finally {
            rw.writeLock().unlock();
        }
    }

    /**
     * Removes a node and all of its replicas.
     *
     * @param node Node to remove (if {@code null}, then no-op).
     * @return {@code True} if node was removed, {@code false} if node is {@code null} or
     *      not present in hash.
     */
    public boolean removeNode(@Nullable N node) {
        if (node == null)
            return false;

        rw.writeLock().lock();

        try {
            if (!nodes.remove(node))
                return false;

            for (Iterator<SortedSet<N>> it = circle.values().iterator(); it.hasNext(); ) {
                SortedSet<N> set = it.next();

                if (!set.remove(node))
                    continue;

                if (set.isEmpty())
                    it.remove();
            }

            return true;
        }
        finally {
            rw.writeLock().unlock();
        }
    }

    /**
     * Clears all nodes from consistent hash.
     */
    public void clear() {
        rw.writeLock().lock();

        try {
            nodes.clear();
            circle.clear();
        }
        finally {
            rw.writeLock().unlock();
        }
    }

    /**
     * Gets number of distinct nodes, excluding replicas, in consistent hash.
     *
     * @return Number of distinct nodes, excluding replicas, in consistent hash.
     */
    public int count() {
        rw.readLock().lock();

        try {
            return nodes.size();
        }
        finally {
            rw.readLock().unlock();
        }
    }

    /**
     * Gets size of all nodes (including replicas) in consistent hash.
     *
     * @return Size of all nodes (including replicas) in consistent hash.
     */
    public int size() {
        rw.readLock().lock();

        try {
            int size = 0;

            for (SortedSet<N> set : circle.values())
                size += set.size();

            return size;
        }
        finally {
            rw.readLock().unlock();
        }
    }

    /**
     * Checks if consistent hash has nodes added to it.
     *
     * @return {@code True} if consistent hash is empty, {@code false} otherwise.
     */
    public boolean isEmpty() {
        return count() == 0;
    }

    /**
     * Gets set of all distinct nodes in the consistent hash (in no particular order).
     *
     * @return Set of all distinct nodes in the consistent hash.
     */
    public Set<N> nodes() {
        rw.readLock().lock();

        try {
            return new HashSet<>(nodes);
        }
        finally {
            rw.readLock().unlock();
        }
    }

    /**
     * Picks a random node from consistent hash.
     *
     * @return Random node from consistent hash or {@code null} if there are no nodes.
     */
    @Nullable public N random() {
        return node(RAND.nextLong());
    }

    /**
     * Gets node for a key.
     *
     * @param key Key.
     * @return Node.
     */
    @Nullable public N node(@Nullable Object key) {
        int hash = hash(key);

        rw.readLock().lock();

        try {
            Map.Entry<Integer, SortedSet<N>> firstEntry = circle.firstEntry();

            if (firstEntry == null)
                return null;

            Map.Entry<Integer, SortedSet<N>> tailEntry = circle.tailMap(hash, true).firstEntry();

            // Get first node hash in the circle clock-wise.
            return circle.get(tailEntry == null ? firstEntry.getKey() : tailEntry.getKey()).first();
        }
        finally {
            rw.readLock().unlock();
        }
    }

    /**
     * Gets node for a given key.
     *
     * @param key Key to get node for.
     * @param inc Optional inclusion set. Only nodes contained in this set may be returned.
     *      If {@code null}, then all nodes may be included.
     * @return Node for key, or {@code null} if node was not found.
     */
    @Nullable public N node(@Nullable Object key, @Nullable Collection<N> inc) {
        return node(key, inc, null);
    }

    /**
     * Gets node for a given key.
     *
     * @param key Key to get node for.
     * @param inc Optional inclusion set. Only nodes contained in this set may be returned.
     *      If {@code null}, then all nodes may be included.
     * @param exc Optional exclusion set. Only nodes not contained in this set may be returned.
     *      If {@code null}, then all nodes may be returned.
     * @return Node for key, or {@code null} if node was not found.
     */
    @Nullable public N node(@Nullable Object key, @Nullable final Collection<N> inc,
        @Nullable final Collection<N> exc) {
        if (inc == null && exc == null)
            return node(key);

        return node(key, new IgnitePredicate<N>() {
            @Override public boolean apply(N n) {
                return (inc == null || inc.contains(n)) && (exc == null || !exc.contains(n));
            }
        });
    }

    /**
     * Gets node for a given key.
     *
     * @param key Key to get node for.
     * @param p Optional predicate for node filtering.
     * @return Node for key, or {@code null} if node was not found.
     */
    @Nullable public N node(@Nullable Object key, @Nullable IgnitePredicate<N>... p) {
        if (p == null || p.length == 0)
            return node(key);

        int hash = hash(key);

        rw.readLock().lock();

        try {
            final int size = nodes.size();

            if (size == 0)
                return null;

            Set<N> failed = null;

            // Move clock-wise starting from selected position 'hash'.
            for (SortedSet<N> set : circle.tailMap(hash, true).values()) { // Circle tail.
                for (N n : set) {
                    if (failed != null && failed.contains(n))
                        continue;

                    if (apply(p, n))
                        return n;

                    if (failed == null)
                        failed = new GridLeanSet<>(size);

                    failed.add(n);

                    if (failed.size() == size)
                        return null;
                }
            }

            //
            // Copy-paste is used to escape several new objects creation.
            //

            // Wrap around moving clock-wise from the circle start.
            for (SortedSet<N> set : circle.headMap(hash, false).values()) { // Circle head.
                for (N n : set) {
                    if (failed != null && failed.contains(n))
                        continue;

                    if (apply(p, n))
                        return n;

                    if (failed == null)
                        failed = new GridLeanSet<>(size);

                    failed.add(n);

                    if (failed.size() == size)
                        return null;
                }
            }

            return null;
        }
        finally {
            rw.readLock().unlock();
        }
    }

    /**
     * Gets specified count of adjacent nodes for a given key. If number of nodes in
     * consistent hash is less than specified count, then all nodes are returned.
     *
     * @param key Key to get adjacent nodes for.
     * @param cnt Number of adjacent nodes to get.
     * @return List containing adjacent nodes for given key.
     */
    public List<N> nodes(@Nullable Object key, int cnt) {
        return nodes(key, cnt, null, null);
    }

    /**
     * Gets specified count of adjacent nodes for a given key. If number of nodes in
     * consistent hash is less than specified count, then all nodes are returned.
     *
     * @param key Key to get adjacent nodes for.
     * @param cnt Number of adjacent nodes to get.
     * @param inc Optional inclusion set. Only nodes contained in this set may be returned.
     *      If {@code null}, then all nodes may be returned.
     * @return List containing adjacent nodes for given key.
     */
    public List<N> nodes(@Nullable Object key, int cnt, @Nullable Collection<N> inc) {
        return nodes(key, cnt, inc, null);
    }

    /**
     * Gets specified count of adjacent nodes for a given key. If number of nodes in
     * consistent hash is less than specified count, then all nodes are returned.
     *
     * @param key Key to get adjacent nodes for.
     * @param cnt Number of adjacent nodes to get.
     * @param inc Optional inclusion set. Only nodes contained in this set may be returned.
     *      If {@code null}, then all nodes may be included.
     * @param exc Optional exclusion set. Only nodes not contained in this set may be returned.
     *      If {@code null}, then all nodes may be returned.
     * @return List containing adjacent nodes for given key.
     */
    public List<N> nodes(@Nullable Object key, int cnt, @Nullable final Collection<N> inc,
        @Nullable final Collection<N> exc) {
        A.ensure(cnt >= 0, "cnt >= 0");

        if (cnt == 0)
            return Collections.emptyList();

        if (cnt == 1)
            return F.asList(node(key, inc, exc));

        return nodes(key, cnt, new IgnitePredicate<N>() {
            @Override public boolean apply(N n) {
                return (inc == null || inc.contains(n)) && (exc == null || !exc.contains(n));
            }
        });
    }

    /**
     * Gets specified count of adjacent nodes for a given key. If number of nodes in
     * consistent hash is less than specified count, then all nodes are returned.
     *
     * @param key Key to get adjacent nodes for.
     * @param cnt Number of adjacent nodes to get.
     * @param p Optional predicate to filter out nodes. Nodes that don't pass the filter
     *      will be skipped.
     * @return List containing adjacent nodes for given key.
     */
    public List<N> nodes(@Nullable Object key, int cnt, @Nullable IgnitePredicate<N>... p) {
        A.ensure(cnt >= 0, "cnt >= 0");

        if (cnt == 0)
            return Collections.emptyList();

        if (cnt == 1)
            return F.asList(node(key, p));

        int hash = hash(key);

        Collection<N> failed = new GridLeanSet<>();

        rw.readLock().lock();

        try {
            if (circle.isEmpty())
                return Collections.emptyList();

            int size = nodes.size();

            List<N> ret = new ArrayList<>(Math.min(cnt, size));

            // Move clock-wise starting from selected position.
            for (SortedSet<N> set : circle.tailMap(hash, true).values()) { // Circle tail.
                for (N n : set) {
                    if (ret.contains(n) || failed.contains(n))
                        continue;

                    if (apply(p, n))
                        ret.add(n);
                    else
                        failed.add(n);

                    if (cnt == ret.size() || size == ret.size() + failed.size())
                        return ret;
                }
            }

            //
            // Copy-paste is used to escape several new objects creation.
            //

            // Move clock-wise starting from head position.
            for (SortedSet<N> set : circle.headMap(hash, false).values()) { // Circle head.
                for (N n : set) {
                    if (ret.contains(n) || failed.contains(n))
                        continue;

                    if (apply(p, n))
                        ret.add(n);
                    else
                        failed.add(n);

                    if (cnt == ret.size() || size == ret.size() + failed.size())
                        return ret;
                }
            }

            return ret;
        }
        finally {
            rw.readLock().unlock();
        }
    }

    /**
     * @param p Predicate.
     * @param n Node.
     * @return {@code True} if filter passed or empty.
     */
    private boolean apply(IgnitePredicate<N>[] p, N n) {
        return F.isAll(n, p);
    }

    /**
     * Checks if key belongs to the given node.
     *
     * @param key Key to check.
     * @param node Node to check.
     * @return {@code True} if key belongs to given node.
     */
    public boolean belongs(@Nullable Object key, N node) {
        A.notNull(node, "node");

        N n = node(key);

        return n != null && n.equals(node);
    }

    /**
     * Checks if given key belongs to any of the given nodes.
     *
     * @param key Key to check.
     * @param nodes Nodes to check (if {@code null} then {@code false} is returned).
     * @return {@code True} if key belongs to any of the given nodes.
     */
    public boolean belongs(@Nullable Object key, @Nullable Collection<N> nodes) {
        if (F.isEmpty(nodes))
            return false;

        assert nodes != null;

        N n = node(key);

        return n != null && nodes.contains(n);
    }

    /**
     * Checks if node the key is mapped to or the given count of its adjacent nodes contain
     * the node passed in.
     *
     * @param key Key to check.
     * @param cnt Number of adjacent nodes to check.
     * @param node Node to check.
     * @return {@code True} if node the key is mapped to or given count of its adjacent
     *      neighbors contain the node.
     */
    public boolean belongs(@Nullable Object key, int cnt, N node) {
        return nodes(key, cnt).contains(node);
    }

    /**
     * Checks if node the key is mapped to or the given count of its adjacent nodes are
     * contained in given set of nodes.
     *
     * @param key Key to check.
     * @param cnt Number of adjacent nodes to check.
     * @param nodes Nodes to check.
     * @return {@code True} if node the key is mapped to or given count of its adjacent
     *      neighbors are contained in given set of nodes.
     */
    public boolean belongs(@Nullable Object key, int cnt, @Nullable Collection<N> nodes) {
        if (F.isEmpty(nodes))
            return false;

        assert nodes != null;

        return nodes.containsAll(nodes(key, cnt));
    }

    /**
     * Gets hash code for a given object.
     *
     * @param o Object to get hash code for.
     * @return Hash code.
     */
    protected int hash(Object o) {
        int h = o == null ? 0 : o.hashCode();

        // Spread bits to hash code.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);

        return h ^ (h >>> 16);
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return S.toString(GridConsistentHash.class, this);
    }
}