import logging
import os
import pickle
import sys
import threading
import time
from typing import List

from Giveme5W1H.extractor.root import path
from Giveme5W1H.extractor.tools.util import bytes_2_human_readable


class KeyValueCache(object):
    def __init__(self, cache_path):
        """
        :param cache_path: path to cache, must be relative to the root.py file
        """

        self.log = logging.getLogger('GiveMe5W')
        # resolve path relative to the path file
        self._cache_path = path(cache_path)
        # ad a meaningful extension
        self._cache_path = self._cache_path + '.prickle'

        self._cache = {}

        if cache_path and os.path.isfile(self._cache_path) and os.path.getsize(self._cache_path) > 0:
            # reload cache object form disc, if any
            with open(self._cache_path, 'rb') as ff:
                self._cache = pickle.load(ff)
                self.log.debug('KeyValueCache: ' + self._cache_path + ' restored')
                self.log_stats()
        else:
            self._cache = {}
        self._lock = threading.Lock()

    def log_stats(self):
        # size is not considering child's
        self.log.info(self._cache_path + ' entries: ' + str(len(self._cache)) + ' size: ' + bytes_2_human_readable(
            sys.getsizeof(self._cache)))

    def persist(self):
        with open(self._cache_path, 'wb') as f:
            pickle.dump(self._cache, f, pickle.HIGHEST_PROTOCOL)

    def cache(self, key: str, value: object):
        """
        None values are considered as invalid results (ToughRequest) is producing none for exceptions
        set -1 if you want to store "No distance"
        :param key:
        :param value:
        :return:
        """
        self._lock.acquire()
        if value is not None:
            self._cache[key] = self._pack(value);
            self.log.debug(self._cache_path + ' CACHED: ' + str(key) + ': ' + str(value))
            self.persist()
        self._lock.release()

    def get(self, key):
        """
        Read cache entries
        :param key:
        :return:
        """
        self._lock.acquire()
        result = None
        value = self._cache.get(key)
        if value is not None:
            self.log.debug(self._cache_path + ' LOADED: ' + str(key) + ': ' + str(value))
            result = self._unpack(value)

        self._lock.release()
        return result

    def get_complex(self, list_of_keys: List[str]):
        """
        Read complex cache entries
        """
        return self.get(self._get_id(list_of_keys))

    def cache_complex(self, list_of_keys: List[str], value):
        """
        helper to cache multi (string)key values.
        They are sorted before concatenation, therefore an order is determined.
        """
        self.cache(self._get_id(list_of_keys), value)

    def _get_id(self, list_of_keys: List[str]):
        """
        sorts list_of_keys, concatenates with # for readability
        :param list_of_keys:
        :return:
        """
        sorted(list_of_keys)
        return "#".join(list_of_keys)

    def _pack(self, value):
        """
        cache tracks the age of an entry, may be helpful in the future
        :param value:
        :return:
        """
        return [value, str(time.time())]

    def _unpack(self, value):
        """
        removes the timestamp around the cached value, if any
        :param value:
        :return:
        """
        # there are some old entries without timestamp
        if isinstance(value, str) or isinstance(value, int):
            return value
        return value[0]

[((19, 19, 19, 48), 'logging.getLogger', 'logging.getLogger', ({(19, 37, 19, 47): '"""GiveMe5W"""'}, {}), "('GiveMe5W')", False, 'import logging\n'), ((21, 27, 21, 43), 'Giveme5W1H.extractor.root.path', 'path', ({(21, 32, 21, 42): 'cache_path'}, {}), '(cache_path)', False, 'from Giveme5W1H.extractor.root import path\n'), ((35, 21, 35, 37), 'threading.Lock', 'threading.Lock', ({}, {}), '()', False, 'import threading\n'), ((27, 26, 27, 58), 'os.path.isfile', 'os.path.isfile', ({(27, 41, 27, 57): 'self._cache_path'}, {}), '(self._cache_path)', False, 'import os\n'), ((44, 12, 44, 64), 'pickle.dump', 'pickle.dump', ({(44, 24, 44, 35): 'self._cache', (44, 37, 44, 38): 'f', (44, 40, 44, 63): 'pickle.HIGHEST_PROTOCOL'}, {}), '(self._cache, f, pickle.HIGHEST_PROTOCOL)', False, 'import pickle\n'), ((27, 63, 27, 96), 'os.path.getsize', 'os.path.getsize', ({(27, 79, 27, 95): 'self._cache_path'}, {}), '(self._cache_path)', False, 'import os\n'), ((30, 30, 30, 45), 'pickle.load', 'pickle.load', ({(30, 42, 30, 44): 'ff'}, {}), '(ff)', False, 'import pickle\n'), ((105, 27, 105, 38), 'time.time', 'time.time', ({}, {}), '()', False, 'import time\n'), ((40, 12, 40, 38), 'sys.getsizeof', 'sys.getsizeof', ({(40, 26, 40, 37): 'self._cache'}, {}), '(self._cache)', False, 'import sys\n')]