# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

import re
import operator
import datetime
from collections import defaultdict
from functools import reduce, partial


def sql_like(value, pattern, ignore_case=True):
    flag = re.IGNORECASE if ignore_case else 0

    escape = False
    chars = []
    for char in re.split(r'(\\|.)', pattern)[1::2]:
        if escape:
            if char in ('%', '_'):
                chars.append(char)
            else:
                chars.extend(['\\', char])
            escape = False
        elif char == '\\':
            escape = True
        elif char == '_':
            chars.append('.')
        elif char == '%':
            chars.append('.*')
        else:
            chars.append(re.escape(char))

    regexp = re.compile(''.join(chars), flag)
    return bool(regexp.fullmatch(value))


like = partial(sql_like, ignore_case=False)
ilike = partial(sql_like, ignore_case=True)


def in_(a, b):
    if isinstance(a, (list, tuple)):
        if isinstance(b, (list, tuple)):
            return any(operator.contains(b, x) for x in a)
        else:
            return operator.contains(a, b)
    else:
        return operator.contains(b, a)


OPERATORS = defaultdict(lambda: lambda a, b: True)
OPERATORS.update({
        '=': operator.eq,
        '>': operator.gt,
        '<': operator.lt,
        '<=': operator.le,
        '>=': operator.ge,
        '!=': operator.ne,
        'in': in_,
        'not in': lambda a, b: not in_(a, b),
        'ilike': ilike,
        'not ilike': lambda a, b: not ilike(a, b),
        'like': like,
        'not like': lambda a, b: not like(a, b),
        })


def locale_part(expression, field_name, locale_name='id'):
    if expression == field_name:
        return locale_name
    if '.' in expression:
        fieldname, local = expression.split('.', 1)
        return local
    return expression


def is_leaf(expression):
    return (isinstance(expression, (list, tuple))
        and len(expression) > 2
        and isinstance(expression[1], str))


def constrained_leaf(part, boolop=operator.and_):
    field, operand, value = part[:3]
    if operand == '=' and boolop == operator.and_:
        # We should consider that other domain inversion will set a correct
        # value to this field
        return True
    return False


def eval_leaf(part, context, boolop=operator.and_):
    field, operand, value = part[:3]
    if '.' in field:
        # In the case where the leaf concerns a m2o then having a value in the
        # evaluation context is deemed suffisant
        return bool(context.get(field.split('.')[0]))
    context_field = context.get(field)
    if (operand not in {'=', '!='}
            and (context_field is None or value is None)
            and not (operand in {'in', 'not in'}
                and context_field is None
                and (isinstance(value, (list, tuple)) and None in value))):
        return
    if isinstance(context_field, datetime.date) and not value:
        if isinstance(context_field, datetime.datetime):
            value = datetime.datetime.min
        else:
            value = datetime.date.min
    if isinstance(value, datetime.date) and not context_field:
        if isinstance(value, datetime.datetime):
            context_field = datetime.datetime.min
        else:
            context_field = datetime.date.min
    if isinstance(context_field, (list, tuple)) and value is None:
        value = type(context_field)()
    if (isinstance(context_field, str)
            and isinstance(value, (list, tuple))):
        try:
            value = '%s,%s' % tuple(value)
        except TypeError:
            pass
    elif (isinstance(context_field, (list, tuple))
            and isinstance(value, str)):
        try:
            context_field = '%s,%s' % tuple(context_field)
        except TypeError:
            pass
    elif ((isinstance(context_field, list) and isinstance(value, tuple))
            or (isinstance(context_field, tuple) and isinstance(value, list))):
        context_field = list(context_field)
        value = list(value)
    if (operand in ('=', '!=')
            and isinstance(context_field, (list, tuple))
            and isinstance(value, int)):
        operand = {
            '=': 'in',
            '!=': 'not in',
            }[operand]
    try:
        return OPERATORS[operand](context_field, value)
    except TypeError:
        return False


def inverse_leaf(domain):
    if domain in ('AND', 'OR'):
        return domain
    elif is_leaf(domain):
        if 'child_of' in domain[1] and '.' not in domain[0]:
            if len(domain) == 3:
                return domain
            else:
                return [domain[3]] + list(domain[1:])
        return domain
    else:
        return list(map(inverse_leaf, domain))


def filter_leaf(domain, field, model):
    if domain in ('AND', 'OR'):
        return domain
    elif is_leaf(domain):
        if domain[0].startswith(field) and len(domain) > 3:
            if domain[3] != model:
                return ('id', '=', None)
        return domain
    else:
        return [filter_leaf(d, field, model) for d in domain]


def prepare_reference_domain(domain, reference):
    "convert domain to replace reference fields by their local part"

    def value2reference(value):
        model, ref_id = None, None
        if isinstance(value, str) and ',' in value:
            model, ref_id = value.split(',', 1)
            if ref_id != '%':
                try:
                    ref_id = int(ref_id)
                except ValueError:
                    model, ref_id = None, value
        elif (isinstance(value, (list, tuple))
                and len(value) == 2
                and isinstance(value[0], str)
                and (isinstance(value[1], int) or value[1] == '%')):
            model, ref_id = value
        else:
            ref_id = value
        return model, ref_id

    if domain in ('AND', 'OR'):
        return domain
    elif is_leaf(domain):
        if domain[0] == reference:
            if domain[1] in {'=', '!='}:
                model, ref_id = value2reference(domain[2])
                if model is not None:
                    if ref_id == '%':
                        if domain[1] == '=':
                            return [reference + '.id', '!=', None, model]
                        else:
                            return [reference, 'not like', domain[2]]
                    return [reference + '.id', domain[1], ref_id, model]
            elif domain[1] in {'in', 'not in'}:
                model_values = {}
                for value in domain[2]:
                    model, ref_id = value2reference(value)
                    if model is None:
                        break
                    model_values.setdefault(model, []).append(ref_id)
                else:
                    new_domain = ['OR'] if domain[1] == 'in' else ['AND']
                    for model, ref_ids in model_values.items():
                        if '%' in ref_ids:
                            if domain[1] == 'in':
                                new_domain.append(
                                    [reference + '.id', '!=', None, model])
                            else:
                                new_domain.append(
                                    [reference, 'not like', model + ',%'])
                        else:
                            new_domain.append(
                                [reference + '.id', domain[1], ref_ids, model])
                    return new_domain
            return []
        return domain
    else:
        return [prepare_reference_domain(d, reference) for d in domain]


def extract_reference_models(domain, field_name):
    "returns the set of the models available for field_name"
    if domain in ('AND', 'OR'):
        return set()
    elif is_leaf(domain):
        local_part = domain[0].split('.', 1)[0]
        if local_part == field_name and len(domain) > 3:
            return {domain[3]}
        return set()
    else:
        return reduce(operator.or_,
            (extract_reference_models(d, field_name) for d in domain))


def eval_domain(domain, context, boolop=operator.and_):
    "compute domain boolean value according to the context"
    if is_leaf(domain):
        return eval_leaf(domain, context, boolop=boolop)
    elif not domain and boolop is operator.and_:
        return True
    elif not domain and boolop is operator.or_:
        return False
    elif domain[0] == 'AND':
        return eval_domain(domain[1:], context)
    elif domain[0] == 'OR':
        return eval_domain(domain[1:], context, operator.or_)
    else:
        return boolop(bool(eval_domain(domain[0], context)),
            bool(eval_domain(domain[1:], context, boolop)))


def localize_domain(domain, field_name=None, strip_target=False):
    "returns only locale part of domain. eg: langage.code -> code"
    if domain in ('AND', 'OR', True, False):
        return domain
    elif is_leaf(domain):
        if 'child_of' in domain[1]:
            if domain[0].count('.'):
                _, target_part = domain[0].split('.', 1)
                return [target_part] + list(domain[1:])
            if len(domain) == 3:
                return domain
            else:
                return [domain[3]] + list(domain[1:-1])
        locale_name = 'id'
        if isinstance(domain[2], str):
            locale_name = 'rec_name'
        n = 3 if strip_target else 4
        return [locale_part(domain[0], field_name, locale_name)] \
            + list(domain[1:n]) + list(domain[4:])
    else:
        return [localize_domain(part, field_name, strip_target)
            for part in domain]


def simplify(domain):
    "remove unused domain delimiter"
    if is_leaf(domain):
        return domain
    elif domain in ('OR', 'AND'):
        return domain
    elif domain in (['OR'], ['AND']):
        return []
    elif (isinstance(domain, list) and len(domain) == 1
            and not is_leaf(domain[0])):
        return simplify(domain[0])
    elif (isinstance(domain, list) and len(domain) == 2
            and domain[0] in ('AND', 'OR')):
        return [simplify(domain[1])]
    else:
        return [simplify(branch) for branch in domain]


def merge(domain, domoperator=None):
    if not domain or domain in ('AND', 'OR'):
        return []
    domain_type = 'OR' if domain[0] == 'OR' else 'AND'
    if is_leaf(domain):
        return [domain]
    elif domoperator is None:
        return [domain_type] + reduce(operator.add,
                [merge(e, domain_type) for e in domain])
    elif domain_type == domoperator:
        return reduce(operator.add, [merge(e, domain_type) for e in domain])
    else:
        # without setting the domoperator
        return [merge(domain)]


def concat(*domains, **kwargs):
    domoperator = kwargs.get('domoperator')
    result = []
    if domoperator:
        result.append(domoperator)
    for domain in domains:
        if domain:
            result.append(domain)
    return simplify(merge(result))


def unique_value(domain):
    "Return if unique, the field and the value"
    if (isinstance(domain, list)
            and len(domain) == 1):
        domain, = domain
        name = domain[0]
        value = domain[2]
        count = 0
        if len(domain) == 4 and name[-3:] == '.id':
            count = 1
            model = domain[3]
            value = [model, value]
        if name.count('.') == count and domain[1] == '=':
            return True, name, value
    return False, None, None


def parse(domain):
    if is_leaf(domain):
        return domain
    elif not domain:
        return And([])
    elif domain[0] == 'OR':
        return Or(domain[1:])
    else:
        return And(domain[1:] if domain[0] == 'AND' else domain)


def domain_inversion(domain, symbol, context=None):
    """compute an inversion of the domain eventually the context is used to
    simplify the expression"""
    if context is None:
        context = {}
    expression = parse(domain)
    if symbol not in expression.variables:
        return True
    return expression.inverse(symbol, context)


class And(object):

    def __init__(self, expressions):
        self.branches = list(map(parse, expressions))
        self.variables = set()
        for expression in self.branches:
            if is_leaf(expression):
                self.variables.add(self.base(expression[0]))
            elif isinstance(expression, And):
                self.variables |= expression.variables

    def base(self, expression):
        if '.' not in expression:
            return expression
        else:
            return expression.split('.')[0]

    def inverse(self, symbol, context):
        result = []
        for part in self.branches:
            if isinstance(part, And):
                part_inversion = part.inverse(symbol, context)
                evaluated = isinstance(part_inversion, bool)
                if symbol not in part.variables:
                    continue
                if not evaluated:
                    result.append(part_inversion)
                elif part_inversion:
                    continue
                else:
                    return False
            elif is_leaf(part) and self.base(part[0]) == symbol:
                result.append(part)
            else:
                field = part[0]
                if (field not in context
                        or field in context
                        and (eval_leaf(part, context, operator.and_)
                            or constrained_leaf(part, operator.and_))):
                    result.append(True)
                else:
                    return False

        result = [e for e in result if e is not True]
        if result == []:
            return True
        else:
            return simplify(result)


class Or(And):

    def inverse(self, symbol, context):
        result = []
        known_variables = set(context.keys())
        if not known_variables >= (self.variables - {symbol}):
            # In this case we don't know enough about this OR part, we
            # consider it to be True (because people will have the constraint
            # on this part later).
            return True
        for part in self.branches:
            if isinstance(part, And):
                part_inversion = part.inverse(symbol, context)
                evaluated = isinstance(part_inversion, bool)
                if symbol not in part.variables:
                    if evaluated and part_inversion:
                        return True
                    continue
                if not evaluated:
                    result.append(part_inversion)
                elif part_inversion:
                    return True
                else:
                    continue
            elif is_leaf(part) and self.base(part[0]) == symbol:
                result.append(part)
            else:
                field = part[0]
                field = self.base(field)
                if (field in context
                        and (eval_leaf(part, context, operator.or_)
                            or constrained_leaf(part, operator.or_))):
                    return True
                elif (field in context
                        and not eval_leaf(part, context, operator.or_)):
                    result.append(False)

        result = [e for e in result if e is not False]
        if result == []:
            return False
        else:
            return simplify(['OR'] + result)
