from bisect import bisect

from uhashring.ring_ketama import KetamaRing
from uhashring.ring_meta import MetaRing


class HashRing:
    """Implement a consistent hashing ring."""

    def __init__(self, nodes=[], **kwargs):
        """Create a new HashRing given the implementation.

        :param nodes: nodes used to create the continuum (see doc for format).
        :param hash_fn: use this callable function to hash keys, can be set to
                        'ketama' to use the ketama compatible implementation.
        :param vnodes: default number of vnodes per node.
        :param weight_fn: use this function to calculate the node's weight.
        """
        hash_fn = kwargs.get("hash_fn", None)
        vnodes = kwargs.get("vnodes", None)
        weight_fn = kwargs.get("weight_fn", None)

        if hash_fn == "ketama":
            ketama_args = {k: v for k, v in kwargs.items() if k in ("replicas",)}
            if vnodes is None:
                vnodes = 40
            self.runtime = KetamaRing(**ketama_args)
        else:
            if vnodes is None:
                vnodes = 160
            self.runtime = MetaRing(hash_fn)

        self._default_vnodes = vnodes
        self.hashi = self.runtime.hashi

        if weight_fn and not hasattr(weight_fn, "__call__"):
            raise TypeError("weight_fn should be a callable function")
        self._weight_fn = weight_fn

        if self._configure_nodes(nodes):
            self.runtime._create_ring(self.runtime._nodes.items())

    def _configure_nodes(self, nodes):
        """Parse and set up the given nodes.

        :param nodes: nodes used to create the continuum (see doc for format).
        """
        if isinstance(nodes, str):
            nodes = [nodes]
        elif not isinstance(nodes, (dict, list)):
            raise ValueError(
                "nodes configuration should be a list or a dict,"
                " got {}".format(type(nodes))
            )

        conf_changed = False
        for node in nodes:
            conf = {
                "hostname": node,
                "instance": None,
                "nodename": node,
                "port": None,
                "vnodes": self._default_vnodes,
                "weight": 1,
            }
            current_conf = self.runtime._nodes.get(node, {})
            nodename = node
            # new node, trigger a ring update
            if not current_conf:
                conf_changed = True
            # complex config
            if isinstance(nodes, dict):
                node_conf = nodes[node]
                if isinstance(node_conf, int):
                    conf["weight"] = node_conf
                elif isinstance(node_conf, dict):
                    for k, v in node_conf.items():
                        if k in conf:
                            conf[k] = v
                            # changing those config trigger a ring update
                            if k in ["nodename", "vnodes", "weight"]:
                                if current_conf.get(k) != v:
                                    conf_changed = True
                else:
                    raise ValueError(
                        "node configuration should be a dict or an int,"
                        " got {}".format(type(node_conf))
                    )
            if self._weight_fn:
                conf["weight"] = self._weight_fn(**conf)
            # changing the weight of a node trigger a ring update
            if current_conf.get("weight") != conf["weight"]:
                conf_changed = True
            self.runtime._nodes[nodename] = conf
        return conf_changed

    def __delitem__(self, nodename):
        """Remove the given node.

        :param nodename: the node name.
        """
        self.runtime._remove_node(nodename)

    remove_node = __delitem__

    def __getitem__(self, key):
        """Returns the instance of the node matching the hashed key.

        :param key: the key to look for.
        """
        return self._get(key, "instance")

    get_node_instance = __getitem__

    def __setitem__(self, nodename, conf={"weight": 1}):
        """Add the given node with its associated configuration.

        :param nodename: the node name.
        :param conf: the node configuration.
        """
        if self._configure_nodes({nodename: conf}):
            self.runtime._create_ring([(nodename, self._nodes[nodename])])

    add_node = __setitem__

    def _get_pos(self, key):
        """Get the index of the given key in the sorted key list.

        We return the position with the nearest hash based on
        the provided key unless we reach the end of the continuum/ring
        in which case we return the 0 (beginning) index position.

        :param key: the key to hash and look for.
        """
        p = bisect(self.runtime._keys, self.hashi(key))
        if p == len(self.runtime._keys):
            return 0
        else:
            return p

    def _get(self, key, what):
        """Generic getter magic method.

        The node with the nearest but not less hash value is returned.

        :param key: the key to look for.
        :param what: the information to look for in, allowed values:
            - instance (default): associated node instance
            - nodename: node name
            - pos: index of the given key in the ring
            - tuple: ketama compatible (pos, name) tuple
            - weight: node weight
        """
        if not self.runtime._ring:
            return None

        pos = self._get_pos(key)
        if what == "pos":
            return pos

        nodename = self.runtime._ring[self.runtime._keys[pos]]
        if what in ["hostname", "instance", "port", "weight"]:
            return self.runtime._nodes[nodename][what]
        elif what == "dict":
            return self.runtime._nodes[nodename]
        elif what == "nodename":
            return nodename
        elif what == "tuple":
            return (self.runtime._keys[pos], nodename)

    def get(self, key):
        """Returns the node object dict matching the hashed key.

        :param key: the key to look for.
        """