<gh_stars>0
import time
from sortedcontainers import SortedSet
from .exception import *
import traceback
import random

class Redis:
    def __init__(self):
        self.hash_table = {}
        self.expire = {}
        self.exp_queue = SortedSet()
        self.changed = {}
        self.z_map = {}

    def __repr__(self):
        return f'curr_time : {time.time()}\n\n' \
               f'hash_table :\n{self.hash_table}\n\n' \
               f'expire :\n{self.expire}\n\n' \
               f'changed :\n{self.changed}\n\n' \
               f'z_map :\n{self.z_map}'

    def log(self, curr_time, func, *args):
        command = [str(curr_time), func]
        for i in args:
            command.append(i)
        command = ' '.join(command)
        command = command.strip(' ')
        command += '\n'
        with open("log.txt", "a") as logfile:
            logfile.write(command)

    def search_util(self, item):
        low, high = 0, len(self.exp_queue)
        while low <= high:
            mid = (low + high) // 2
            if self.exp_queue[mid][0] == item:
                self.exp_queue.pop(mid)
                return True
            elif self.exp_queue[mid][0] < item[0]:
                low = mid + 1
            else:
                high = mid - 1
        return False

    def search_index_util(self, sorted_set, item):
        low, high = 0, len(sorted_set)-1
        while low <= high:
            mid = (low + high) // 2
            if sorted_set[mid][0] == item:
                return mid
            elif sorted_set[mid][0] < item:
                low = mid + 1
            else:
                high = mid - 1
        # return str(None)
        return 'Index Not Found'

    def destroy_util(self, curr_time, hash_key):
        """
        :param param:
        :return:
        """
        if hash_key in self.hash_table and self.expire[hash_key] is not None and self.expire[hash_key] <= round(time.time(), 4):
            self.DELETE(curr_time, hash_key)
            return True
        else:
            return False

    def auto_clean_util(self, curr_time):
        count = 0
        while count < 10 and len(self.hash_table) > 100:
            sample = random.sample(self.hash_table.keys(), 10)
            for key in sample:
                self.destroy_util(curr_time, key)
            count += 1


    def START(self):
        pass

    def GET(self, curr_time, hash_key):
        """
        :param param:
        :return:
        """
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table:
            if isinstance(self.hash_table[hash_key], str):
                # Element Found
                return self.hash_table[hash_key]
            else:
                # Throw Error -> Element found but not string
                raise NoStringValueError('NoStringValueError')
        else:
            # Element not found
            return str(None)

    def EXPIRE(self, curr_time, hash_key, seconds):
        """
        :param hash_key:
        :param seconds:
        :return:
        """
        if hash_key in self.hash_table:
            exp_time = round(curr_time, 4) + float(seconds)
            self.expire[hash_key] = exp_time
            if self.expire[hash_key] is None:
                self.exp_queue.add((exp_time, hash_key))
            else:
                # old_exp = self.expire[hash_key]
                # self.search_util((old_exp, hash_key))
                self.exp_queue.add((exp_time, hash_key))
            self.log(curr_time, "EXPIRE", hash_key, seconds)
            return '1'
        else:
            return '0'

    def TTL(self, curr_time, hash_key):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table and self.expire[hash_key] is not None:
            return str(time.time() - self.expire[hash_key])
        else:
            return str(None)

    def ping(self):
        return 'PONG'

    def DELETE(self, curr_time, hash_key):
        """
        :param param:
        :return:
        """

        if hash_key in self.hash_table:
            del self.hash_table[hash_key]
            del self.expire[hash_key]
            if hash_key in self.changed:
                del self.changed[hash_key]
            if hash_key in self.z_map:
                del self.z_map[hash_key]
            self.log(curr_time, "DELETE", hash_key)

            return '1'
        else:
            return '0'


    def SET(self, curr_time, hash_key, hash_val, *args):
        self.destroy_util(curr_time, hash_key)
        x = hash_key in self.hash_table
        ex, px, xx, nx, keepttl = [False] * 5
        kwargs = {}
        for i in range(len(args)):
            if args[i].lower() == 'ex':
                ex = True
                try:
                    kwargs['EX'] = int(args[i + 1])
                except:
                    raise InvalidRequest('InvalidRequest')
            elif args[i].lower() == 'px':
                px = True
                try:
                    kwargs['PX'] = int(args[i + 1])
                except:
                    raise InvalidRequest('InvalidRequest')
            elif args[i].lower() == 'xx':
                xx = True
            elif args[i].lower() == 'nx':
                nx = True
            elif args[i].lower() == 'keepttl':
                keepttl = True

        if (x is False and xx) or (x is True and nx):
            return str(None)
        # Make entry in both expiry table and hash table
        if (ex and px) or (nx and xx):
            raise InvalidRequest('InvalidRequest')
        if keepttl:
            if ex or nx:
                raise InvalidRequest('InvalidRequest')
            else:
                if hash_key in self.hash_table and self.expire[hash_key] is not None:
                    self.hash_table[hash_key] = str(hash_val)
                else:
                    self.hash_table[hash_key] = str(hash_val)
                    self.expire[hash_key] = None
                self.log(curr_time, 'SET', hash_key, hash_val, *args)
                return 'OK'
        else:
            self.hash_table[hash_key] = str(hash_val)
            old_exp = None
            if hash_key in self.expire:
                old_exp = self.expire[hash_key]
            self.expire[hash_key] = None
            if old_exp is not None:
                self.search_util((old_exp, hash_key))
            if ex:
                exp_time = round(curr_time, 4) + kwargs['EX']
                self.expire[hash_key] = exp_time
                # Remove then Add in Queue
                self.exp_queue.add((exp_time, hash_key))
            elif px:
                seconds = round((kwargs['PX'] / 1000), 4)
                exp_time = round(curr_time, 4) + seconds
                self.expire[hash_key] = exp_time
                # Remove and Add in Queue
                self.exp_queue.add((exp_time, hash_key))
            self.log(curr_time, 'SET', hash_key, hash_val, *args)
            return 'OK'

    def ZINCRBY(self, hash_key, score, key):
        # if hash_key in self.hash_table and type(self.hash_table[hash_key]) == SortedSet:
        pass



    def ZADD(self, curr_time, hash_key, score, key, *args):
        self.destroy_util(curr_time, hash_key)
        nx, xx, ch, incr, index = False, False, False, False, len(args)
        ind = 0
        for arg in args:
            if arg.lower() == 'nx':
                nx = True
                index = min(ind, index)
            elif arg.lower() == 'xx':
                xx = True
                index = min(ind, index)
            elif arg.lower() == 'ch':
                ch = True
                index = min(ind, index)
            elif arg.lower() == 'incr':
                incr = True
                index = min(ind, index)
            ind += 1
        keyword = ['NX', 'XX', 'CH', 'INCR']
        if index % 2 != 0 or (nx and xx):
            raise SyntaxError('SyntaxError')
        pairs = [(int(score), key)]
        # self.z_map[hash_key] = {key: int(score)}
        for i in range(0, index, 2):
            score1 = args[i]
            key1 = args[i + 1]
            pairs.append((int(score1), key1))
        if incr and len(pairs) > 0:
            raise SyntaxError('SyntaxError')

        # handle INCR Case
        # if incr:

        x = hash_key in self.hash_table
        if (x is False and xx) or (x is True and nx):
            if ch:
                if x:
                    return str(self.changed[hash_key])
                else:
                    return '0'
            else:
                if x:
                    return str(len(self.hash_table[hash_key]))
                else:
                    return '0'

        if hash_key not in self.hash_table:
            self.hash_table[hash_key] = SortedSet()
            self.changed[hash_key] = 0
            self.expire[hash_key] = None
            self.z_map[hash_key] = {}
        elif hash_key in self.hash_table and type(self.hash_table[hash_key]) != SortedSet:
            raise InvalidDataType('InvalidDataType')
        for score1, key1 in pairs:
            self.hash_table[hash_key].add((score1, key1))
            self.changed[hash_key] += 1
            self.z_map[hash_key][key1] = score1

        if ch:
            self.log(curr_time, 'ZADD', hash_key, str(score), key, *args)
            return str(self.changed[hash_key])
        else:
            self.log(curr_time, 'ZADD', hash_key, str(score), key, *args)
            return str(len(self.hash_table[hash_key]))

    def ZRANK(self, curr_time, hash_key, value):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table and value in self.z_map[hash_key]:
            x = self.hash_table[hash_key]
            if type(x) == SortedSet:
                index = self.search_index_util(x, self.z_map[hash_key][value])
                return str(index)
            else:
                raise InvalidDataType('WrongDataType')
        else:
            return str(None)

    def ZREVRANK(self, curr_time, hash_key, value):
        rank = self.ZRANK(curr_time, hash_key, value)
        if rank == 'None':
            return str(None)
        else:
            rank = int(rank)
            rev_rank = len(hash_key[value]) - rank - 1
            return str(rev_rank)


    def ZRANGE(self, curr_time, hash_key, start_index, end_index, *args):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table:
            ss = self.hash_table[hash_key]
            if type(ss) == SortedSet:
                if int(start_index) < 0:
                    temp = abs(int(start_index))
                    start_index = len(ss) - temp
                if int(end_index) < 0:
                    temp = abs(int(end_index))
                    end_index = len(ss) - temp
                if int(start_index) <= int(end_index) <= len(ss) - 1:
                    ans = []
                    s = ''
                    if len(args) == 1 and args[0].lower() == 'withscores':
                        for i in range(int(start_index), int(end_index) + 1):
                            ans.append(' '.join(map(str, ss[i])))
                            # for a, b in ans:
                            #     s += str(a) + " " + str(b) + ", "
                        s = ', '.join(ans)
                    elif len(args) == 0:
                        for i in range(int(start_index), int(end_index) + 1):
                            ans.append(ss[i][1])
                            # for a in ans:
                            #     s += str(a) + ", "
                        s = ', '.join(ans)
                    else:
                        raise SyntaxError('SyntaxError')
                    return s
                else:
                    raise InvalidFormat('WrongFormat')
            else:
                raise InvalidDataType('WrongDataType')
        else:
            return str(None)

    def ZREVRANGE(self, curr_time, hash_key, start_index, end_index, *args):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table:
            ss = self.hash_table[hash_key]
            if type(ss) == SortedSet:
                if int(start_index) < 0:
                    temp = abs(int(start_index))
                    start_index = len(ss) - temp
                if int(end_index) < 0:
                    temp = abs(int(end_index))
                    end_index = len(ss) - temp
                if int(start_index) <= int(end_index) <= len(ss) - 1:
                    ans = []
                    s = ''
                    n = len(ss)
                    start, stop = n - int(start_index) - 1, n - int(end_index) - 1
                    if len(args) == 1 and args[0].lower() == 'withscores':

                        for i in range(start, stop - 1, -1):
                            ans.append(' '.join(map(str, ss[i])))
                            # for a, b in ans:
                            #     s += str(a) + " " + str(b) + ", "
                        s = ', '.join(ans)
                    elif len(args) == 0:
                        for i in range(start, stop - 1, -1):
                            ans.append(ss[i][1])
                            # for a in ans:
                            #     s += str(a) + ", "
                        s = ', '.join(ans)
                    else:
                        raise SyntaxError('SyntaxError')
                    return s
                else:
                    raise InvalidFormat('WrongFormat')
            else:
                raise InvalidDataType('WrongDataType')
        else:
            return str(None)

import time
from sortedcontainers import SortedSet
from .exception import *
import traceback
import random

class Redis:
    def __init__(self):
        self.hash_table = {}
        self.expire = {}
        self.exp_queue = SortedSet()
        self.changed = {}
        self.z_map = {}

    def __repr__(self):
        return f'curr_time : {time.time()}\n\n' \
               f'hash_table :\n{self.hash_table}\n\n' \
               f'expire :\n{self.expire}\n\n' \
               f'changed :\n{self.changed}\n\n' \
               f'z_map :\n{self.z_map}'

    def log(self, curr_time, func, *args):
        command = [str(curr_time), func]
        for i in args:
            command.append(i)
        command = ' '.join(command)
        command = command.strip(' ')
        command += '\n'
        with open("log.txt", "a") as logfile:
            logfile.write(command)

    def search_util(self, item):
        low, high = 0, len(self.exp_queue)
        while low <= high:
            mid = (low + high) // 2
            if self.exp_queue[mid][0] == item:
                self.exp_queue.pop(mid)
                return True
            elif self.exp_queue[mid][0] < item[0]:
                low = mid + 1
            else:
                high = mid - 1
        return False

    def search_index_util(self, sorted_set, item):
        low, high = 0, len(sorted_set)-1
        while low <= high:
            mid = (low + high) // 2
            if sorted_set[mid][0] == item:
                return mid
            elif sorted_set[mid][0] < item:
                low = mid + 1
            else:
                high = mid - 1
        # return str(None)
        return 'Index Not Found'

    def destroy_util(self, curr_time, hash_key):
        """
        :param param:
        :return:
        """
        if hash_key in self.hash_table and self.expire[hash_key] is not None and self.expire[hash_key] <= round(time.time(), 4):
            self.DELETE(curr_time, hash_key)
            return True
        else:
            return False

    def auto_clean_util(self, curr_time):
        count = 0
        while count < 10 and len(self.hash_table) > 100:
            sample = random.sample(self.hash_table.keys(), 10)
            for key in sample:
                self.destroy_util(curr_time, key)
            count += 1


    def START(self):
        pass

    def GET(self, curr_time, hash_key):
        """
        :param param:
        :return:
        """
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table:
            if isinstance(self.hash_table[hash_key], str):
                # Element Found
                return self.hash_table[hash_key]
            else:
                # Throw Error -> Element found but not string
                raise NoStringValueError('NoStringValueError')
        else:
            # Element not found
            return str(None)

    def EXPIRE(self, curr_time, hash_key, seconds):
        """
        :param hash_key:
        :param seconds:
        :return:
        """
        if hash_key in self.hash_table:
            exp_time = round(curr_time, 4) + float(seconds)
            self.expire[hash_key] = exp_time
            if self.expire[hash_key] is None:
                self.exp_queue.add((exp_time, hash_key))
            else:
                # old_exp = self.expire[hash_key]
                # self.search_util((old_exp, hash_key))
                self.exp_queue.add((exp_time, hash_key))
            self.log(curr_time, "EXPIRE", hash_key, seconds)
            return '1'
        else:
            return '0'

    def TTL(self, curr_time, hash_key):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table and self.expire[hash_key] is not None:
            return str(time.time() - self.expire[hash_key])
        else:
            return str(None)

    def ping(self):
        return 'PONG'

    def DELETE(self, curr_time, hash_key):
        """
        :param param:
        :return:
        """

        if hash_key in self.hash_table:
            del self.hash_table[hash_key]
            del self.expire[hash_key]
            if hash_key in self.changed:
                del self.changed[hash_key]
            if hash_key in self.z_map:
                del self.z_map[hash_key]
            self.log(curr_time, "DELETE", hash_key)

            return '1'
        else:
            return '0'


    def SET(self, curr_time, hash_key, hash_val, *args):
        self.destroy_util(curr_time, hash_key)
        x = hash_key in self.hash_table
        ex, px, xx, nx, keepttl = [False] * 5
        kwargs = {}
        for i in range(len(args)):
            if args[i].lower() == 'ex':
                ex = True
                try:
                    kwargs['EX'] = int(args[i + 1])
                except:
                    raise InvalidRequest('InvalidRequest')
            elif args[i].lower() == 'px':
                px = True
                try:
                    kwargs['PX'] = int(args[i + 1])
                except:
                    raise InvalidRequest('InvalidRequest')
            elif args[i].lower() == 'xx':
                xx = True
            elif args[i].lower() == 'nx':
                nx = True
            elif args[i].lower() == 'keepttl':
                keepttl = True

        if (x is False and xx) or (x is True and nx):
            return str(None)
        # Make entry in both expiry table and hash table
        if (ex and px) or (nx and xx):
            raise InvalidRequest('InvalidRequest')
        if keepttl:
            if ex or nx:
                raise InvalidRequest('InvalidRequest')
            else:
                if hash_key in self.hash_table and self.expire[hash_key] is not None:
                    self.hash_table[hash_key] = str(hash_val)
                else:
                    self.hash_table[hash_key] = str(hash_val)
                    self.expire[hash_key] = None
                self.log(curr_time, 'SET', hash_key, hash_val, *args)
                return 'OK'
        else:
            self.hash_table[hash_key] = str(hash_val)
            old_exp = None
            if hash_key in self.expire:
                old_exp = self.expire[hash_key]
            self.expire[hash_key] = None
            if old_exp is not None:
                self.search_util((old_exp, hash_key))
            if ex:
                exp_time = round(curr_time, 4) + kwargs['EX']
                self.expire[hash_key] = exp_time
                # Remove then Add in Queue
                self.exp_queue.add((exp_time, hash_key))
            elif px:
                seconds = round((kwargs['PX'] / 1000), 4)
                exp_time = round(curr_time, 4) + seconds
                self.expire[hash_key] = exp_time
                # Remove and Add in Queue
                self.exp_queue.add((exp_time, hash_key))
            self.log(curr_time, 'SET', hash_key, hash_val, *args)
            return 'OK'

    def ZINCRBY(self, hash_key, score, key):
        # if hash_key in self.hash_table and type(self.hash_table[hash_key]) == SortedSet:
        pass



    def ZADD(self, curr_time, hash_key, score, key, *args):
        self.destroy_util(curr_time, hash_key)
        nx, xx, ch, incr, index = False, False, False, False, len(args)
        ind = 0
        for arg in args:
            if arg.lower() == 'nx':
                nx = True
                index = min(ind, index)
            elif arg.lower() == 'xx':
                xx = True
                index = min(ind, index)
            elif arg.lower() == 'ch':
                ch = True
                index = min(ind, index)
            elif arg.lower() == 'incr':
                incr = True
                index = min(ind, index)
            ind += 1
        keyword = ['NX', 'XX', 'CH', 'INCR']
        if index % 2 != 0 or (nx and xx):
            raise SyntaxError('SyntaxError')
        pairs = [(int(score), key)]
        # self.z_map[hash_key] = {key: int(score)}
        for i in range(0, index, 2):
            score1 = args[i]
            key1 = args[i + 1]
            pairs.append((int(score1), key1))
        if incr and len(pairs) > 0:
            raise SyntaxError('SyntaxError')

        # handle INCR Case
        # if incr:

        x = hash_key in self.hash_table
        if (x is False and xx) or (x is True and nx):
            if ch:
                if x:
                    return str(self.changed[hash_key])
                else:
                    return '0'
            else:
                if x:
                    return str(len(self.hash_table[hash_key]))
                else:
                    return '0'

        if hash_key not in self.hash_table:
            self.hash_table[hash_key] = SortedSet()
            self.changed[hash_key] = 0
            self.expire[hash_key] = None
            self.z_map[hash_key] = {}
        elif hash_key in self.hash_table and type(self.hash_table[hash_key]) != SortedSet:
            raise InvalidDataType('InvalidDataType')
        for score1, key1 in pairs:
            self.hash_table[hash_key].add((score1, key1))
            self.changed[hash_key] += 1
            self.z_map[hash_key][key1] = score1

        if ch:
            self.log(curr_time, 'ZADD', hash_key, str(score), key, *args)
            return str(self.changed[hash_key])
        else:
            self.log(curr_time, 'ZADD', hash_key, str(score), key, *args)
            return str(len(self.hash_table[hash_key]))

    def ZRANK(self, curr_time, hash_key, value):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table and value in self.z_map[hash_key]:
            x = self.hash_table[hash_key]
            if type(x) == SortedSet:
                index = self.search_index_util(x, self.z_map[hash_key][value])
                return str(index)
            else:
                raise InvalidDataType('WrongDataType')
        else:
            return str(None)

    def ZREVRANK(self, curr_time, hash_key, value):
        rank = self.ZRANK(curr_time, hash_key, value)
        if rank == 'None':
            return str(None)
        else:
            rank = int(rank)
            rev_rank = len(hash_key[value]) - rank - 1
            return str(rev_rank)


    def ZRANGE(self, curr_time, hash_key, start_index, end_index, *args):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table:
            ss = self.hash_table[hash_key]
            if type(ss) == SortedSet:
                if int(start_index) < 0:
                    temp = abs(int(start_index))
                    start_index = len(ss) - temp
                if int(end_index) < 0:
                    temp = abs(int(end_index))
                    end_index = len(ss) - temp
                if int(start_index) <= int(end_index) <= len(ss) - 1:
                    ans = []
                    s = ''
                    if len(args) == 1 and args[0].lower() == 'withscores':
                        for i in range(int(start_index), int(end_index) + 1):
                            ans.append(' '.join(map(str, ss[i])))
                            # for a, b in ans:
                            #     s += str(a) + " " + str(b) + ", "
                        s = ', '.join(ans)
                    elif len(args) == 0:
                        for i in range(int(start_index), int(end_index) + 1):
                            ans.append(ss[i][1])
                            # for a in ans:
                            #     s += str(a) + ", "
                        s = ', '.join(ans)
                    else:
                        raise SyntaxError('SyntaxError')
                    return s
                else:
                    raise InvalidFormat('WrongFormat')
            else:
                raise InvalidDataType('WrongDataType')
        else:
            return str(None)

    def ZREVRANGE(self, curr_time, hash_key, start_index, end_index, *args):
        self.destroy_util(curr_time, hash_key)
        if hash_key in self.hash_table:
            ss = self.hash_table[hash_key]
            if type(ss) == SortedSet:
                if int(start_index) < 0:
                    temp = abs(int(start_index))
                    start_index = len(ss) - temp
                if int(end_index) < 0:
                    temp = abs(int(end_index))
                    end_index = len(ss) - temp
                if int(start_index) <= int(end_index) <= len(ss) - 1:
                    ans = []
                    s = ''
                    n = len(ss)
                    start, stop = n - int(start_index) - 1, n - int(end_index) - 1
                    if len(args) == 1 and args[0].lower() == 'withscores':

                        for i in range(start, stop - 1, -1):
                            ans.append(' '.join(map(str, ss[i])))
                            # for a, b in ans:
                            #     s += str(a) + " " + str(b) + ", "
                        s = ', '.join(ans)
                    elif len(args) == 0:
                        for i in range(start, stop - 1, -1):
                            ans.append(ss[i][1])
                            # for a in ans:
                            #     s += str(a) + ", "
                        s = ', '.join(ans)
                    else:
                        raise SyntaxError('SyntaxError')
                    return s
                else:
                    raise InvalidFormat('WrongFormat')
            else:
                raise InvalidDataType('WrongDataType')
        else:
            return str(None)
# return str(None) :param param:         :return: :param param:         :return: # Element Found # Throw Error -> Element found but not string # Element not found :param hash_key:         :param seconds:         :return: # old_exp = self.expire[hash_key] # self.search_util((old_exp, hash_key)) :param param:         :return: # Make entry in both expiry table and hash table # Remove then Add in Queue # Remove and Add in Queue # if hash_key in self.hash_table and type(self.hash_table[hash_key]) == SortedSet: # self.z_map[hash_key] = {key: int(score)} # handle INCR Case # if incr: # for a, b in ans: #     s += str(a) + " " + str(b) + ", " # for a in ans: #     s += str(a) + ", " # for a, b in ans: #     s += str(a) + " " + str(b) + ", " # for a in ans: #     s += str(a) + ", "