import copy
import pickle

from collections import defaultdict
from functools import total_ordering
from uuid import uuid4

from .abstract_base_db import AbstractBaseDB
from .exceptions import *
from .field import Field
from .utils import INDEXABLE_TYPES, dereference


@total_ordering
class MinType():

  def __le__(self, other):
    return True

  def __eq__(self, other):
    return (self is other)

_MIN = MinType()


class PickleKey():

  def __hash__(self):
    return 0

  def __eq__(self, other):
    return type(other) == PickleKey

_PICKLE_KEY = PickleKey()


def _is_match(field, data):
  value = dereference(data, [ _encode_key(x) for x in field._keys ])
  for operator, operand in field._filter.items():
    try:
      if (operator == 'in') and (not value in operand):
        return False
      operand = _encode(operand)
      if (operator == '<') and (not value < operand):
        return False
      if (operator == '<=') and (not value <= operand):
        return False
      if (operator == '>') and (not value > operand):
        return False
      if (operator == '>=') and (not value >= operand):
        return False
      if (operator == '==') and (not value == operand):
        return False
      if (operator == '!=') and (not value != operand):
        return False
    except TypeError:
      return False
  return True


def _check_duplicate_operators(conditions):
  key_operators = defaultdict(set)
  for condition in conditions:
    if not condition._filter:
      continue
    for filter_key in condition._filter.keys():
      if filter_key in key_operators[condition._keys]:
        raise FieldFilterException(f"Duplicate '{filter_key}' operators")
      else:
        key_operators[condition._keys].add(filter_key)


def _encode_key(key):
  if type(key) in INDEXABLE_TYPES:
    return key
  return (_PICKLE_KEY, pickle.dumps(key))


def _encode(data):
  if data is None:
    return None
  if type(data) is dict:
    return { _encode_key(key): _encode(value) for key, value in data.items() }
  if type(data) in INDEXABLE_TYPES:
    return data
  return { _PICKLE_KEY: pickle.dumps(data) }


def _decode_key(key):
  if type(key) is tuple:
    return pickle.loads(key[1])
  return key


def _decode(data):
  if type(data) is dict:
    if (len(data) == 1) and (_PICKLE_KEY in data):
      return pickle.loads(data[_PICKLE_KEY])
    else:
      return { _decode_key(key): _decode(value) for key, value in data.items() }
  return copy.deepcopy(data)


class DB(AbstractBaseDB):


  def __init__(self):
    self._data = defaultdict(dict)


  def list_data_types(self):
    return list(self._data.keys())


  def create(self, data_type, data, *unique_fields):
    encoded = _encode(data)
    for field in unique_fields:
      new_value = dereference(encoded, [ _decode_key(x) for x in field._keys ])
      for existing_value in self._data[data_type].values():
        if new_value == dereference(existing_value, [ _decode_key(x) for x in field._keys ]):
          raise DuplicateKeyException()
    data_id = uuid4()
    self._data[data_type][data_id] = encoded
    return data_id


  def update(self, data_type, data_id, data, *conditions):
    try:
      existing = self._data[data_type][data_id]
    except KeyError:
      raise NotFoundException()
    _check_duplicate_operators(conditions)
    for condition in conditions:
      if not _is_match(condition, existing):
        raise NotFoundException()
    self._data[data_type][data_id] = _encode(data)


  def get(self, data_type, *data_ids):
    if not data_ids:
      raise Exception('No data IDs provided')
    try:
      if len(data_ids) == 1:
        return _decode(self._data[data_type][ data_ids[0] ])
      else:
        return _decode([ self._data[data_type][x] for x in data_ids ])
    except KeyError:
      raise NotFoundException()


  def delete(self, data_type, *data_ids):
    for data_id in data_ids:
      del self._data[data_type][data_id]


  def iter(self, data_type, *conditions, reverse=False, limit=None):
    to_sort = []
    for data_id, data in self._data[data_type].items():
      _check_duplicate_operators(conditions)
      for condition in conditions:
        if not _is_match(condition, data):
          break
      else:
        to_sort.append((data_id, data))

    def _get_sort_key(data_pair):
      values = []
      for field in conditions:
        value = dereference(data_pair[1], [ _decode_key(x) for x in field._keys ])
        values.append(_MIN if value is None else value)
      return tuple(values)

    n = 0
    for data_id, data in sorted(to_sort, key=_get_sort_key, reverse=reverse):
      n += 1
      if (limit is not None) and (n > limit):
        return
      yield data_id, _decode(data)


  def count(self, data_type, *conditions, limit=None):
    return len(self.list(data_type, *conditions, limit=limit))
