import math
from typing import List, Any

from ..cpl import Cpl, CplFloat, CplInt, CplNBT, CplFloatNBT, CplIntNBT, CplScore
from ..error import raise_syntax_error
from ..transpiler import TranspilerContext, add_lib, CustomCplObject
from ..utils import FLOAT_PREC, VariableDeclaration, get_uuid


def helper_float0_check(ctx: TranspilerContext, args: List[Cpl], token, method: str):
    if len(args) != 1:
        raise_syntax_error(f"Expected 1 argument for {method}()", token)
    n = args[0]
    if n.unique_type.type not in {"int", "float"}:
        raise_syntax_error(f"Expected an int or float argument for {method}()", token)


def helper_float0(ctx: TranspilerContext, args: List[Cpl], token, method: str,
                  score_loc=None) -> float | CplScore:
    helper_float0_check(ctx, args, token, method)
    n = args[0]
    if isinstance(n, CplInt) or isinstance(n, CplFloat):
        return float(n.value)
    return n.cache(ctx, score_loc=score_loc, force="score", force_t="float")


def lib_sqrt(ctx: TranspilerContext, args: List[Cpl], token):
    tr = ctx.transpiler
    helper = helper_float0(ctx, args, token, "Math.sqrt", "__sqrt__x __temp__")
    if isinstance(helper, float):
        return CplFloat(token, math.sqrt(helper))

    if "has_sqrt_init" not in tr.data:
        tr.data["has_sqrt_init"] = True
        tr.files["__load__"].append("scoreboard players set __sqrt__2 __temp__ 2")

    # y^2 - x = 0
    # y -= (y^2 - x)/(2 y)

    # y -= (y - x/y) / 2

    # z = y
    # w = x
    # w /= y
    # z -= w
    # z /= 2

    # y -= z

    last_y = CplScore(token, "__sqrt__last_output __temp__", "float")
    y = CplScore(token, "__sqrt__output __temp__", "float")
    z = CplScore(token, "__sqrt__z __temp__", "float")
    w = CplScore(token, "__sqrt__w __temp__", "float")

    # initial guess: y = x
    y._set(ctx, helper)

    if "__lib__/sqrt_loop" not in tr.files:
        fil = [f"scoreboard players operation {last_y.location} = {y.location}"]
        tr.files["__lib__/sqrt_loop"] = fil
        loop_ctx = ctx.clone_at("__lib__/sqrt_loop")
        z._set(loop_ctx, y)
        w._set(loop_ctx, helper)
        w._set_div(loop_ctx, y)
        z._set_sub(loop_ctx, w)
        z._set_div(loop_ctx, CplScore(token, "__sqrt__2 __temp__", "int"))
        y._set_sub(loop_ctx, z)
        fil.append(
            f"execute unless score {last_y.location} = {y.location} run function {tr.pack_namespace}:__lib__/sqrt_loop")

    ctx.file.append(f"function {tr.pack_namespace}:__lib__/sqrt_loop")

    return CplScore(token, "__sqrt__output __temp__", "float")


def lib_isqrt(ctx: TranspilerContext, args: List[Cpl], token):
    if len(args) != 1:
        raise_syntax_error("Expected 1 argument for Math.isqrt()", token)
    n = args[0]
    if n.unique_type.type != "int":
        raise_syntax_error("Expected an int argument for Math.isqrt()", token)
    if isinstance(n, CplInt):
        return CplInt(token, math.isqrt(n.value))
    n.cache(ctx, score_loc="__isqrt__x", force="score", force_t="int")

    tr = ctx.transpiler
    file = ctx.file
    if "has_sqrt_init" not in tr.data:
        tr.data["has_sqrt_init"] = True
        tr.files["__load__"].append("scoreboard players set __sqrt__2 __temp__ 2")
        tr.files["__load__"].append("scoreboard players set __sqrt__4 __temp__ 4")

    file.extend(
        [
            "scoreboard players operation __isqrt__x_4 __temp__ = __isqrt__x __temp__",
            "scoreboard players operation __isqrt__x_4 __temp__ /= __isqrt__4 __temp__",
            "scoreboard players operation __isqrt__output __temp__ = __isqrt__x_4 __temp__",
            f"function {tr.pack_namespace}:__lib__/__isqrt__loop",
        ]
    )

    tr.files["__lib__/isqrt_loop"] = [
        "scoreboard players operation __isqrt__last_output __temp__ = __isqrt__output __temp__",
        "scoreboard players operation __isqrt__output __temp__ /= __isqrt__2 __temp__",
        "scoreboard players operation __isqrt__x_t __temp__ =  __isqrt__x_4 __temp__",
        "scoreboard players operation __isqrt__x_t __temp__ /= __isqrt__output __temp__",
        "scoreboard players operation __isqrt__output __temp__ += __isqrt__x_t __temp__",
        "execute unless score __isqrt__last_output __temp__ = __isqrt__output __temp__ run function $PACK_NAME$:__lib__/__isqrt__loop",
    ]

    return CplScore(token, "__isqrt__output __temp__", "int")


def lib_cbrt(ctx: TranspilerContext, args: List[Cpl], token):
    tr = ctx.transpiler
    helper = helper_float0(ctx, args, token, "Math.cbrt", "__cbrt__x __temp__")
    if isinstance(helper, float):
        return CplFloat(token, helper ** (1 / 3))

    if "has_cbrt_init" not in tr.data:
        tr.data["has_cbrt_init"] = True
        tr.files["__load__"].append(
            f"scoreboard players set __cbrt__3d2 __temp__ {int(FLOAT_PREC * 3 / 2)}"
        )
        tr.files["__load__"].append(
            f"scoreboard players set __cbrt__4d3 __temp__ {int(FLOAT_PREC * 4 / 3)}"
        )

    ctx.file.extend(
        [
            "scoreboard players operation __cbrt__x __temp__ *= __cbrt__4d3 __temp__",
            "scoreboard players operation __cbrt__output __temp__ = __cbrt__x __temp__",
            f"function {tr.pack_namespace}:__lib__/__cbrt__loop",
        ]
    )

    tr.files["__lib__/cbrt_loop"] = [
        "scoreboard players operation __cbrt__last_output __temp__ = __cbrt__output __temp__",
        "scoreboard players operation __cbrt__output __temp__ *= __cbrt__3d2 __temp__",
        "scoreboard players operation __cbrt__output __temp__ /= FLOAT_PREC __temp__",
        "scoreboard players operation __cbrt__x_t __temp__ =  __cbrt__x __temp__",
        "scoreboard players operation __cbrt__x_t __temp__ *= FLOAT_PREC __temp__",
        "scoreboard players operation __cbrt__x_t __temp__ /= __cbrt__output __temp__",
        "scoreboard players operation __cbrt__x_t __temp__ *= FLOAT_PREC __temp__",
        "scoreboard players operation __cbrt__x_t __temp__ /= __cbrt__output __temp__",
        "scoreboard players operation __cbrt__output __temp__ += __cbrt__x_t __temp__",
        "execute unless score __cbrt__last_output __temp__ = __cbrt__output __temp__ run function $PACK_NAME$:__lib__/__cbrt__loop",
    ]

    return CplScore(token, "__cbrt__output __temp__", "float")


def lib_floor(ctx: TranspilerContext, args: List[Cpl], token):
    helper = helper_float0(ctx, args, token, "Math.floor")
    if isinstance(helper, float):
        return CplFloat(token, math.floor(helper))
    return helper


def lib_ceil(ctx: TranspilerContext, args: List[Cpl], token):
    helper = helper_float0(ctx, args, token, "Math.ceil", "__ceil__x __temp__")
    if isinstance(helper, float):
        return CplFloat(token, math.ceil(helper))
    n = args[0]
    if n.unique_type.type == "int":
        return n

    ctx.file.append(f"scoreboard players add __ceil__x __temp__ {int(FLOAT_PREC - 1)}")
    ctx.file.append(f"scoreboard players operation __ceil__x __temp__ /= FLOAT_PREC __temp__")
    return helper


def lib_round(ctx: TranspilerContext, args: List[Cpl], token):
    helper = helper_float0(ctx, args, token, "Math.round", "__round__x __temp__")
    if isinstance(helper, float):
        return CplFloat(token, round(helper))
    n = args[0]
    if n.unique_type.type == "int":
        return n

    ctx.file.append(f"scoreboard players add __round__x __temp__ {int(FLOAT_PREC / 2)}")
    ctx.file.append(f"scoreboard players operation __round__x __temp__ /= FLOAT_PREC __temp__")
    return helper


def _min_max_help(x, y, func_name):
    return x.value < y.value if func_name == "min" else x.value > y.value


def lib_min_max(
        ctx: TranspilerContext, arguments: List[Cpl], token, func_name
):
    if len(arguments) == 0:
        raise_syntax_error(f"Expected at least 1 argument for Math.{func_name}()", token)
    min_num: Any = None
    result_type = "int"
    non_literals = []
    for arg in arguments:
        if arg.unique_type.type not in {"int", "float"}:
            raise_syntax_error(f"Expected an int or float argument for Math.{func_name}()", token)
        if arg.unique_type.type == "float":
            result_type = "float"
        if isinstance(arg, CplInt) or isinstance(arg, CplFloat):
            if not min_num or _min_max_help(arg, min_num, func_name):
                min_num = arg
        else:
            non_literals.append(arg)

    result = min_num or CplInt(token, 0)
    for arg in non_literals:
        op = "<" if func_name == "min" else ">"
        if not isinstance(arg, CplScore):
            arg = arg.cache(ctx, force="score", force_t=result_type)
        if isinstance(result, CplInt) or isinstance(result, CplFloat):
            v = result.value if isinstance(result, CplInt) else int(result.value * FLOAT_PREC)
            if func_name == "min":
                cmp = f"..{v - 1}"
            else:
                cmp = f"{v + 1}.."
            ctx.file.append(f"execute if score {arg.location} matches {cmp} "
                            f"run scoreboard players set __min__output __temp__ {v}")
            result = CplScore(token, "__min__output __temp__", result_type)
        else:
            ctx.file.append(f"execute if score {arg.location} {op} {result.location} "
                            f"run scoreboard players set __min__output __temp__ {result.location}")
            result = CplScore(token, "__min__output __temp__", result_type)

    return result


def lib_min(ctx: TranspilerContext, arguments: List[Cpl], token):
    return lib_min_max(ctx, arguments, token, "min")


def lib_max(ctx: TranspilerContext, arguments: List[Cpl], token):
    return lib_min_max(ctx, arguments, token, "max")


def lib_random(ctx: TranspilerContext, args: List[Cpl], token):
    tr = ctx.transpiler
    min_ = (args and args[0]) or CplInt(token, 0)
    max_ = args[1] if len(args) > 1 else CplInt(token, 2147483647)
    if min_.unique_type.type != "int" or max_.unique_type.type != "int":
        raise_syntax_error("Expected an int argument for Math.random()", token)
    if tr.pack_format >= 18:
        # Just use /random command that got added in 1.20.2
        if isinstance(min_, CplInt) and isinstance(max_, CplInt):
            eid = f"int_{get_uuid()} __temp__"
            ctx.file.append(f"execute store result score {eid} run random value {min_.value}..{max_.value}")
            return CplScore(token, eid)

    if isinstance(min_, CplNBT):
        min_ = min_.cache(ctx, force="score")
    if isinstance(max_, CplNBT):
        max_ = max_.cache(ctx, force="score")
    if "random_setup" not in tr.data:
        tr.files["__math__/random_setup"] = [
            "summon area_effect_cloud ~ ~ ~ {Tags:['__math__.random']}",
            "execute store result score __random__.seed __temp__ run data get entity @e[limit=1,type=area_effect_cloud,tag=__math__.random] UUID[0] 1",
            "kill @e[type=area_effect_cloud,tag=__math__.random]",
            "scoreboard players set __random__.rng.a __temp__ 656891",
            "scoreboard players set __random__.rng.c __temp__ 875773"
        ]
        tr.files["__math__/random"] = [
            "scoreboard players operation __random__.seed __temp__ *= __random__.rng.a __temp__",
            "scoreboard players operation __random__.seed __temp__ += __random__.rng.c __temp__",
            "scoreboard players operation __random__.rng.result __temp__ = __random__.seed __temp__",
            "scoreboard players operation __random__.tmp __temp__ = __random__.rng.result __temp__",
            "scoreboard players operation __random__.rng.result __temp__ %= __random__.rng.bound __temp__",
            "scoreboard players operation __random__.tmp __temp__ -= __random__.rng.result __temp__",
            "scoreboard players operation __random__.tmp __temp__ += __random__.rng.bound __temp__",
            f"execute if score __random__.tmp __temp__ matches ..0 run function {tr.pack_namespace}:__math__/random"
        ]

    if isinstance(min_, CplInt) and isinstance(max_, CplInt):
        ctx.file.append(f"scoreboard players set __random__.rng.bound __temp__ {max_.value - min_.value + 1}")
    elif isinstance(min_, CplScore) and isinstance(max_, CplInt):
        ctx.file.append(f"scoreboard players set __random__.rng.bound __temp__ {max_.value + 1}")
        ctx.file.append(f"scoreboard players operation __random__.rng.bound __temp__ -= {min_.location}")
    elif isinstance(min_, CplInt) and isinstance(max_, CplScore):
        ctx.file.append(f"scoreboard players set __random__.rng.bound __temp__ {-min_.value + 1}")
        ctx.file.append(f"scoreboard players operation __random__.rng.bound __temp__ += {max_.location}")
    else:
        ctx.file.append(f"scoreboard players operation __random__.rng.bound __temp__ = {max_.location}")
        ctx.file.append(f"scoreboard players operation __random__.rng.bound __temp__ -= {min_.location}")
        ctx.file.append(f"scoreboard players add __random__.rng.bound __temp__ 1")
    ctx.file.append(f"function {tr.pack_namespace}:__math__/random")

    return CplScore(token, "__random__.rng.result __temp__", "int")._set_add(ctx, min_)


def pseudo_int(ctx, cpl):
    if isinstance(cpl, CplFloat):
        return CplInt(cpl.token, int(cpl.value * FLOAT_PREC))
    if isinstance(cpl, CplScore):
        return CplScore(cpl.token, cpl.location, "int")
    if isinstance(cpl, CplFloatNBT):
        cached = cpl.cache(ctx, force="score")
        return CplScore(cpl.token, cached.location, "int")
    raise ValueError("Unexpected input")


def lib_frandom(ctx: TranspilerContext, args: List[Cpl], token):
    if len(args) != 2:
        raise_syntax_error("Expected 2 arguments for Math.frandom()", token)
    if args[0].unique_type.type != "float" or args[1].unique_type.type != "float":
        raise_syntax_error("Expected a float argument for Math.frandom()", token)
    score = lib_random(ctx, list(map(lambda arg: pseudo_int(ctx, arg), args)), token)
    return CplScore(token, score.location, "float")


def lib_ipow(ctx: TranspilerContext, args: List[Cpl], token):
    tr = ctx.transpiler
    if len(args) != 2:
        raise_syntax_error(f"Expected 2 arguments for Math.ipow()", token)
    a0 = args[0]
    a1 = args[1]
    if a0.unique_type.type not in {"int", "float"}:
        raise_syntax_error(f"Expected an int or float for the first argument of Math.ipow()", token)
    if a1.unique_type.type != "int":
        raise_syntax_error(f"Expected an int for the second argument of Math.ipow()", token)
    if isinstance(a0, CplNBT):
        a0 = a0.cache(ctx, force="score")
    if isinstance(a1, CplIntNBT):
        a1 = a1.cache(ctx, force="score")
    if (isinstance(a0, CplInt) or isinstance(a0, CplFloat)) and isinstance(a1, CplInt):
        if isinstance(a0, CplFloat):
            return CplFloat(token, a0.value ** a1.value)
        return CplInt(token, int(a0.value ** a1.value))  # type: ignore
    output = CplScore(token, "__ipow__output __temp__", a0.unique_type.type)

    if isinstance(a1, CplInt):
        if a1.value == 0:
            return CplInt(token, 1)
        if a1.value == 1:
            return a0
        output._set(ctx, a0)
        for i in range(a1.value - 1):
            output._set_mul(ctx, a0)
    else:
        if isinstance(a0, CplInt):
            if a0.value == 0:
                return CplInt(token, 0)
            if a0.value == 1:
                return CplInt(token, 1)
        output._set(ctx, CplInt(token, 1))
        a0.cache(ctx, force="score", score_loc="__ipow__x __temp__")
        a1.cache(ctx, force="score", score_loc="__ipow__n __temp__")
        ctx.file.append(f"function {tr.pack_namespace}:__ipow__/loop{a0.unique_type.type[0]}")
        tr.files[f"__ipow__/loop{a0.unique_type.type[0]}"] = [
            f"scoreboard players remove __ipow__n __temp__ 1",
            f"scoreboard players operation __ipow__output __temp__ *= __ipow__x __temp__",
            f"scoreboard players operation __ipow__output __temp__ /= FLOAT_PREC __temp__" if a0.unique_type.type == "float" else "",
            f"execute if score __ipow__n __temp__ matches 1.. run function {tr.pack_namespace}:__ipow__/loop",
        ]

    return output


def lib_fastexp(ctx: TranspilerContext, args: List[Cpl], token):
    score = helper_float0(ctx, args, token, "Math.fastexp")
    if isinstance(score, float):
        return CplFloat(token, math.exp(score))

    exp_n = 8
    score._set_div(ctx, CplInt(token, 2 ** exp_n))
    score._set_add(ctx, CplInt(token, 1))
    for i in range(exp_n):
        score._set_mul(ctx, score)

    return score


def lib_exp(ctx: TranspilerContext, args: List[Cpl], token):
    tr = ctx.transpiler
    score = helper_float0(ctx, args, token, "Math.exp", "__exp__x __temp__")
    if isinstance(score, float):
        return CplFloat(token, math.exp(score))

    ctx.file.append(f"function {tr.pack_namespace}:__exp__/run")

    tr.files["__exp__/init"] = [
        f"scoreboard players set __exp__output __temp__ {int(FLOAT_PREC)}",
        f"scoreboard players set __exp__xn __temp__ {int(FLOAT_PREC)}",
        f"scoreboard players set __exp__nf __temp__ 1",
        f"scoreboard players set __exp__n __temp__ 0",
        f"function {tr.pack_namespace}:__exp__/loop"
    ]

    tr.files["__exp__/loop"] = [
        f"scoreboard players add __exp__n __temp__ 1",
        f"scoreboard players operation __exp__nf __temp__ *= __exp__n __temp__",
        f"scoreboard players operation __exp__xn __temp__ *= __exp__x __temp__",
        f"scoreboard players operation __exp__xn __temp__ /= FLOAT_PREC __temp__",
        f"scoreboard players operation __exp__xmn __temp__ = __exp__xn __temp__",
        f"scoreboard players operation __exp__xmn __temp__ /= __exp__nf __temp__",
        f"scoreboard players operation __exp__output __temp__ += __exp__xmn __temp__",
        f"execute unless score __exp__xmn __temp__ matches 0 run function {tr.pack_namespace}:__exp__/loop",
    ]

    return CplScore(token, "__exp__output __temp__", "float")


def lib_sin_cos(ctx: TranspilerContext, args: List[Cpl], token, cos):
    name = "cos" if cos else "sin"
    x = helper_float0(ctx, args, token, f"Math.{name}", f"__{name}__x __temp__")
    if isinstance(x, float):
        return CplFloat(token, math.cos(x) if cos else math.sin(x))

    if cos:
        # score = pi/2 - score
        x._set_sub(ctx, CplFloat(token, math.pi / 2))
        x._set_mul(ctx, CplFloat(token, -1))

    # https://en.wikipedia.org/wiki/Bh%C4%81skara_I%27s_sine_approximation_formula

    # sin(x) = 16 * x * (pi - x) / (5pi^2 - 4x(pi - x))
    # x0 = pi
    # x0 -= x
    # x1 = 16
    # x1 *= x
    # x1 *= x0
    # x2 = 5pi^2
    # x3 = 4
    # x3 *= x
    # x3 *= x0
    # x2 -= x3
    # x1 /= x2
    # return x1

    x0 = CplScore(token, f"__{name}__x0 __temp__", "float")
    x1 = CplScore(token, f"__{name}__x1 __temp__", "float")
    x2 = CplScore(token, f"__{name}__x2 __temp__", "float")
    x3 = CplScore(token, f"__{name}__x3 __temp__", "float")

    x0._set(ctx, CplFloat(token, math.pi))
    x0._set_sub(ctx, x)
    x1._set(ctx, CplFloat(token, 16))
    x1._set_mul(ctx, x)
    x1._set_mul(ctx, x0)
    x2._set(ctx, CplFloat(token, 5 * math.pi * math.pi))
    x3._set(ctx, CplFloat(token, 4))
    x3._set_mul(ctx, x)
    x3._set_mul(ctx, x0)
    x2._set_sub(ctx, x3)
    x1._set_div(ctx, x2)

    return x1


def lib_sin(ctx: TranspilerContext, args: List[Cpl], token):
    return lib_sin_cos(ctx, args, token, False)


def lib_cos(ctx: TranspilerContext, args: List[Cpl], token):
    return lib_sin_cos(ctx, args, token, True)


def lib_tan(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.tan")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.tan(x.value))
    return lib_sin(ctx, args, token)._set_div(ctx, lib_cos(ctx, args, token))


def lib_csc(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.csc")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, 1 / math.sin(x.value))
    return CplFloat(token, 1)._div(ctx, lib_sin(ctx, args, token))


def lib_sec(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.sec")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, 1 / math.cos(x.value))
    return CplFloat(token, 1)._div(ctx, lib_cos(ctx, args, token))


def lib_cot(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.cot")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, 1 / math.tan(x.value))
    return CplFloat(token, 1)._div(ctx, lib_tan(ctx, args, token))


def lib_fastarcsin(ctx: TranspilerContext, args: List[Cpl], token):
    x = helper_float0(ctx, args, token, f"Math.fastarcsin", f"__fastarcsin__x __temp__")
    if isinstance(x, float):
        return CplFloat(token, math.asin(x))
    x2 = CplScore(token, f"__fastarcsin__x2 __temp__", "float")
    num = CplScore(token, f"__fastarcsin__num __temp__", "float")
    denom = CplScore(token, f"__fastarcsin__denom __temp__", "float")

    # x2 = x*x
    # num = ((-0.0187293 * x2 + 0.0742610) * x2 - 0.2121144) * x2 + 1.5707288;
    # denom = ((0.0758669 * x2 + 1.0) * x2 + 1.0);
    # return x * num / denom

    # x2 = x
    # x2 *= x
    # num = -0.0187293
    # num *= x2
    # num += 0.0742610
    # num *= x2
    # num -= 0.2121144
    # num *= x2
    # num += 1.5707288
    # denom = 0.0758669
    # denom *= x2
    # denom += 1.0
    # denom *= x2
    # denom += 1.0
    # x *= num
    # x /= denom
    # return x

    x2._set(ctx, x)
    x2._set_mul(ctx, x)
    num._set(ctx, CplFloat(token, -0.0187293))
    num._set_mul(ctx, x2)
    num._set_add(ctx, CplFloat(token, 0.0742610))
    num._set_mul(ctx, x2)
    num._set_sub(ctx, CplFloat(token, 0.2121144))
    num._set_mul(ctx, x2)
    num._set_add(ctx, CplFloat(token, 1.5707288))
    denom._set(ctx, CplFloat(token, 0.0758669))
    denom._set_mul(ctx, x2)
    denom._set_add(ctx, CplFloat(token, 1.0))
    denom._set_mul(ctx, x2)
    denom._set_add(ctx, CplFloat(token, 1.0))
    x._set_mul(ctx, num)
    x._set_div(ctx, denom)

    return x


def lib_fastarccos(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.fastarccos")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.acos(x.value))
    return (lib_fastarcsin(ctx, args, token)
            ._set_sub(ctx, CplFloat(token, math.pi / 2))
            ._set_mul(ctx, CplFloat(token, -1)))


def lib_fastarctan(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.fastarctan")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.atan(x.value))
    # arctan(x) = arcsin(x / sqrt(1 + x^2))

    x0 = CplScore(token, f"__fastarcsin__x __temp__", "float")

    x0._set(ctx, x)
    x0._set_mul(ctx, x)
    x0._set_add(ctx, CplFloat(token, 1.0))
    x0 = x0._call_index(ctx, "sqrt", [x0], token)
    x._set_div(ctx, x0)

    return lib_fastarcsin(ctx, [x], token)


def lib_fastarccsc(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.fastarccsc")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.asin(1 / x.value))
    return lib_fastarcsin(ctx, [CplFloat(token, 1)._div(ctx, x)], token)


def lib_fastarcsec(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.fastarcsec")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.acos(1 / x.value))
    return lib_fastarccos(ctx, [CplFloat(token, 1)._div(ctx, x)], token)


def lib_fastarccot(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.fastarccot")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.atan(1 / x.value))
    return lib_fastarctan(ctx, [CplFloat(token, 1)._div(ctx, x)], token)


def lib_arcsin(ctx: TranspilerContext, args: List[Cpl], token):
    x = helper_float0(ctx, args, token, f"Math.arcsin", "__arcsin__x __temp__")
    if isinstance(x, float):
        return CplFloat(token, math.asin(x))

    # d/dx arcsin(x) = 1 / sqrt(1-x^2)
    # arcsin(a) = x
    # a = sin(x)
    # a - sin(x) = 0

    # iterate: y -= (sin(y) - x) / cos(y)

    y = CplScore(token, f"__arcsin__y __temp__", "float")
    z = CplScore(token, f"__arcsin__z __temp__", "float")

    # heuristic guess
    y._set(ctx, x)

    for i in range(2):
        z._set(ctx, lib_sin(ctx, [x], token))
        z._set_sub(ctx, x)
        z._set_div(ctx, lib_cos(ctx, [x], token))
        y._set_sub(ctx, z)

    return y


def lib_arccos(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.arccos")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.acos(x.value))

    return (lib_arcsin(ctx, args, token)
            ._set_sub(ctx, CplFloat(token, math.pi / 2))
            ._set_mul(ctx, CplFloat(token, -1)))


def lib_arctan(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.arctan")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.atan(x.value))

    # arctan(x) = arcsin(x / sqrt(1 + x^2))

    x0 = CplScore(token, f"__arcsin__x __temp__", "float")

    x0._set(ctx, x)
    x0._set_mul(ctx, x)
    x0._set_add(ctx, CplFloat(token, 1.0))
    x0 = x0._call_index(ctx, "sqrt", [x0], token)
    x._set_div(ctx, x0)

    return lib_arcsin(ctx, [x], token)


def lib_arccsc(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.arccsc")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.asin(1 / x.value))

    return lib_arcsin(ctx, [CplFloat(token, 1)._div(ctx, x)], token)


def lib_arcsec(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.arcsec")
    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.acos(1 / x.value))

    return lib_arccos(ctx, [CplFloat(token, 1)._div(ctx, x)], token)


def lib_arccot(ctx: TranspilerContext, args: List[Cpl], token):
    helper_float0_check(ctx, args, token, f"Math.arccot")

    x = args[0]
    if isinstance(x, CplInt) or isinstance(x, CplFloat):
        return CplFloat(token, math.atan(1 / x.value))

    return lib_arctan(ctx, [CplFloat(token, 1)._div(ctx, x)], token)


factorial_map = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800, 479001600]


def lib_factorial(ctx: TranspilerContext, args: List[Cpl], token):
    if len(args) != 1:
        raise_syntax_error(f"Expected 1 argument for Math.factorial()", token)
    n = args[0]
    if n.unique_type.type not in {"int", "float"}:
        raise_syntax_error(f"Expected an int or float argument for Math.factorial()", token)
    if isinstance(n, CplInt) or isinstance(n, CplFloat):
        return CplInt(token, factorial_map[int(n.value)])
    if isinstance(n, CplNBT):
        n = n.cache(ctx, force="score", force_t="int")
    ctx.file.append(f"scoreboard players set __factorial__output __temp__ 0")
    for index, value in enumerate(factorial_map):
        ctx.file.append(
            f"execute if score {n.location} matches {index} run scoreboard players set __factorial__output __temp__ {value}")

    return CplScore(token, "__factorial__output __temp__", "int")


add_lib(VariableDeclaration(
    name="Math",
    type=CustomCplObject({
        "sqrt": lib_sqrt,
        "cbrt": lib_cbrt,
        "isqrt": lib_isqrt,
        "floor": lib_floor,
        "ceil": lib_ceil,
        "round": lib_round,
        "min": lib_min,
        "max": lib_max,
        "random": lib_random,
        "frandom": lib_frandom,
        "ipow": lib_ipow,
        "fastexp": lib_fastexp,
        "exp": lib_exp,
        "sin": lib_sin,
        "cos": lib_cos,
        "tan": lib_tan,
        "csc": lib_csc,
        "sec": lib_sec,
        "cot": lib_cot,
        "arcsin": lib_arcsin,
        "arccos": lib_arccos,
        "arctan": lib_arctan,
        "arccsc": lib_arccsc,
        "arcsec": lib_arcsec,
        "arccot": lib_arccot,
        "fastarcsin": lib_fastarcsin,
        "fastarccos": lib_fastarccos,
        "fastarctan": lib_fastarctan,
        "fastarccsc": lib_fastarccsc,
        "fastarcsec": lib_fastarcsec,
        "fastarccot": lib_fastarccot,
        "factorial": lib_factorial,
        "PI": CplFloat(None, math.pi),
        "E": CplFloat(None, math.e),
        "LN2": CplFloat(None, math.log(2)),
        "LN10": CplFloat(None, math.log(10)),
        "LOG2E": CplFloat(None, math.log(math.e, 2)),
        "LOG10E": CplFloat(None, math.log(math.e, 10)),
        "SQRT1_2": CplFloat(None, math.sqrt(0.5)),
        "SQRT2": CplFloat(None, math.sqrt(2)),
    }),
    constant=True
))
