from __future__ import absolute_import, print_function, unicode_literals

import math
from .shared import string, to_str, fromNow, JSONTemplateError


class BuiltinError(JSONTemplateError):
    pass


def build():
    builtins = {}

    def builtin(
        name, variadic=None, argument_tests=None, minArgs=None, needs_context=False
    ):
        def wrap(fn):
            if variadic:

                def invoke(context, *args):
                    if minArgs:
                        if len(args) < minArgs:
                            raise BuiltinError(
                                "invalid arguments to builtin: {}: expected at least {} arguments".format(
                                    name, minArgs
                                )
                            )
                    for arg in args:
                        if not variadic(arg):
                            raise BuiltinError(
                                "invalid arguments to builtin: {}".format(name)
                            )
                    if needs_context is True:
                        return fn(context, *args)
                    return fn(*args)

            elif argument_tests:

                def invoke(context, *args):
                    if len(args) != len(argument_tests):
                        raise BuiltinError(
                            "invalid arguments to builtin: {}".format(name)
                        )
                    for t, arg in zip(argument_tests, args):
                        if not t(arg):
                            raise BuiltinError(
                                "invalid arguments to builtin: {}".format(name)
                            )
                    if needs_context is True:
                        return fn(context, *args)
                    return fn(*args)

            else:

                def invoke(context, *args):
                    if needs_context is True:
                        return fn(context, *args)
                    return fn(*args)

            invoke._jsone_builtin = True
            builtins[name] = invoke
            return fn

        return wrap

    def is_number(v):
        return isinstance(v, (int, float)) and not isinstance(v, bool)

    def is_int(v):
        return isinstance(v, int)

    def is_string(v):
        return isinstance(v, string)

    def is_string_or_number(v):
        return is_string(v) or is_number(v)

    def is_array(v):
        return isinstance(v, list)

    def is_string_or_array(v):
        return isinstance(v, (string, list))

    def anything_except_array(v):
        return isinstance(v, (string, int, float, bool)) or v is None

    def anything(v):
        return (
            isinstance(v, (string, int, float, list, dict)) or v is None or callable(v)
        )

    # ---

    builtin("min", variadic=is_number, minArgs=1)(min)
    builtin("max", variadic=is_number, minArgs=1)(max)
    builtin("sqrt", argument_tests=[is_number])(math.sqrt)
    builtin("abs", argument_tests=[is_number])(abs)

    @builtin("ceil", argument_tests=[is_number])
    def ceil(v):
        return int(math.ceil(v))

    @builtin("floor", argument_tests=[is_number])
    def floor(v):
        return int(math.floor(v))

    @builtin("range", minArgs=2)
    def range_builtin(start, stop, step=1):
        if step == 0 or not all([is_int(n) for n in [start, stop, step]]):
            raise BuiltinError("invalid arguments to builtin: range")

        return list(range(start, stop, step))

    @builtin("lowercase", argument_tests=[is_string])
    def lowercase(v):
        return v.lower()

    @builtin("uppercase", argument_tests=[is_string])
    def lowercase(v):
        return v.upper()

    builtin("len", argument_tests=[is_string_or_array])(len)
    builtin("str", argument_tests=[anything_except_array])(to_str)
    builtin("number", variadic=is_string, minArgs=1)(float)

    @builtin("strip", argument_tests=[is_string])
    def strip(s):
        return s.strip()

    @builtin("rstrip", argument_tests=[is_string])
    def rstrip(s):
        return s.rstrip()

    @builtin("lstrip", argument_tests=[is_string])
    def lstrip(s):
        return s.lstrip()

    @builtin("join", argument_tests=[is_array, is_string_or_number])
    def join(list, separator):
        # convert potential numbers into strings
        string_list = [str(int) for int in list]

        return str(separator).join(string_list)

    @builtin("split", argument_tests=[is_string, is_string_or_number], minArgs=2)
    def split(s, d=""):
        if not d and is_string(s):
            return list(s)

        return s.split(to_str(d))

    @builtin("fromNow", variadic=is_string, minArgs=1, needs_context=True)
    def fromNow_builtin(context, offset, reference=None):
        return fromNow(offset, reference or context.get("now"))

    @builtin("typeof", argument_tests=[anything])
    def typeof(v):
        if isinstance(v, bool):
            return "boolean"
        elif isinstance(v, string):
            return "string"
        elif isinstance(v, (int, float)):
            return "number"
        elif isinstance(v, list):
            return "array"
        elif isinstance(v, dict):
            return "object"
        elif v is None:
            return "null"
        elif callable(v):
            return "function"

    @builtin("defined", argument_tests=[is_string], needs_context=True)
    def defined(context, s):
        if s not in context:
            return False
        else:
            return True

    return builtins
