#
# Copyright (C) 2014-2024 S[&]T, The Netherlands.
#

from __future__ import absolute_import, division, print_function

from muninn._compat import string_types as basestring

import copy
import datetime
import re
import uuid

import muninn.geometry as geometry

from muninn.enum import Enum
from muninn.exceptions import *
from muninn.function import Prototype, FunctionTable
from muninn.schema import *
from muninn.visitor import Visitor


#
# Table of all supported operators and functions
#
type_map = {
    UUID: Boolean,
}
function_table = FunctionTable(type_map=type_map)
#
# Logical operators
#
function_table.add(Prototype("not", (Boolean,), Boolean))
function_table.add(Prototype("and", (Boolean, Boolean), Boolean))
function_table.add(Prototype("or", (Boolean, Boolean), Boolean))
#
# Membership operators
#
function_table.add(Prototype("in", (Integer, Sequence), Boolean))
function_table.add(Prototype("in", (Long, Sequence), Boolean))
function_table.add(Prototype("in", (Real, Sequence), Boolean))
function_table.add(Prototype("in", (Text, Sequence), Boolean))
function_table.add(Prototype("not in", (Integer, Sequence), Boolean))
function_table.add(Prototype("not in", (Long, Sequence), Boolean))
function_table.add(Prototype("not in", (Real, Sequence), Boolean))
function_table.add(Prototype("not in", (Text, Sequence), Boolean))
#
# Comparison operators
#
function_table.add(Prototype("==", (Long, Long), Boolean))
function_table.add(Prototype("==", (Long, Integer), Boolean))
function_table.add(Prototype("==", (Integer, Long), Boolean))
function_table.add(Prototype("==", (Integer, Integer), Boolean))
function_table.add(Prototype("==", (Real, Real), Boolean))
function_table.add(Prototype("==", (Real, Long), Boolean))
function_table.add(Prototype("==", (Long, Real), Boolean))
function_table.add(Prototype("==", (Real, Integer), Boolean))
function_table.add(Prototype("==", (Integer, Real), Boolean))
function_table.add(Prototype("==", (Boolean, Boolean), Boolean))
function_table.add(Prototype("==", (Text, Text), Boolean))
function_table.add(Prototype("==", (Timestamp, Timestamp), Boolean))
function_table.add(Prototype("==", (UUID, UUID), Boolean))

function_table.add(Prototype("!=", (Long, Long), Boolean))
function_table.add(Prototype("!=", (Long, Integer), Boolean))
function_table.add(Prototype("!=", (Integer, Long), Boolean))
function_table.add(Prototype("!=", (Integer, Integer), Boolean))
function_table.add(Prototype("!=", (Real, Real), Boolean))
function_table.add(Prototype("!=", (Real, Long), Boolean))
function_table.add(Prototype("!=", (Long, Real), Boolean))
function_table.add(Prototype("!=", (Real, Integer), Boolean))
function_table.add(Prototype("!=", (Integer, Real), Boolean))
function_table.add(Prototype("!=", (Boolean, Boolean), Boolean))
function_table.add(Prototype("!=", (Text, Text), Boolean))
function_table.add(Prototype("!=", (Timestamp, Timestamp), Boolean))
function_table.add(Prototype("!=", (UUID, UUID), Boolean))

function_table.add(Prototype("<", (Long, Long), Boolean))
function_table.add(Prototype("<", (Long, Integer), Boolean))
function_table.add(Prototype("<", (Integer, Long), Boolean))
function_table.add(Prototype("<", (Integer, Integer), Boolean))
function_table.add(Prototype("<", (Real, Real), Boolean))
function_table.add(Prototype("<", (Real, Long), Boolean))
function_table.add(Prototype("<", (Long, Real), Boolean))
function_table.add(Prototype("<", (Real, Integer), Boolean))
function_table.add(Prototype("<", (Integer, Real), Boolean))
function_table.add(Prototype("<", (Text, Text), Boolean))
function_table.add(Prototype("<", (Timestamp, Timestamp), Boolean))

function_table.add(Prototype(">", (Long, Long), Boolean))
function_table.add(Prototype(">", (Long, Integer), Boolean))
function_table.add(Prototype(">", (Integer, Long), Boolean))
function_table.add(Prototype(">", (Integer, Integer), Boolean))
function_table.add(Prototype(">", (Real, Real), Boolean))
function_table.add(Prototype(">", (Real, Long), Boolean))
function_table.add(Prototype(">", (Long, Real), Boolean))
function_table.add(Prototype(">", (Real, Integer), Boolean))
function_table.add(Prototype(">", (Integer, Real), Boolean))
function_table.add(Prototype(">", (Text, Text), Boolean))
function_table.add(Prototype(">", (Timestamp, Timestamp), Boolean))

function_table.add(Prototype("<=", (Long, Long), Boolean))
function_table.add(Prototype("<=", (Long, Integer), Boolean))
function_table.add(Prototype("<=", (Integer, Long), Boolean))
function_table.add(Prototype("<=", (Integer, Integer), Boolean))
function_table.add(Prototype("<=", (Real, Real), Boolean))
function_table.add(Prototype("<=", (Real, Long), Boolean))
function_table.add(Prototype("<=", (Long, Real), Boolean))
function_table.add(Prototype("<=", (Real, Integer), Boolean))
function_table.add(Prototype("<=", (Integer, Real), Boolean))
function_table.add(Prototype("<=", (Text, Text), Boolean))
function_table.add(Prototype("<=", (Timestamp, Timestamp), Boolean))

function_table.add(Prototype(">=", (Long, Long), Boolean))
function_table.add(Prototype(">=", (Long, Integer), Boolean))
function_table.add(Prototype(">=", (Integer, Long), Boolean))
function_table.add(Prototype(">=", (Integer, Integer), Boolean))
function_table.add(Prototype(">=", (Real, Real), Boolean))
function_table.add(Prototype(">=", (Real, Long), Boolean))
function_table.add(Prototype(">=", (Long, Real), Boolean))
function_table.add(Prototype(">=", (Real, Integer), Boolean))
function_table.add(Prototype(">=", (Integer, Real), Boolean))
function_table.add(Prototype(">=", (Text, Text), Boolean))
function_table.add(Prototype(">=", (Timestamp, Timestamp), Boolean))

function_table.add(Prototype("~=", (Text, Text), Boolean))

function_table.add(Prototype("+", (Long,), Long))
function_table.add(Prototype("+", (Integer,), Integer))
function_table.add(Prototype("+", (Real,), Real))

function_table.add(Prototype("-", (Long,), Long))
function_table.add(Prototype("-", (Integer,), Integer))
function_table.add(Prototype("-", (Real,), Real))

function_table.add(Prototype("+", (Long, Long), Long))
function_table.add(Prototype("+", (Long, Integer), Long))
function_table.add(Prototype("+", (Integer, Long), Long))
function_table.add(Prototype("+", (Integer, Integer), Integer))
function_table.add(Prototype("+", (Real, Real), Real))
function_table.add(Prototype("+", (Real, Long), Real))
function_table.add(Prototype("+", (Long, Real), Real))
function_table.add(Prototype("+", (Real, Integer), Real))
function_table.add(Prototype("+", (Integer, Real), Real))

function_table.add(Prototype("-", (Long, Long), Long))
function_table.add(Prototype("-", (Long, Integer), Long))
function_table.add(Prototype("-", (Integer, Long), Long))
function_table.add(Prototype("-", (Integer, Integer), Integer))
function_table.add(Prototype("-", (Real, Real), Real))
function_table.add(Prototype("-", (Real, Long), Real))
function_table.add(Prototype("-", (Long, Real), Real))
function_table.add(Prototype("-", (Real, Integer), Real))
function_table.add(Prototype("-", (Integer, Real), Real))

function_table.add(Prototype("*", (Long, Long), Long))
function_table.add(Prototype("*", (Long, Integer), Long))
function_table.add(Prototype("*", (Integer, Long), Long))
function_table.add(Prototype("*", (Integer, Integer), Integer))
function_table.add(Prototype("*", (Real, Real), Real))
function_table.add(Prototype("*", (Real, Long), Real))
function_table.add(Prototype("*", (Long, Real), Real))
function_table.add(Prototype("*", (Real, Integer), Real))
function_table.add(Prototype("*", (Integer, Real), Real))

function_table.add(Prototype("/", (Long, Long), Long))
function_table.add(Prototype("/", (Long, Integer), Long))
function_table.add(Prototype("/", (Integer, Long), Long))
function_table.add(Prototype("/", (Integer, Integer), Integer))
function_table.add(Prototype("/", (Real, Real), Real))
function_table.add(Prototype("/", (Real, Long), Real))
function_table.add(Prototype("/", (Long, Real), Real))
function_table.add(Prototype("/", (Real, Integer), Real))
function_table.add(Prototype("/", (Integer, Real), Real))

function_table.add(Prototype("-", (Timestamp, Timestamp), Real))

#
# Functions.
#
function_table.add(Prototype("covers", (Geometry, Geometry), Boolean))
function_table.add(Prototype("covers", (Timestamp, Timestamp, Timestamp, Timestamp), Boolean))
function_table.add(Prototype("distance", (Geometry, Geometry), Real))
function_table.add(Prototype("intersects", (Geometry, Geometry), Boolean))
function_table.add(Prototype("intersects", (Timestamp, Timestamp, Timestamp, Timestamp), Boolean))
function_table.add(Prototype("is_defined", (Long,), Boolean))
function_table.add(Prototype("is_defined", (Integer,), Boolean))
function_table.add(Prototype("is_defined", (Real,), Boolean))
function_table.add(Prototype("is_defined", (Boolean,), Boolean))
function_table.add(Prototype("is_defined", (Text,), Boolean))
function_table.add(Prototype("is_defined", (Namespace,), Boolean))
function_table.add(Prototype("is_defined", (Timestamp,), Boolean))
function_table.add(Prototype("is_defined", (UUID,), Boolean))
function_table.add(Prototype("is_defined", (Geometry,), Boolean))
function_table.add(Prototype("is_source_of", (UUID,), Boolean))
function_table.add(Prototype("is_source_of", (Boolean,), Boolean))
function_table.add(Prototype("is_derived_from", (UUID,), Boolean))
function_table.add(Prototype("is_derived_from", (Boolean,), Boolean))
function_table.add(Prototype("has_tag", (Text,), Boolean))
function_table.add(Prototype("now", (), Timestamp))


class TokenType(Enum):
    _items = ("TEXT", "UUID", "TIMESTAMP", "REAL", "INTEGER", "BOOLEAN", "NAME", "OPERATOR", "END")


class Token(object):
    def __init__(self, type_, value=None):
        self.type_ = type_
        self.value = value

    def __repr__(self):
        return "Token(type_ = TokenType.%s, value = %r)" % (TokenType.to_string(self.type_), self.value)


class TokenStream(object):
    _sub_patterns = \
        (
            r"""\"(?:[^\\"]|\\.)*\"""",                                      # Text literals
            r"""\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{0,6})?)?""",   # Timestamp literals
            r"""[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}""",  # UUID literals
            r"""\d+(?:\.\d*(?:[eE][+-]?\d+)?|[eE][+-]?\d+)""",               # Real literals
            r"""0x[0-9a-fA-F]+|0o[0-7]\d+|0b[0-1]+|\d+""",                   # Integer literals
            r"""<=|>=|==|!=|~=|not in|[*<>@()\[\],.+-/]""",                  # Operators and delimiters
            r"""[a-zA-Z]\w*""",                                              # Names (incl. true, false, in)
        )

    _pattern = r"""(?:%s)""" % ("|".join(["(%s)" % sub_pattern for sub_pattern in _sub_patterns]))
    _re_token = re.compile(_pattern)

    _re_datemin = re.compile(r"0000-00-00(?:T00:00:00(?:\.0{0,6})?)?$")
    _re_datemax = re.compile(r"9999-99-99(?:T99:99:99(?:\.9{0,6})?)?$")

    def __init__(self, text):
        self.text = text
        self.at_end = not self.text
        self.token_start_position, self.token_end_position = 0, 0
        self.next()

    def next(self):
        if self.at_end:
            raise Error("char %d: unexpected end of input" % (self.token_start_position + 1))

        self.token = self._next_token()
        return self.token

    def test(self, types, values=None):
        return False if not self._test_token_types(types) else (values is None or self._test_token_values(values))

    def accept(self, types, values=None):
        if not self.test(types, values):
            return False

        self.next()
        return True

    def expect(self, types, values=None):
        if not self.test(types, values):
            if self.token.type_ == TokenType.END:
                raise Error("char %d: unexpected end of input" % (self.token_start_position + 1))
            else:
                if self.token.value is None:
                    token_str = TokenType.to_string(self.token.type_)
                else:
                    token_str = "\"%s\"" % self.token.value

                expected_str = self._types_to_string(types) if values is None else self._values_to_string(values)
                raise Error("char %d: expected %s, got %s" % (self.token_start_position + 1, expected_str, token_str))

        token = self.token
        self.next()
        return token

    def _types_to_string(self, types):
        try:
            strings = [TokenType.to_string(t) for t in types]
        except TypeError:
            return TokenType.to_string(types)

        return "%s%s" % ("" if len(strings) == 1 else "one of: ", ", ".join(strings))

    def _values_to_string(self, values):
        if isinstance(values, basestring):
            return "\"%s\"" % values

        try:
            strings = ["\"%s\"" % value for value in values]
        except TypeError:
            return "\"%s\"" % values

        return "%s%s" % ("" if len(strings) == 1 else "one of: ", ", ".join(strings))

    def _test_token_types(self, types):
        try:
            return self.token.type_ in types
        except TypeError:
            return self.token.type_ == types

    def _test_token_values(self, values):
        if isinstance(values, basestring):
            return self.token.value == values

        try:
            return self.token.value in values
        except TypeError:
            return self.token.value == values

    def _next_token(self):
        self.token_start_position = self._skip_white_space(self.token_end_position)

        if self.token_start_position == len(self.text):
            self.at_end = True
            return Token(TokenType.END)

        match_object = self._re_token.match(self.text, self.token_start_position)
        if match_object is None:
            raise Error("char %d: syntax error: \"%s\"" % (self.token_start_position + 1,
                        self.text[self.token_start_position:]))

        self.token_start_position, self.token_end_position = match_object.span()
        text, timestamp, uuid_, real, integer, operator, name = match_object.groups()

        if text is not None:
            return Token(TokenType.TEXT, string_unescape(text[1:-1]))

        if timestamp is not None:
            return Token(TokenType.TIMESTAMP, self._parse_timestamp(timestamp))

        if uuid_ is not None:
            return Token(TokenType.UUID, uuid.UUID(uuid_))

        if real is not None:
            return Token(TokenType.REAL, float(real))

        if integer is not None:
            if integer.startswith('0x'):
                base = 16
            elif integer.startswith('0o'):
                base = 8
            elif integer.startswith('0b'):
                base = 2
            else:
                base = 10
            return Token(TokenType.INTEGER, int(integer, base))

        if operator is not None:
            return Token(TokenType.OPERATOR, operator)

        if name is not None:
            if name in ["true", "false"]:
                return Token(TokenType.BOOLEAN, name == "true")
            elif name == "in":
                return Token(TokenType.OPERATOR, name)
            else:
                return Token(TokenType.NAME, name)

        raise Error("char %d: syntax error: \"%s\"" % (self.token_start_position + 1, match_object.group()))

    def _skip_white_space(self, start):
        while start < len(self.text) and self.text[start].isspace():
            start += 1
        return start

    def _parse_timestamp(self, timestamp):
        if self._re_datemin.match(timestamp) is not None:
            return datetime.datetime.min

        if self._re_datemax.match(timestamp) is not None:
            return datetime.datetime.max

        for format_string in ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"):
            try:
                return datetime.datetime.strptime(timestamp, format_string)
            except ValueError:
                pass

        raise Error("char %d: invalid timestamp: \"%s\"" % (self.token_start_position + 1, timestamp))


class AbstractSyntaxTreeNode(object):
    pass


class Literal(AbstractSyntaxTreeNode):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return "(%s %s)" % (type(self).__name__, self.value)


class Name(AbstractSyntaxTreeNode):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return "(%s %s)" % (type(self).__name__, self.value)


class List(AbstractSyntaxTreeNode):
    def __init__(self, values):
        self.values = values

    def __str__(self):
        return "(%s %s)" % (type(self).__name__, self.value)


class ParameterReference(AbstractSyntaxTreeNode):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return "(%s %s)" % (type(self).__name__, self.name)


class FunctionCall(AbstractSyntaxTreeNode):
    def __init__(self, name, *args):
        self.name = name
        self.arguments = list(args)

    def __str__(self):
        if not self.arguments:
            return "(%s %s)" % (type(self).__name__, self.name)
        return "(%s %s %s)" % (type(self).__name__, self.name, " ".join(map(str, self.arguments)))


def parse_sequence(stream, parse_item_function, start='(', end=')'):
    stream.expect(TokenType.OPERATOR, start)
    if stream.accept(TokenType.OPERATOR, end):
        return []

    sequence = [parse_item_function(stream)]
    while stream.accept(TokenType.OPERATOR, ","):
        sequence.append(parse_item_function(stream))
    stream.expect(TokenType.OPERATOR, end)
    return sequence


def parse_geometry_sequence(stream, parse_item_function):
    if stream.accept(TokenType.NAME, "EMPTY"):
        return []

    stream.expect(TokenType.OPERATOR, "(")
    sequence = [parse_item_function(stream)]
    while stream.accept(TokenType.OPERATOR, ","):
        sequence.append(parse_item_function(stream))
    stream.expect(TokenType.OPERATOR, ")")
    return sequence


def parse_signed_coordinate(stream):
    if stream.accept(TokenType.OPERATOR, "-"):
        token = stream.expect((TokenType.INTEGER, TokenType.REAL))
        return -float(token.value)

    stream.accept(TokenType.OPERATOR, "+")
    token = stream.expect((TokenType.INTEGER, TokenType.REAL))
    return float(token.value)


def parse_point_raw(stream):
    return geometry.Point(parse_signed_coordinate(stream),
                          parse_signed_coordinate(stream))


def parse_point(stream):
    stream.expect(TokenType.OPERATOR, "(")
    point = parse_point_raw(stream)
    stream.expect(TokenType.OPERATOR, ")")
    return point


def parse_line_string(stream):
    return geometry.LineString(parse_geometry_sequence(stream, parse_point_raw))


def parse_linear_ring(stream):
    points = parse_geometry_sequence(stream, parse_point_raw)
    if len(points) == 0:
        return geometry.LinearRing()

    if len(points) < 4:
        raise Error("char %d: linear ring should be empty or should contain >= 4 points" % stream.token_start_position)

    if points[-1] != points[0]:
        raise Error("char %d: linear ring should be closed" % stream.token_start_position)

    return geometry.LinearRing(points[:-1])


def parse_polygon(stream):
    return geometry.Polygon(parse_geometry_sequence(stream, parse_linear_ring))


def parse_multi_point(stream):
    return geometry.MultiPoint(parse_geometry_sequence(stream, parse_point))


def parse_multi_line_string(stream):
    return geometry.MultiLineString(parse_geometry_sequence(stream, parse_line_string))


def parse_multi_polygon(stream):
    return geometry.MultiPolygon(parse_geometry_sequence(stream, parse_polygon))


def parse_atom(stream):
    # Sub-expression.
    if stream.accept(TokenType.OPERATOR, "("):
        sub_expression = parse_expression(stream)
        stream.expect(TokenType.OPERATOR, ")")
        return sub_expression

    # Parameter reference.
    if stream.accept(TokenType.OPERATOR, "@"):
        name_token = stream.expect(TokenType.NAME)
        return ParameterReference(name_token.value)

    # Geometry literal, function call, or name.
    if stream.test(TokenType.NAME):
        name_token = stream.expect(TokenType.NAME)

        # Geometry literals.
        if name_token.value == "POINT":
            return Literal(parse_point(stream))
        elif name_token.value == "LINESTRING":
            return Literal(parse_line_string(stream))
        elif name_token.value == "POLYGON":
            return Literal(parse_polygon(stream))
        elif name_token.value == "MULTIPOINT":
            return Literal(parse_multi_point(stream))
        elif name_token.value == "MULTILINESTRING":
            return Literal(parse_multi_line_string(stream))
        elif name_token.value == "MULTIPOLYGON":
            return Literal(parse_multi_polygon(stream))

        # Function call.
        if stream.test(TokenType.OPERATOR, "("):
            return FunctionCall(name_token.value, *parse_sequence(stream, parse_expression))

        # Name (possibly qualified).
        parts = [name_token.value]
        while stream.accept(TokenType.OPERATOR, "."):
            name_token = stream.expect(TokenType.NAME)
            parts.append(name_token.value)
        return Name(".".join(parts))

    if stream.test(TokenType.OPERATOR, "["):
        return List(parse_sequence(stream, parse_expression, "[", "]"))

    # Literal.
    token = stream.expect((TokenType.TEXT, TokenType.TIMESTAMP, TokenType.UUID, TokenType.REAL, TokenType.INTEGER,
                           TokenType.BOOLEAN))
    return Literal(token.value)


def parse_term(stream):
    if stream.test(TokenType.OPERATOR, ("+", "-")):
        operator_token = stream.expect(TokenType.OPERATOR, ("+", "-"))
        return FunctionCall(operator_token.value, parse_term(stream))
    return parse_atom(stream)


def parse_arithmetic_expression(stream):
    lhs = parse_term(stream)
    if stream.test(TokenType.OPERATOR, ("+", "-", "*", "/")):
        operator_token = stream.expect(TokenType.OPERATOR, ("+", "-", "*", "/"))
        return FunctionCall(operator_token.value, lhs, parse_arithmetic_expression(stream))
    return lhs


def parse_comparison(stream):
    lhs = parse_arithmetic_expression(stream)
    if stream.test(TokenType.OPERATOR, ("<", ">", "==", ">=", "<=", "!=", "~=", "in", "not in")):
        operator_token = stream.expect(TokenType.OPERATOR, ("<", ">", "==", ">=", "<=", "!=", "~=", "in", "not in"))
        return FunctionCall(operator_token.value, lhs, parse_comparison(stream))
    return lhs


def parse_not_expression(stream):
    if stream.accept(TokenType.NAME, "not"):
        return FunctionCall("not", parse_not_expression(stream))
    return parse_comparison(stream)


def parse_and_expression(stream):
    lhs = parse_not_expression(stream)
    if stream.accept(TokenType.NAME, "and"):
        return FunctionCall("and", lhs, parse_and_expression(stream))
    return lhs


def parse_or_expression(stream):
    lhs = parse_and_expression(stream)
    if stream.accept(TokenType.NAME, "or"):
        return FunctionCall("or", lhs, parse_or_expression(stream))
    return lhs


def parse_expression(stream):
    return parse_or_expression(stream)


def _literal_type(literal):
    if isinstance(literal, list):  # TODO use Sequence.validate..?
        return Sequence

    for type in (Text, Timestamp, UUID, Boolean, Integer, Long, Real, Geometry):
        try:
            type.validate(literal)
        except ValueError:
            pass
        else:
            return type

    raise Error("unable to determine type of literal value: %r" % literal)


class SemanticAnalysis(Visitor):
    def __init__(self, namespace_schemas, parameters, having=False):
        super(SemanticAnalysis, self).__init__()
        self._namespace_schemas = namespace_schemas
        self._parameters = parameters
        self._having = having

    def visit_Literal(self, visitable):
        visitable.type = _literal_type(visitable.value)

    def visit_Name(self, visitable):
        if self._having:
            item = Identifier(visitable.value, self._namespace_schemas)
            visitable.type = item.muninn_type
            visitable.value = item
            return

        split_name = visitable.value.split(".")

        # namespace/implicit core property
        if len(split_name) == 1:
            if split_name[0] in self._namespace_schemas:
                namespace = split_name[0]
                name = None
            else:
                namespace, name = "core", split_name[0]

        # namespace.property
        elif len(split_name) == 2:
            namespace, name = split_name

        else:
            raise Error("invalid property name: \"%s\"" % visitable.value)

        # check that namespace exists
        try:
            schema = self._namespace_schemas[namespace]
        except KeyError:
            raise Error("undefined namespace: \"%s\"" % namespace)

        # namespace
        if name is None:
            visitable.value = split_name[0]
            visitable.type = Namespace

        # namespace.property
        else:
            try:
                type_ = schema[name]
            except KeyError:
                if len(split_name) == 2:
                    raise Error("undefined property: \"%s\"" % visitable.value)
                else:
                    raise Error("undefined name: \"%s\"" % name)

            visitable.value = "%s.%s" % (namespace, name)
            visitable.type = type_

    def visit_List(self, visitable):  # TODO check same literal type
        values = []
        for value in visitable.values:
            if not isinstance(value, Literal):
                raise Error("list contains non-literal")
            values.append(value.value)
        visitable.value = values
        visitable.type = Sequence

    def visit_ParameterReference(self, visitable):
        try:
            value = self._parameters[visitable.name]
        except KeyError:
            raise Error("no value for parameter: \"%s\"" % visitable.name)

        visitable.value = value
        visitable.type = _literal_type(value)

    def visit_FunctionCall(self, visitable):
        # Resolve the type of the function arguments.
        for argument in visitable.arguments:
            self.visit(argument)

        prototype = Prototype(visitable.name, [argument.type for argument in visitable.arguments])

        try:
            prototypes = function_table.resolve(prototype)
        except KeyError:
            prototypes = []

        if not prototypes:
            raise Error("undefined function: \"%s\"" % prototype)

        if len(prototypes) > 1:
            raise InternalError("cannot uniquely resolve function: \"%s\"" % prototype)

        prototype = prototypes[0]
        visitable.prototype = prototype
        visitable.type = prototype.return_type

    def visit_AbstractSyntaxTreeNode(self, visitable):
        if not hasattr(visitable, "type"):
            raise InternalError("encountered abstract syntax tree node without type attribute: %s" %
                                type(visitable).__name__)

    def default(self, visitable):
        raise InternalError("unsupported abstract syntax tree node type: %s" % type(visitable).__name__)


def parse(text):
    stream = TokenStream(text)
    abstract_syntax_tree = parse_expression(stream)
    if not stream.test(TokenType.END):
        raise Error("char %d: extra characters after expression: \"%s\"" % (stream.token_start_position + 1,
                                                                            text[stream.token_start_position:]))
    return abstract_syntax_tree


def analyze(abstract_syntax_tree, namespace_schemas={}, parameters={}, having=False):
    annotated_syntax_tree = copy.deepcopy(abstract_syntax_tree)
    SemanticAnalysis(namespace_schemas, parameters, having=having).visit(annotated_syntax_tree)
    return annotated_syntax_tree


def parse_and_analyze(text, namespace_schemas={}, parameters={}, having=False):
    return analyze(parse(text), namespace_schemas, parameters, having)


def string_unescape(text):
    '''
    Unescape special characters in a string.
    Python2 and 3 compatible, uses the native string type.
    In python2, the same effect can also be achieved with `string.decode("string-escape")`
    '''
    text = str(text)  # make sure we are using the native string type
    regex = re.compile('\\\\(\\\\|[\'"abfnrtv])')
    translator = {
        '\\': '\\',
        "'": "'",
        '"': '"',
        'a': '\a',
        'b': '\b',
        'f': '\f',
        'n': '\n',
        'r': '\r',
        't': '\t',
        'v': '\v',
    }

    def _replace(m):
        c = m.group(1)
        return translator[c]

    result = regex.sub(_replace, text)
    return result


class Identifier(object):
    def __init__(self, canonical_identifier, namespace_schemas):
        self.canonical = canonical_identifier
        self.subscript = None

        if canonical_identifier == 'tag':
            # the rules to get the namespace database table name also apply to 'tag'
            self.namespace = canonical_identifier
            self.identifier = canonical_identifier
            self.muninn_type = Text

        elif canonical_identifier == 'count':
            self.namespace = None
            self.identifier = canonical_identifier
            self.muninn_type = Long

        elif not re.match(r'[\w]+\.[\w.]+', canonical_identifier):
            raise Error("cannot resolve identifier: %r" % canonical_identifier)

        else:
            segments = canonical_identifier.split('.')

            if len(segments) == 1:
                self.namespace = 'core'
                self.identifier = segments[0]

            elif len(segments) == 2:
                if segments[0] in namespace_schemas:
                    self.namespace, self.identifier = segments
                else:
                    self.namespace = 'core'
                    self.identifier, self.subscript = segments

            elif len(segments) == 3:
                self.namespace, self.identifier, self.subscript = segments

            else:
                raise Error("cannot resolve identifier: %r" % canonical_identifier)

            # check if namespace is valid
            if self.namespace not in namespace_schemas:
                raise Error("undefined namespace: \"%s\"" % self.namespace)

            # check if property name is valid
            if self.identifier not in namespace_schemas[self.namespace]:
                if self.property_name != 'core.validity_duration':
                    raise Error("no property: %r defined within namespace: %r" % (self.identifier, self.namespace))

            # note: not checking if subscript is valid; the list of possible subscripts varies depending on context
            if self.property_name == 'core.validity_duration':
                self.muninn_type = None
            else:
                self.muninn_type = namespace_schemas[self.namespace][self.identifier]

    @property
    def property_name(self):
        return '%s.%s' % (self.namespace, self.identifier)

    @property
    def resolve(self):
        if self.canonical in ('count', 'tag'):
            return self.canonical
        elif self.subscript is None:
            return '%s.%s' % (self.namespace, self.identifier)
        else:
            return '%s.%s.%s' % (self.namespace, self.identifier, self.subscript)
