#
# Copyright (c) 2010, Abdelkader ALLAM <abdelkader.allam at gmail dot com>
# All rights reserved.
#
# This source also contains source code from Redis
# developped by Salvatore Sanfilippo <antirez at gmail dot com>
# available at http://github.com/antirez/redis
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the distribution.
#    * Neither the name of Redis nor the names of its contributors may be used
#      to endorse or promote products derived from this software without
#      specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#  POSSIBILITY OF SUCH DAMAGE.
#
import socket
import os
import pickle
import time
import urllib2
import threading
import random
import json
from pkg_resources import resource_string
import __builtin__
redisCommands = None


def reloadCommands(url):
    global redisCommands
    try:
        u = urllib2.urlopen(url)
        redisCommands = json.load(u)
    except urllib2.HTTPError:
        raise(Exception("Error unable to load commmands json file"))


if "urlCommands" in dir(__builtin__):
    reloadCommands(__builtin__.urlCommands)
# urlCommands="https://raw.githubusercontent.com/antirez/redis-doc/master/commands.json"
# reloadCommands(urlCommands) uncomment if you want to update it at each import
if not redisCommands:
    try:
        redisCommands = json.loads(resource_string(__name__, "commands.json"))
    except IOError:
        raise(Exception("Error unable to load commmands json file"))


class SWM(dict):
    """
    Connector message
    """
    def __init__(self, initd=None):
        if initd is None:
            initd = {}
        dict.__init__(self, initd)

    def __getattr__(self, item):
        try:
            d = self.__getitem__(item)
        except KeyError:
            raise(AttributeError)
        # if value is the only key in object, you can omit it
        if isinstance(d, dict) and 'value' in d and len(d) == 1:
            return d['value']
        else:
            return d

    def __setattr__(self, item, value):
        self.__setitem__(item, value)


class RedisError(Exception):
    pass


class NodeError(Exception):
    pass


class ConnectorError(Exception):
    pass


class RedisInner(object):
    def __init__(self, cls):
        self.cls = cls

    def __get__(self, instance, outerclass):
        class Wrapper(self.cls):
            _redis = instance
        Wrapper.__name__ = self.cls.__name__
        return Wrapper


cmdmap = {"del": "delete", "exec": "execute"}


class MetaRedis(type):
    def __new__(metacls, name, bases, dct):
        def _wrapper(name, redisCommand, methoddct):
            runcmd = "runcmd"
            if name == "SELECT":
                runcmd = "_select"

            def _rediscmd(self, *args):
                return methoddct[runcmd](self, name, *args)
            _rediscmd.__name__ = cmdmap.get(
                name.lower(), str(name.lower().replace(" ", "_")))
            _rediscmd.__redisname__ = name
            _rediscmd._json = redisCommand
            if "summary" in redisCommand:
                _doc = redisCommand["summary"]
                if "arguments" in redisCommand:
                    _doc += "\nParameters:\n"
                    for d in redisCommand["arguments"]:
                        if "name" in d:
                            _doc += ("Name: %s,\tType: %s,\t"
                                     "Multiple parameter:%s\n") % (
                                         d["name"], d.get("type", "?"),
                                         d.get("multiple", "False"))
                _rediscmd.__doc__ = _doc
            _rediscmd.__dict__.update(methoddct[runcmd].__dict__)
            return _rediscmd
        if name != "Redis":
            return type.__new__(metacls, name, bases, dct)
        newDct = {}
        for k in redisCommands.keys():
            newDct[cmdmap.get(k.lower(), str(k.lower().replace(" ", "_")))] = \
                _wrapper(k, redisCommands[k], dct)
        newDct.update(dct)
        return type.__new__(metacls, name, bases, newDct)


class SubAsync(threading.Thread):
    def __init__(self, channel, callback, redis_param={}):
        threading.Thread.__init__(self)
        self.setDaemon(1)
        self.channel = channel
        self.callback = callback
        self.param = redis_param
        self.start()

    def run(self):
        import desir
        self._redis = desir.Redis(**self.param)
        self._redis.subscribe(self.channel)
        for v in self._redis.listen():
            self.callback(v)


class Redis(threading.local):
    """
    class providing a client interface to Redis
    this class is a minimalist implementation of
    http://code.google.com/p/redis/wiki/CommandReference
    except for the DEL and EXEC command which are renamed delete and execute
    because they are reserved names in python
    """
    __metaclass__ = MetaRedis

    class String(object):
        """
        Redis String descriptor object
        """
        def __init__(self, name):
            self.name = name

        def __get__(self, instance, owner):
            return [self, instance, owner, self._redis.get(self.name)]

        def __set__(self, instance, value):
            return self._redis.set(self.name, value)

    String = RedisInner(String)

    class Counter:
        def __init__(self, name, seed=0):
            self.name = name
            if seed is not None:
                self._redis.set(self.name, seed)

        def __iter__(self):
            return self

        def __int__(self):
            return int(self._redis.get(self.name))

        def __str__(self):
            return self._redis.get(self.name)

        def next(self):
            return self._redis.incr(self.name)

    Counter = RedisInner(Counter)

    class Connector(object):
        """
        safe queue implementation under work
        CONNECTORNAME:PID:TIMESTAMP
        """
        def __init__(self, name, ctype="", timeout=0, fifo=True, safe=False,
                     secret=None):
            self.name = name
            self.timeout = timeout
            # connector queue/list set to fifo when fifo is true,
            # and lifo when false
            self.fifo = fifo
            # when safe is true, connector works in safe mode meaning each time
            # a value is popped out of the list it is atomically pushed to
            # a dedicated list
            # when the program is done processing the object that was
            # popped out,
            # it can release it with the release commands which will remove it
            # from the dedicated list
            self.safe = safe
            self.ctype = ctype
            self.secret = secret

        def __iter__(self):
            return self

        def sendreceive(self, name, val=None, timeout=0):
            srcreply = self.name+":"+str(time.time())+str(random.random())
            self.send(name, val, srcreply)
            return self.receive(timeout=timeout, srcreply=srcreply)

        def send(self, name, val=None, srcreply=None):
            if srcreply:
                vd = SWM(dict(src=srcreply, srctype=self.ctype,
                              dst=name, time=time.time(), val=val))
            else:
                vd = SWM(dict(src=self.name, srctype=self.ctype,
                              dst=name, time=time.time(), val=val))
            # if not val:
            #    vd.update(name)
            vp = pickle.dumps(vd)
            if self.secret:
                import hashlib
                vs = hashlib.sha1()
                vs.update(vp)
                vs.update(self.secret)
                vp = vs.digest()+vp
            if self.fifo:
                return self._redis.lpush(vd.dst, vp)
            else:
                return self._redis.rpush(vd.dst, vp)

        def receive(self, timeout=0, srcreply=None):
            tmpname = "%s:%d:%d" % (self.name, os.getpid(), int(time.time()))
            if srcreply is not None:
                srcreply = self.name
            if self.safe:
                if timeout == -1:
                    resp = self._redis.rpoplpush(srcreply, tmpname)
                else:
                    resp = self._redis.brpoplpush(srcreply, tmpname, timeout)
            else:
                if timeout == -1:
                    resp = self._redis.rpop(srcreply)
                else:
                    resp = self._redis.brpop(srcreply, timeout)
                    resp = resp and resp[1]
            if resp:
                if self.secret:
                    import hashlib
                    vs = hashlib.sha1()
                    vs.update(resp[20:])
                    vs.update(self.secret)
                    if resp[:20] != vs.digest():
                        raise ConnectorError("Digest signature failed")
                    resp = resp[20:]
                resp = pickle.loads(resp)
                if self.safe:
                    resp["srcack"] = tmpname
            return resp

        def unreceive(self, val):
            if "srcack" in val:
                return self._redis.rpoplpush(val.srcack, self.name)

        def transfer(self, name, val, newval, force=True):
            res = None
            while not res:
                self._redis.watch(val.srcack)
                self._redis.multi()
                self.release(val)
                self.send(name, newval)
                res = self._redis.execute()
                if not force:
                    break
            return res

        def reply(self, val, newval, force=True):
            res = None
            while not res:
                if "srcack" in val:
                    self._redis.watch(val.srcack)
                self._redis.multi()
                self.release(val)
                self.send(val.src, newval)
                res = self._redis.execute()
                if not force:
                    break
            return res

        def release(self, val):
            if "srcack" in val:
                return self._redis.rpop(val.srcack)

        def next(self):
            resp = self.receive(self.timeout)
            if resp:
                return resp
            else:
                raise StopIteration

    Connector = RedisInner(Connector)

    class Hash(object):
        def __init__(self, name):
            self._keyid = name

        def __repr__(self):
            return str(self.items())

        def __getattr__(self, item):
            if item.startswith("_"):
                return object.__getattribute__(self, item)
            resp = self._redis.hget(self._keyid, item)
            if resp:
                return resp
            else:
                raise AttributeError(
                    "Unkown attribute %s for object %s" % (item, self._keyid))

        def __setattr__(self, item, value):
            if item.startswith("_"):
                return object.__setattr__(self, item, value)
            else:
                self._redis.hset(self._keyid, item, value)

        def keys(self):
            return self._redis.hkeys(self._keyid)

        def values(self):
            return self._redis.hvals(self._keyid)

        def items(self):
            resp = self._redis.hgetall(self._keyid)
            if resp:
                return zip(resp[::2], resp[1::2])
    Hash = RedisInner(Hash)

    def __init__(self, host="localhost", port=6379, db=0,
                 password=None, timeout=None, safe=False):
        self.host = host
        self.port = port
        self.timeout = timeout
        self.db = db
        self.password = password
        self.safe = safe
        self.safewait = 0.1
        self.Nodes = [Node(host, port, db, password, timeout)]
        self.transaction = False
        self.subscribed = False

    def listen(self, todict=False):
        while self.subscribed:
            r = self.Nodes[0].parse_resp()
            if r[0] == 'unsubscribe' and r[2] == 0:
                self.subscribed = False
            if todict:
                if r[0] == "pmessage":
                    r = dict(type=r[0], pattern=r[1], channel=r[2], data=r[3])
                else:
                    r = dict(type=r[0], pattern=None, channel=r[1], data=r[2])
            yield r

    def runcmd(self, cmdname, *args):
        # cluster implementation to come soon after antirez publish
        # the first cluster implementation
        if cmdname in ["MULTI", "WATCH"]:
            self.transaction = True
        if self.safe and not self.transaction and not self.subscribed:
            try:
                return self.Nodes[0].runcmd(cmdname, *args)
            except NodeError:
                time.sleep(self.safewait)
        if cmdname in ["DISCARD", "EXEC", "UNWATCH"]:
            self.transaction = False
        try:
            if cmdname in ["SUBSCRIBE", "PSUBSCRIBE",
                           "UNSUBSCRIBE", "PUNSUBSCRIBE"]:
                self.Nodes[0].sendcmd(cmdname, *args)
                rsp = self.Nodes[0].parse_resp()
            else:
                rsp = self.Nodes[0].runcmd(cmdname, *args)
            if cmdname in ["SUBSCRIBE", "PSUBSCRIBE"]:
                self.subscribed = True
            return rsp
        except NodeError as e:
            self.transaction = False
            self.subscribed = False
            raise NodeError(e)

    def _select(self, cmdname, *args):
        resp = self.runcmd(cmdname, *args)
        if resp == "OK":
            self.db = int(args[0])
        return resp

    def runcmdon(self, node, cmdname, *args):
        return self.node.runcmd(cmdname, *args)


class Node(object):
    """
    Manage TCP connections to a redis node
    """
    def __init__(self, host="localhost", port=6379, db=0,
                 password=None, timeout=None):
        self.host = host
        self.port = port
        self.timeout = timeout
        self.password = password
        self._sock = None
        self._fp = None
        self.db = db

    def connect(self):
        if self._sock:
            return
        addrinfo = socket.getaddrinfo(self.host, self.port)
        addrinfo.sort(key=lambda x: 0 if x[0] == socket.AF_INET else 1)
        family, _, _, _, _ = addrinfo[0]
        sock = socket.socket(family, socket.SOCK_STREAM)
        try:
            sock.connect((self.host, self.port))
            sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
            sock.settimeout(self.timeout)
            self._sock = sock
            self._fp = sock.makefile('r')
        except socket.error as msg:
            if len(msg.args) == 1:
                raise NodeError("Error connecting %s:%s. %s." %
                                (self.host, self.port, msg.args[0]))
            else:
                raise NodeError("Error %s connecting %s:%s. %s." % (
                    msg.args[0], self.host, self.port, msg.args[1]))
        finally:
            if self.password:
                if not self.runcmd("auth", self.password):
                    raise RedisError("Authentication error: Invalid password")
            if self._sock:
                self.runcmd("select", "0")

    def disconnect(self):
        if self._sock:
            try:
                self._sock.close()
            except socket.error:
                pass
            finally:
                self._sock = None
                self._fp = None

    def read(self, length):
        try:
            return self._fp.read(length)
        except socket.error as msg:
            self.disconnet()
            if len(msg.args) == 1:
                raise NodeError("Error connecting %s:%s. %s." %
                                (self.host, self.port, msg.args[0]))
            else:
                raise NodeError("Error %s connecting %s:%s. %s." % (
                    msg.args[0], self.host, self.port, msg.args[1]))

    def readline(self):
        try:
            return self._fp.readline()
        except socket.error as msg:
            self.disconnect()
            if len(msg.args) == 1:
                raise NodeError("Error connecting %s:%s. %s." %
                                (self.host, self.port, msg.args[0]))
            else:
                raise NodeError("Error %s connecting %s:%s. %s." % (
                    msg.args[0], self.host, self.port, msg.args[1]))

    def sendline(self, message):
        self.connect()
        try:
            self._sock.send(message+"\r\n")
        except socket.error as msg:
            self.disconnect()
            if len(msg.args) == 1:
                raise NodeError("Error connecting %s:%s. %s." %
                                (self.host, self.port, msg.args[0]))
            else:
                raise NodeError("Error %s connecting %s:%s. %s." % (
                    msg.args[0], self.host, self.port, msg.args[1]))

    def sendcmd(self, *args):
        args2 = args[0].split()
        args2.extend(args[1:])
        cmd = ""
        cmd += "*%d" % (len(args2))
        for arg in args2:
            cmd += "\r\n"
            cmd += "$%d\r\n" % (len(str(arg)))
            cmd += str(arg)
        self.sendline(cmd)

    def parse_resp(self):
        resp = self.readline()
        if not resp:
            # resp empty what is happening ? to be investigated
            return None
        if resp[:-2] in ["$-1", "*-1"]:
            return None
        fb, resp = resp[0], resp[1:]
        if fb == "+":
            return resp[:-2]
        if fb == "-":
            raise RedisError(resp)
        if fb == ":":
            return int(resp)
        if fb == "$":
            resp = self.read(int(resp))
            self.read(2)
            return resp
        if fb == "*":
            return [self.parse_resp() for i in range(int(resp))]

    def runcmd(self, cmdname, *args):
        self.sendcmd(cmdname, *args)
        return self.parse_resp()
