"""
raymarch toy

usage: march <scene1> <scene2> ... <sceneN>

<scene>         ::= <scene> "." <command>
                |   <command>

<command>       ::= <id>
                |   <id> "(" <args> ")"

example:
    march cube.pos(0, 0, 0).color(red) cam(1.0).pos(1, 1, 1).lookat(0, 0, 0)
"""

import pathlib
from dataclasses import dataclass
import re
import numpy as np
from dataclasses import dataclass
from typing import Optional
from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES
from konabot.plugins.marchtoy.op import OPERATION_ENTRIES

@dataclass
class Command:
    id: str
    args: list[str]


class CommandChainParser:
    CHAIN_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?(\.[a-zA-Z]+(\([^(]*\))?)*"

    def __init__(self, _command_chain: str) -> None:
        self.command_chain = _command_chain

    def __iter__(self):
        return self

    def __next__(self):
        if query := re.match(CommandChainParser.CHAIN_PATTERN, self.command_chain):
            cmd_chain = query[0]
            self.command_chain = self.command_chain[len(cmd_chain) + 1 :]
            return cmd_chain
        raise StopIteration


class CommandParser:
    CMD_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?"
    ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)"

    def __init__(self, _command: str) -> None:
        self.command = _command

    def __iter__(self):
        return self

    def __next__(self):
        if query := re.match(CommandParser.CMD_PATTERN, self.command):
            cmd = query[0]
            if cmd_id_qry := re.match(CommandParser.ID_PATTERN, cmd):
                cmd_id = cmd_id_qry[0]
                self.command = self.command[len(cmd) + 1 :]
                cmd_args = cmd[len(cmd_id) + 1 : -1].replace(" ", "").split(",")
                while "" in cmd_args:
                    cmd_args.remove("")
                return Command(cmd_id, cmd_args)
        raise StopIteration

class Scene:
    def __init__(self, _instruction: str) -> None:
        self.canvas_objs: list[tuple[Object, str]] = []
        self.camera: Camera = Camera()
        for raw_cmd in CommandChainParser(_instruction):
            cmd_queue = CommandParser(raw_cmd)
            cmd_obj = next(cmd_queue)
            obj_id, obj_args = cmd_obj.id, cmd_obj.args
            obj_instance: Optional[Object] = None
            if obj_id in OBJECT_ENTRIES:
                obj_cls = OBJECT_ENTRIES[obj_id]
                if not issubclass(obj_cls, Object):
                    raise Exception(f"{obj_id} is not a subclass of Object.")
                obj_instance = obj_cls()
                try:
                    if len(obj_args) != 0:
                        obj_instance.parse_args(obj_args)
                except Exception as e:
                    raise Exception(
                        f"object {obj_id} failed to parse args passed in: {obj_args}"
                    ) from e
            else:
                raise Exception(f"{obj_id} is not a valid object type.")

            if obj_instance != None:
                for cmd in cmd_queue:
                    op_id, op_args = cmd.id, cmd.args
                    if op_id in OPERATION_ENTRIES:
                        op_func = OPERATION_ENTRIES[op_id]
                        if not callable(op_func):
                            raise Exception(f"{op_id} is not a valid operation.")
                        op_func(obj_instance, op_args)
                    else:
                        raise Exception(f"{op_id} is not a valid operation.")

                try:
                    sdf_block = obj_instance.sdf_block()
                    self.canvas_objs.append((obj_instance, sdf_block))
                except:
                    if type(obj_instance) == Camera:
                        self.camera = obj_instance

    def __str__(self) -> str:
        return ", ".join([str(type(obj)) for obj in self.canvas_objs])

    def compile(self, fs_src: str = "frag.glsl") -> str:
        PATH = pathlib.Path(__file__).parent / "shaders" / fs_src
        with PATH.open(encoding="utf-8") as f:
            content = f.read()
        sdf_block: str = ""
        color_block: str = ""

        index = 0
        for canvas_item in self.canvas_objs:
            obj, sdf_expr = canvas_item
            sdf_block += f"float sd{index} = {sdf_expr};"
            sdf_block += f"if(sd{index} < qry.value)"
            sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n"
            color = obj.texture.color
            color_block += (
                f"if(obj_id == {index}) return float4("
                f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n"
            )
            index += 1

        content = content.replace("<SDF_BLOCK>", sdf_block)
        content = content.replace("<COLOR_BLOCK>", color_block)

        cam_pos = self.camera.transform.t[0:3, 3]
        cam_focus = self.camera.focus
        cam_dir = self.camera.transform.t @ np.array((0.0, 0.0, -1.0, 0.0))
        content = content.replace(
            "<CAM_POS>", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}"
        )
        content = content.replace("<CAM_FOCUS>", str(cam_focus))
        content = content.replace(
            "<CAM_DIR>", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}"
        )
        return content
