"""Module containing JMCFunction subclasses for custom JMC function that cannot be used with `/execute`"""

from ...tokenizer import Token, Tokenizer, TokenType
from ...exception import JMCSyntaxException
from ..jmc_function import JMCFunction, FuncType, func_property
from ..utils import ArgType, eval_expr, find_scoreboard_player_type
from .._flow_control import parse_switch


def _hardcode_parse(calc_pos: int, string: str, token: Token,
                    tokenizer: Tokenizer) -> str:
    count = 0
    expression = ''
    index = calc_pos
    if len(string) < calc_pos + 14 or string[calc_pos + 13] != '(':
        raise JMCSyntaxException(
            f"Expected ( after Hardcode.calc", token, tokenizer, display_col_length=False)
    for char in string[calc_pos + 13:]:  # len('Hardcode.calc') = 13
        index += 1
        if char == '(':
            count += 1
        elif char == ')':
            count -= 1

        if char not in {'0', '1', '2', '3', '4', '5', '6', '7',
                        '8', '9', '+', '-', '*', '/', ' ', '\t', '\n', '(', ')'}:
            raise JMCSyntaxException(
                f"Invalid charater({char}) in Hardcode.calc", token, tokenizer, display_col_length=False)

        expression += char
        if count == 0:
            break

    if count != 0:
        raise JMCSyntaxException(
            f"Invalid syntax in Hardcode.calc", token, tokenizer, display_col_length=False)

    return string[:calc_pos] + eval_expr(expression) + string[index + 13:]


def _hardcode_process(string: str, index_string: str,
                      i: int, token: Token, tokenizer: Tokenizer) -> str:
    string = string.replace(index_string, str(i))
    calc_pos = string.find('Hardcode.calc')
    if calc_pos != -1:
        string = _hardcode_parse(calc_pos, string, token, tokenizer)
    return string


@func_property(
    func_type=FuncType.EXECUTE_EXCLUDED,
    call_string='Hardcode.repeat',
    arg_type={
        "indexString": ArgType.STRING,
        "function": ArgType.ARROW_FUNC,
        "start": ArgType.INTEGER,
        "stop": ArgType.INTEGER,
        "step": ArgType.INTEGER
    },
    name='hardcode_repeat',
    ignore={
        "function"
    },
    defaults={
        "step": "1"
    }
)
class HardcodeRepeat(JMCFunction):
    def call(self) -> str:
        start = int(self.args["start"])
        step = int(self.args["step"])
        stop = int(self.args["stop"])
        if step == 0:
            raise JMCSyntaxException(
                "'step' must not be zero", self.raw_args["step"].token, self.tokenizer)

        commands: list[str] = []
        for i in range(start, stop, step):
            try:
                commands.extend(self.datapack.parse_function_token(
                    Token(
                        TokenType.PAREN_CURLY,
                        self.raw_args["function"].token.line,
                        self.raw_args["function"].token.col,
                        _hardcode_process(
                            self.raw_args["function"].token.string, self.args["indexString"], i, self.token, self.tokenizer
                        )
                    ), self.tokenizer)
                )
            except JMCSyntaxException as error:
                error.msg = 'WARNING: This error happens inside Hardcode.repeat, if you use Hardcode.calc the error position might not be accurate\n\n' + error.msg
                raise error

        return "\n".join(commands)


@func_property(
    func_type=FuncType.EXECUTE_EXCLUDED,
    call_string='Hardcode.switch',
    arg_type={
        "switch": ArgType.SCOREBOARD,
        "indexString": ArgType.STRING,
        "function": ArgType.ARROW_FUNC,
        "count": ArgType.INTEGER
    },
    name='hardcode_switch',
    ignore={
        "function",
        "switch"
    },
)
class HardcodeSwitch(JMCFunction):
    def call(self) -> str:
        count = int(self.args["count"])
        func_contents: list[list[str]] = []
        scoreboard_player = find_scoreboard_player_type(
            self.raw_args["switch"].token, self.tokenizer)
        for i in range(1, count + 1):
            try:
                func_contents.append(self.datapack.parse_function_token(
                    Token(
                        TokenType.PAREN_CURLY,
                        self.raw_args["function"].token.line,
                        self.raw_args["function"].token.col,
                        _hardcode_process(
                            self.raw_args["function"].token.string, self.args["indexString"], i, self.token, self.tokenizer
                        )
                    ), self.tokenizer)
                )
            except JMCSyntaxException as error:
                error.msg = 'WARNING: This error happens inside Hardcode.switch, if you use Hardcode.calc the error position might not be accurate\n\n' + error.msg
                raise error

        return parse_switch(scoreboard_player, func_contents,
                            self.datapack, self.name)


@func_property(
    func_type=FuncType.EXECUTE_EXCLUDED,
    call_string='Raycast.simple',
    arg_type={
        "onHit": ArgType.FUNC,
        "onStep": ArgType.FUNC,
        "interval": ArgType.FLOAT,
        "maxIter": ArgType.INTEGER,
        "boxSize": ArgType.FLOAT,
        "target": ArgType.SELECTOR,
        "stopAtEntity": ArgType.KEYWORD,
        "stopAtBlock": ArgType.KEYWORD,
        "runAtEnd": ArgType.KEYWORD
    },
    name='raycast_simple',
    defaults={
        "onStep": "",
        "interval": "0.1",
        "maxIter": "1000",
        "boxSize": "0.001",
        "target": "@e",
        "stopAtEntity": "true",
        "stopAtBlock": "true",
        "runAtEnd": "false"
    }
)
class RaycastSimple(JMCFunction):
    caster_tag = '__self__'
    current_iter = '__current_iter_raycast__'
    is_continue = '__is_continue_raycast__'

    def call(self) -> str:
        box_size = float(self.args["boxSize"])
        dx = "0" if box_size < 1 else str(box_size - 1)

        if self.args["target"].endswith(']'):
            target = f'{self.args["target"][:-1]},dx={dx},tag=!{self.caster_tag}]'
        else:
            target = f'{self.args["target"]}[dx={dx},tag=!{self.caster_tag}]'

        count = self.datapack.get_count(f"{self.name}/loop")
        raycast_loop = self.datapack.call_func(f"{self.name}/loop", count)

        is_stop_entity = self.check_bool("stopAtEntity")
        is_stop_block = self.check_bool("stopAtBlock")
        is_run_end = self.check_bool("runAtEnd")

        if is_stop_block:
            self.datapack.add_private_json("tags/blocks", f"{self.name}/default_raycast_pass", {
                "values": [
                    "minecraft:air",
                    "minecraft:void_air",
                    "minecraft:cave_air",
                    "minecraft:water",
                    "minecraft:lava",
                    "minecraft:grass",
                    "#minecraft:small_flowers",
                    "#minecraft:tall_flowers",
                    "#minecraft:small_dripleaf_placeable",
                    "minecraft:fern",
                    "minecraft:fire",
                    "minecraft:tall_grass",
                    "minecraft:large_fern",
                    "minecraft:vine",
                    "minecraft:twisting_vines",
                    "minecraft:twisting_vines_plant",
                    "minecraft:weeping_vines",
                    "minecraft:weeping_vines_plant",
                    "#minecraft:crops",
                    "#minecraft:saplings",
                    "#minecraft:signs",
                    "minecraft:attached_melon_stem",
                    "minecraft:attached_pumpkin_stem",
                    "minecraft:nether_wart",
                    "minecraft:sweet_berry_bush",
                    "minecraft:cocoa",
                    "minecraft:sugar_cane",
                    "minecraft:seagrass",
                    "minecraft:tall_seagrass",
                    "minecraft:redstone_wire",
                    "minecraft:rail",
                    "minecraft:powered_rail",
                    "minecraft:activator_rail",
                    "minecraft:detector_rail",
                    "minecraft:torch",
                    "minecraft:soul_torch",
                    "minecraft:redstone_torch",
                    "minecraft:glow_lichen"
                ]
            })

        if is_stop_entity:
            collide = self.datapack.add_raw_private_function(
                f"{self.name}/collide", [
                    f"scoreboard players reset {self.is_continue} {self.datapack.var_name}",
                    self.args["onHit"]
                ])
        else:
            collide = self.args["onHit"]

        if box_size <= 0.001:
            check_colide = f"execute as {target} positioned ~-{1-box_size} ~-{1-box_size} ~-{1-box_size} if entity @s[dx=0] positioned ~{1-box_size} ~{1-box_size} ~{1-box_size} run {collide}"
        elif box_size < 1:
            check_colide = f"execute positioned ~-{(1-box_size/2)} ~-{(1-box_size/2)} ~-{(1-box_size/2)} as {target} positioned ~{1-box_size} ~{1-box_size} ~{1-box_size} if entity @s[dx=0] positioned ~{box_size/2} ~{box_size/2} ~{box_size/2} run {collide}"
        else:
            check_colide = f"execute positioned ~-{box_size/2} ~-{box_size/2} ~-{box_size/2} as {target} positioned ~{box_size/2} ~{box_size/2} ~{box_size/2} run {collide}"

        self.datapack.add_raw_private_function(
            f"{self.name}/loop",
            [
                check_colide,
                f"scoreboard players add {self.current_iter} {self.datapack.var_name} 1",
                f"execute if score {self.current_iter} {self.datapack.var_name} matches {self.args['maxIter']}.. run scoreboard players reset {self.is_continue} {self.datapack.var_name}",
                self.args["onStep"],
                (f"execute unless block ~ ~ ~ #{self.datapack.namespace}:{self.datapack.private_name}/{self.name}/default_raycast_pass run scoreboard players reset {self.is_continue} {self.datapack.var_name}" if is_stop_block else ""),
                (f"execute unless score {self.is_continue} {self.datapack.var_name} matches 1 run {collide}" if is_run_end else ""),
                f"execute if score {self.is_continue} {self.datapack.var_name} matches 1 positioned ^ ^ ^{self.args['interval']} run {raycast_loop}"
            ],
            count=count)
        return f"""tag @s add {self.caster_tag}
scoreboard players set {self.current_iter} {self.datapack.var_name} 0
scoreboard players set {self.is_continue} {self.datapack.var_name} 1
execute anchored eyes positioned ^ ^ ^{self.args['interval']} run {raycast_loop}"""
