# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License
# 2.0; you may not use this file except in compliance with the Elastic License
# 2.0.

import re
from string import Template

from eql.ast import BaseNode
from eql.errors import EqlCompileError
from eql.utils import is_number, is_string

__all__ = (
    "KqlNode",
    "Value",
    "Null",
    "Number",
    "Boolean",
    "List",
    "Expression",
    "String",
    "Wildcard",
    "NotValue",
    "OrValues",
    "AndValues",
    "AndExpr",
    "OrExpr",
    "NotExpr",
    "FieldComparison",
    "Field",
    "FieldRange",
    "NestedQuery",
    "Exists",
)


class KqlNode(BaseNode):
    def optimize(self, recursive=True):
        from .optimizer import Optimizer
        return Optimizer().walk(self)

    def _render(self):
        return BaseNode.render(self)

    def render(self, precedence=None, **kwargs):
        """Render an EQL node and add parentheses to support orders of operation."""
        rendered = self._render(**kwargs)
        if precedence is not None and self.precedence is not None and self.precedence > precedence:
            return '({})'.format(rendered)
        return rendered


class Value(KqlNode):
    __slots__ = "value",
    precedence = 1

    def __init__(self, value):
        self.value = value

    @classmethod
    def from_python(cls, value):
        if value is None:
            return Null()
        elif isinstance(value, bool):
            return Boolean(value)
        elif is_number(value):
            return Number(value)
        elif is_string(value):
            return String(value)
        else:
            raise EqlCompileError("Unknown type {} for value {}".format(type(value).__name__, value))


class Null(Value):
    def __init__(self, value=None):
        Value.__init__(self, None)

    def _render(self):
        return "null"


class Number(Value):
    def _render(self):
        return str(self.value)


class Boolean(Value):
    def _render(self):
        return 'true' if self.value else 'false'


class String(Value):
    unescapable = re.compile(r'^[^\\():<>"*{} \t\r\n]+$')
    escapes = {"\t": "\\t", "\r": "\\r", "\"": "\\\""}

    def _render(self):
        # pass through as-is since nothing needs to be escaped
        if self.unescapable.match(self.value) is not None:
            return str(self.value)

        regex = r"[{}]".format("".join(re.escape(s) for s in sorted(self.escapes)))
        return '"{}"'.format(re.sub(regex, lambda r: self.escapes[r.group()], self.value))


class Wildcard(Value):
    escapes = {"\t": "\\t", "\r": "\\r"}
    slash_escaped = r'''^\\():<>"*{} '''

    def _render(self):
        escaped = []
        for char in self.value:
            if char in self.slash_escaped:
                escaped.append("\\")
                escaped.append(char)
            elif char in self.escapes:
                escaped.append(self.escapes[char])
            else:
                escaped.append(char)
        return ''.join(escaped)


class List(KqlNode):
    __slots__ = "items",
    precedence = Value.precedence + 1
    operator = ""
    template = Template("$items")

    def __init__(self, items):
        self.items = items
        KqlNode.__init__(self)

    @property
    def delims(self):
        return {"items": " {} ".format(self.operator)}

    def __eq__(self, other):
        from .optimizer import Optimizer
        from functools import cmp_to_key
        if type(self) == type(other):
            a = list(self.items)
            b = list(other.items)
            a.sort(key=cmp_to_key(Optimizer.sort_key))
            b.sort(key=cmp_to_key(Optimizer.sort_key))
            return a == b

        return False


class NotValue(KqlNode):
    __slots__ = "value",
    template = Template("not $value")
    precedence = Value.precedence + 1

    def __init__(self, value):
        self.value = value
        KqlNode.__init__(self)


class AndValues(List):
    precedence = List.precedence + 1
    operator = "and"


class OrValues(List):
    precedence = AndValues.precedence + 1
    operator = "or"


class Field(KqlNode):
    __slots__ = "name",
    precedence = Value.precedence
    template = Template("$name")

    def __init__(self, name):
        self.name = name
        KqlNode.__init__(self)

    @property
    def path(self):
        return self.name.split(".")

    @classmethod
    def from_path(cls, path):
        dotted = ".".join(path)
        return cls(dotted)


class Expression(KqlNode):
    """Intermediate node for class hierarchy."""


class FieldRange(Expression, KqlNode):
    __slots__ = "field", "operator", "value",
    precedence = Field.precedence
    template = Template("$field $operator $value")

    def __init__(self, field, operator, value):
        self.field = field
        self.operator = operator
        self.value = value


class NestedQuery(Expression):
    __slots__ = "field", "expr",
    precedence = Field.precedence + 1
    template = Template("$field:{$expr}")

    def __init__(self, field, expr):
        self.field = field
        self.expr = expr


class FieldComparison(Expression):
    __slots__ = "field", "value",
    precedence = FieldRange.precedence
    template = Template("$field:$value")

    def __init__(self, field, value):
        self.field = field
        self.value = value


class Exists(KqlNode):
    __slots__ = tuple()
    precedence = FieldComparison.precedence
    template = Template("*")


class NotExpr(Expression):
    __slots__ = "expr",
    precedence = FieldComparison.precedence + 1
    template = Template("not $expr")

    def __init__(self, expr):
        self.expr = expr


class AndExpr(Expression, List):
    precedence = NotExpr.precedence + 1
    operator = "and"


class OrExpr(Expression, List):
    precedence = AndExpr.precedence + 1
    operator = "or"
