import queue
import threading
import traceback
import uuid
from typing import Any, List, Optional

import bpy
import mathutils
from smolagents import FinalAnswerTool, PythonInterpreterTool, Tool


class ToolManager:
    """Singleton manager for Blender tools that handles task queueing and execution on the main thread."""

    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super(ToolManager, cls).__new__(cls)
                cls._instance._initialize()
        return cls._instance

    def _initialize(self):
        prefs = bpy.context.preferences.addons[__package__].preferences

        self._task_queue = queue.Queue()
        self._result_dict = {}
        self._condition = threading.Condition()
        self._tools = [
            PythonInterpreterTool(),
            FinalAnswerTool(),
            GetSceneInfoTool(),
            GetObjectInfoTool(),
            CreateObjectTool(),
            ModifyObjectTool(),
            DeleteObjectTool(),
            SetMaterialTool(),
        ]

        if LlamaMeshModelManager.instance().is_loaded:
            self._tools += [
                LlamaMeshGenerateTool(),
                LlamaMeshDescribeTool(),
            ]

        if prefs.enable_hyper3d:
            self._tools += [Hyper3dGenerateObjectTool()]

    @property
    def tools(self):
        return self._tools

    def add_task(self, task):
        with self._condition:
            self._task_queue.put(task)
            self._condition.notify()

    def get_result(self, task_id):
        with self._condition:
            while task_id not in self._result_dict:
                self._condition.wait()
            return self._result_dict.pop(task_id)

    def process_tasks(self, context):
        try:
            while True:
                task = self._task_queue.get_nowait()
                func = task["func"]
                params = task.get("params") or {}

                if callable(func):
                    result = func(context, **params)
                    with self._condition:
                        self._result_dict[task["id"]] = result
                        self._condition.notify_all()
                else:
                    raise ValueError(f"Task function {func} is not callable")
        except queue.Empty:
            pass

    @classmethod
    def instance(cls):
        return cls()

    @classmethod
    def reset(cls):
        cls._instance = None


class BlenderTool(Tool):
    """
    Base class for all Blender tools.

    Provides utility method to run functions on the main blender thread.
    """

    def run_main_thread_func(self, func: callable, params: dict = None) -> str:
        task_id = str(uuid.uuid4())
        task = {
            "id": task_id,
            "func": func,
            "params": params,
        }
        tool_manager = ToolManager.instance()
        tool_manager.add_task(task)
        result = tool_manager.get_result(task_id)
        if result["status"] == "error":
            return f"Error in {func.__name__}: {result['data']}"
        return result["data"]


def get_aabb(obj):
    """Returns the world-space axis-aligned bounding box (AABB) of an object."""
    if obj.type != "MESH":
        raise TypeError("Object must be a mesh")

    # Get the bounding box corners in local space
    local_bbox_corners = [mathutils.Vector(corner) for corner in obj.bound_box]

    # Convert to world coordinates
    world_bbox_corners = [obj.matrix_world @ corner for corner in local_bbox_corners]

    # Compute axis-aligned min/max coordinates
    min_corner = mathutils.Vector(map(min, zip(*world_bbox_corners)))
    max_corner = mathutils.Vector(map(max, zip(*world_bbox_corners)))

    return [
        [*min_corner],
        [*max_corner],
    ]


def get_scene_info(context: Any):
    try:
        print("Getting scene info...")
        scene_info = {
            "name": context.scene.name,
            "object_count": len(context.scene.objects),
            "objects": [],
            "materials_count": len(bpy.data.materials),
        }

        for i, obj in enumerate(context.scene.objects):
            if i >= 10:
                break

            obj_info = {
                "name": obj.name,
                "type": obj.type,
                "location": [obj.location.x, obj.location.y, obj.location.z],
            }
            scene_info["objects"].append(obj_info)

        print(f"Scene info collected: {len(scene_info['objects'])} objects")
        return {"status": "success", "data": scene_info}
    except Exception as e:
        print(f"Error in get_scene_info: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class GetSceneInfoTool(BlenderTool):
    name = "get_scene_info"
    description = """
    This is a tool that gets detailed information about the current Blender scene.

    It returns the scene name, object count, a list of objects with their names, types, and locations, and the number of materials in the scene.

    Details are only provided for the first 10 objects, but the total object count is provided.

    The output is a dictionary with the following structure:
    {
        "name": "Scene",
        "object_count": 3,
        "objects": [
            {
                "name": "Cube",
                "type": "MESH",
                "location": [1.00, 2.00, 3.00]
            },
            {
                "name": "Sphere",
                "type": "MESH",
                "location": [0.00, 0.00, 0.00]
            },
            {
                "name": "Camera",
                "type": "CAMERA",
                "location": [0.00, 0.00, 0.00]
            }
        ],
        "materials_count": 2
    }
    """
    inputs = {}
    output_type = "object"

    def forward(self):
        return self.run_main_thread_func(get_scene_info)


def get_object_info(context: Any, name: str):
    try:
        obj = bpy.data.objects.get(name)
        if not obj:
            raise ValueError(f"Object with name {name} not found")

        obj_info = {
            "name": obj.name,
            "type": obj.type,
            "location": [obj.location.x, obj.location.y, obj.location.z],
            "rotation": [
                obj.rotation_euler.x,
                obj.rotation_euler.y,
                obj.rotation_euler.z,
            ],
            "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
            "visible": obj.visible_get(),
            "materials": [],
        }

        if obj.type == "MESH":
            bounding_box = get_aabb(obj)
            obj_info["world_bounding_box"] = bounding_box

        for slot in obj.material_slots:
            if slot.material:
                obj_info["materials"].append(slot.material.name)

        if obj.type == "MESH" and obj.data:
            mesh = obj.data
            obj_info["mesh"] = {
                "vertices": len(mesh.vertices),
                "edges": len(mesh.edges),
                "polygons": len(mesh.polygons),
            }

        return {"status": "success", "data": obj_info}
    except Exception as e:
        print(f"Error in get_object_info: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class GetObjectInfoTool(BlenderTool):
    name = "get_object_info"
    description = """
    This is a tool that gets detailed information about a specific object in the Blender scene.

    It returns the object name, type, location, rotation, scale, visibility, materials, and world bounding box (if applicable).

    The output is a dictionary with the following structure:
    {
        "name": "Cube",
        "type": "MESH",
        "location": [1.00, 2.00, 3.00],
        "rotation": [0.00, 0.00, 0.00],
        "scale": [1.00, 1.00, 1.00],
        "visible": True,
        "materials": ["Material1"],
        "world_bounding_box": [1.00, 2.00, 3.00, 1.00, 1.00, 1.00]
    }
    """
    inputs = {
        "name": {
            "type": "string",
            "description": "Name of the object to get information about",
        }
    }
    output_type = "object"

    def forward(self, name: str):
        return self.run_main_thread_func(get_object_info, {"name": name})


def create_object(
    context: Any,
    type: str,
    name: Optional[str] = None,
    location: List[float] = None,
    rotation: List[float] = None,
    scale: List[float] = None,
    align: Optional[str] = None,
    major_segments: Optional[int] = None,
    minor_segments: Optional[int] = None,
    mode: Optional[str] = None,
    major_radius: Optional[float] = None,
    minor_radius: Optional[float] = None,
    abso_major_rad: Optional[float] = None,
    abso_minor_rad: Optional[float] = None,
    generate_uvs: Optional[bool] = None,
):
    try:
        view_area_3d = next(
            (area for area in context.screen.areas if area.type == "VIEW_3D"), None
        )
        if view_area_3d is None:
            raise RuntimeError("View 3D area not found")

        override = context.copy()
        override["area"] = view_area_3d

        with context.temp_override(**override):
            # Deselect all objects first
            bpy.ops.object.select_all(action="DESELECT")

            location = location or [0, 0, 0]
            rotation = rotation or [0, 0, 0]
            scale = scale or [1, 1, 1]

            # Create the object based on type
            if type == "CUBE":
                bpy.ops.mesh.primitive_cube_add(
                    location=location, rotation=rotation, scale=scale
                )
            elif type == "SPHERE":
                bpy.ops.mesh.primitive_uv_sphere_add(
                    location=location, rotation=rotation, scale=scale
                )
            elif type == "CYLINDER":
                bpy.ops.mesh.primitive_cylinder_add(
                    location=location, rotation=rotation, scale=scale
                )
            elif type == "PLANE":
                bpy.ops.mesh.primitive_plane_add(
                    location=location, rotation=rotation, scale=scale
                )
            elif type == "CONE":
                bpy.ops.mesh.primitive_cone_add(
                    location=location, rotation=rotation, scale=scale
                )
            elif type == "TORUS":
                bpy.ops.mesh.primitive_torus_add(
                    align=align,
                    location=location,
                    rotation=rotation,
                    major_segments=major_segments,
                    minor_segments=minor_segments,
                    mode=mode,
                    major_radius=major_radius,
                    minor_radius=minor_radius,
                    abso_major_rad=abso_major_rad,
                    abso_minor_rad=abso_minor_rad,
                    generate_uvs=generate_uvs,
                )
            elif type == "EMPTY":
                bpy.ops.object.empty_add(
                    location=location, rotation=rotation, scale=scale
                )
            elif type == "CAMERA":
                bpy.ops.object.camera_add(location=location, rotation=rotation)
            elif type == "LIGHT":
                bpy.ops.object.light_add(
                    type="POINT",
                    location=location,
                    rotation=rotation,
                    scale=scale,
                )
            else:
                raise ValueError(f"Unsupported object type: {type}")

            # Force update the view layer
            bpy.context.view_layer.update()

            # Get the active object (which should be our newly created object)
            obj = bpy.context.view_layer.objects.active

            # If we don't have an active object, something went wrong
            if obj is None:
                raise RuntimeError("Failed to create object - no active object")

            # Make sure it's selected
            obj.select_set(True)

            # Rename if name is provided
            if name:
                obj.name = name
                if obj.data:
                    obj.data.name = name

            # Patch for PLANE: scale don't work with bpy.ops.mesh.primitive_plane_add()
            if type in {"PLANE"}:
                obj.scale = scale

            # Return the object info
            result = {
                "name": obj.name,
                "type": obj.type,
                "location": [obj.location.x, obj.location.y, obj.location.z],
                "rotation": [
                    obj.rotation_euler.x,
                    obj.rotation_euler.y,
                    obj.rotation_euler.z,
                ],
                "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
            }

            if obj.type == "MESH":
                bounding_box = get_aabb(obj)
                result["world_bounding_box"] = bounding_box

            return {"status": "success", "data": result}
    except Exception as e:
        print(f"Error in create_object: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class CreateObjectTool(BlenderTool):
    name = "create_object"
    description = """
    This is a tool that creates a new object in the Blender scene.

    It returns the created object name, type, location, rotation, scale, and world bounding box (if applicable).

    The output is a dictionary with the following structure:
    {
        "name": "Cube",
        "type": "MESH",
        "location": [1.00, 2.00, 3.00],
        "rotation": [0.00, 0.00, 0.00],
        "scale": [1.00, 1.00, 1.00],
        "world_bounding_box": [1.00, 2.00, 3.00, 1.00, 1.00, 1.00]
    }
    """
    inputs = {
        "type": {
            "type": "string",
            "description": "Object type (CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT)",
        },
        "name": {
            "type": "string",
            "description": "Optional name for the object",
            "nullable": True,
        },
        "location": {
            "type": "array",
            "description": "Optional [x, y, z] location coordinates",
            "nullable": True,
        },
        "rotation": {
            "type": "array",
            "description": "Optional [x, y, z] rotation in radians",
            "nullable": True,
        },
        "scale": {
            "type": "array",
            "description": "Optional [x, y, z] scale factors (not used for TORUS)",
            "nullable": True,
        },
        "align": {
            "type": "string",
            "description": "How to align the torus ('WORLD', 'VIEW', or 'CURSOR')",
            "nullable": True,
        },
        "major_segments": {
            "type": "integer",
            "description": "Number of segments for the main ring",
            "nullable": True,
        },
        "minor_segments": {
            "type": "integer",
            "description": "Number of segments for the cross-section",
            "nullable": True,
        },
        "mode": {
            "type": "string",
            "description": "Dimension mode ('MAJOR_MINOR' or 'EXT_INT')",
            "nullable": True,
        },
        "major_radius": {
            "type": "number",
            "description": "Radius from the origin to the center of the cross sections",
            "nullable": True,
        },
        "minor_radius": {
            "type": "number",
            "description": "Radius of the torus' cross section",
            "nullable": True,
        },
        "abso_major_rad": {
            "type": "number",
            "description": "Total exterior radius of the torus",
            "nullable": True,
        },
        "abso_minor_rad": {
            "type": "number",
            "description": "Total interior radius of the torus",
            "nullable": True,
        },
        "generate_uvs": {
            "type": "boolean",
            "description": "Whether to generate a default UV map",
            "nullable": True,
        },
    }
    output_type = "object"

    def forward(
        self,
        type: str,
        name: Optional[str] = None,
        location: Optional[List[float]] = None,
        rotation: Optional[List[float]] = None,
        scale: Optional[List[float]] = None,
        align: Optional[str] = "WORLD",
        major_segments: Optional[int] = 48,
        minor_segments: Optional[int] = 12,
        mode: Optional[str] = "MAJOR_MINOR",
        major_radius: Optional[float] = 1.0,
        minor_radius: Optional[float] = 0.25,
        abso_major_rad: Optional[float] = 1.25,
        abso_minor_rad: Optional[float] = 1.25,
        generate_uvs: Optional[bool] = True,
    ):
        location = location or [0, 0, 0]
        rotation = rotation or [0, 0, 0]
        scale = scale or [1, 1, 1]

        params = {
            "type": type,
            "location": location,
            "rotation": rotation,
            "scale": scale,
        }

        if name:
            params["name"] = name

        if type == "TORUS":
            params.update(
                {
                    "align": align,
                    "major_segments": major_segments,
                    "minor_segments": minor_segments,
                    "mode": mode,
                    "major_radius": major_radius,
                    "minor_radius": minor_radius,
                    "abso_major_rad": abso_major_rad,
                    "abso_minor_rad": abso_minor_rad,
                    "generate_uvs": generate_uvs,
                }
            )

        return self.run_main_thread_func(create_object, params)


def modify_object(
    context: Any,
    name: str,
    location: Optional[List[float]] = None,
    rotation: Optional[List[float]] = None,
    scale: Optional[List[float]] = None,
    visible: Optional[bool] = None,
):
    try:
        view_area_3d = next(
            (area for area in context.screen.areas if area.type == "VIEW_3D"), None
        )
        if view_area_3d is None:
            raise RuntimeError("View 3D area not found")

        override = context.copy()
        override["area"] = view_area_3d

        with context.temp_override(**override):
            obj = bpy.data.objects.get(name)
            if not obj:
                raise ValueError(f"Object with name {name} not found")

            if location is not None:
                obj.location = location
            if rotation is not None:
                obj.rotation_euler = rotation
            if scale is not None:
                obj.scale = scale
            if visible is not None:
                obj.visible_set(visible)

            result = {
                "name": obj.name,
                "type": obj.type,
                "location": [obj.location.x, obj.location.y, obj.location.z],
                "rotation": [
                    obj.rotation_euler.x,
                    obj.rotation_euler.y,
                    obj.rotation_euler.z,
                ],
                "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
                "visible": obj.visible_get(),
            }

            if obj.type == "MESH":
                bounding_box = get_aabb(obj)
                result["world_bounding_box"] = bounding_box

            return {"status": "success", "data": result}
    except Exception as e:
        print(f"Error in modify_object: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class ModifyObjectTool(BlenderTool):
    name = "modify_object"
    description = """
    This is a tool that modifies an existing object in the Blender scene.

    It can modify the object's location, rotation, scale, and visibility.
    
    It returns the modified object name, type, location, rotation, scale, and visibility.
    
    The output is a dictionary with the following structure:
    {
        "name": "Cube",
        "type": "MESH",
        "location": [1.00, 2.00, 3.00],
        "rotation": [0.00, 0.00, 0.00],
        "scale": [1.00, 1.00, 1.00],
        "visible": True
    }
    """
    inputs = {
        "name": {
            "type": "string",
            "description": "Name of the object to modify",
        },
        "location": {
            "type": "array",
            "description": "Optional [x, y, z] location coordinates",
            "nullable": True,
        },
        "rotation": {
            "type": "array",
            "description": "Optional [x, y, z] rotation in radians",
            "nullable": True,
        },
        "scale": {
            "type": "array",
            "description": "Optional [x, y, z] scale factors",
            "nullable": True,
        },
        "visible": {
            "type": "boolean",
            "description": "Optional visibility of the object",
            "nullable": True,
        },
    }
    output_type = "object"

    def forward(
        self,
        name: str,
        location: Optional[List[float]] = None,
        rotation: Optional[List[float]] = None,
        scale: Optional[List[float]] = None,
        visible: Optional[bool] = None,
    ):
        params = {
            "name": name,
            "location": location,
            "rotation": rotation,
            "scale": scale,
            "visible": visible,
        }
        return self.run_main_thread_func(modify_object, params)


def delete_object(context: Any, name: str) -> dict:
    try:
        view_area_3d = next(
            (area for area in context.screen.areas if area.type == "VIEW_3D"), None
        )
        if view_area_3d is None:
            raise RuntimeError("View 3D area not found")

        override = context.copy()
        override["area"] = view_area_3d

        with context.temp_override(**override):
            obj = bpy.data.objects.get(name)
            if not obj:
                raise ValueError(f"Object with name {name} not found")

            bpy.data.objects.remove(obj, do_unlink=True)
            return {"status": "success", "data": name}
    except Exception as e:
        print(f"Error in delete_object: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class DeleteObjectTool(BlenderTool):
    name = "delete_object"
    description = """
    This is a tool that deletes an existing object from the Blender scene.

    It returns the deleted object name.
    """
    inputs = {
        "name": {
            "type": "string",
            "description": "Name of the object to delete",
        }
    }
    output_type = "string"

    def forward(self, name: str):
        return self.run_main_thread_func(delete_object, {"name": name})


def set_material(
    context: Any,
    object_name: str,
    material_name: Optional[str] = None,
    create_if_missing: Optional[bool] = True,
    color: Optional[List[float]] = None,
):
    try:
        obj = bpy.data.objects.get(object_name)
        if not obj:
            raise ValueError(f"Object with name {object_name} not found")

        if not hasattr(obj, "data") or not hasattr(obj.data, "materials"):
            raise ValueError(f"Object {object_name} cannot accept materials")

        # Create or get material
        if material_name:
            mat = bpy.data.materials.get(material_name)
            if not mat and create_if_missing:
                mat = bpy.data.materials.new(name=material_name)
                print(f"Created new material: {material_name}")
        else:
            mat_name = f"{object_name}_material"
            mat = bpy.data.materials.get(mat_name)
            if not mat:
                mat = bpy.data.materials.new(name=mat_name)
            material_name = mat_name
            print(f"Using material: {material_name}")

        # Set up material nodes if needed
        if mat:
            if not mat.use_nodes:
                mat.use_nodes = True

            # Get or create Principled BSDF
            principled = mat.node_tree.nodes.get("Principled BSDF")
            if not principled:
                principled = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
                output = mat.node_tree.nodes.new("Material Output")
                if not output:
                    output = mat.node_tree.nodes.new("Material Output")
                if not principled.outputs[0].links:
                    mat.node_tree.links.new(principled.outputs[0], output.inputs[0])

            # Set color if provided
            if color and len(color) >= 3:
                principled.inputs["Base Color"].default_value = (
                    color[0],
                    color[1],
                    color[2],
                    1.0 if len(color) < 4 else color[3],
                )
                print(f"Set material color to {color}")

        if mat:
            if not obj.data.materials:
                obj.data.materials.append(mat)
            else:
                obj.data.materials[0] = mat

            print(f"Assigned material {mat.name} to {object_name}")

            result = {
                "object": object_name,
                "material": mat.name,
                "color": color if color else None,
            }

            return {"status": "success", "data": result}
    except Exception as e:
        print(f"Error in set_material: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class SetMaterialTool(BlenderTool):
    name = "set_material"
    description = """
    Set or create a material on an object.

    Returns the object name, material name, and color.

    The output is a dictionary with the following structure:
    {
        "object": "Cube",
        "material": "Material1",
        "color": [1.00, 0.00, 0.00, 1.00]
    }
    """
    inputs = {
        "object_name": {
            "type": "string",
            "description": "Name of the object to set the material on",
        },
        "material_name": {
            "type": "string",
            "description": "Optional name of the material to use or create",
            "nullable": True,
        },
        "create_if_missing": {
            "type": "boolean",
            "description": "Whether to create a new material if it doesn't exist",
            "nullable": True,
        },
        "color": {
            "type": "array",
            "description": "Optional [r, g, b, a] color values (0-1)",
            "nullable": True,
        },
    }
    output_type = "object"

    def forward(
        self,
        object_name: str,
        material_name: Optional[str] = None,
        create_if_missing: Optional[bool] = True,
        color: Optional[List[float]] = None,
    ):
        params = {
            "object_name": object_name,
            "material_name": material_name,
            "create_if_missing": create_if_missing,
            "color": color,
        }
        return self.run_main_thread_func(set_material, params)


class LlamaMeshModelManager:
    _instance = None
    _lock = threading.Lock()
    _model = None

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super(LlamaMeshModelManager, cls).__new__(cls)

        return cls._instance

    @property
    def is_loaded(self):
        return self._model is not None

    def load_model(self):
        if self._model is not None:
            return

        from llama_cpp import Llama

        repo_id = "bartowski/LLaMA-Mesh-GGUF"
        filename = "LLaMA-Mesh-Q4_K_M.gguf"

        self._model = Llama.from_pretrained(
            repo_id=repo_id, filename=filename, n_gpu_layers=-1, n_ctx=8192
        )

    def unload_model(self):
        if self._model is None:
            return

        import gc

        del self._model
        gc.collect()

        self._model = None

    def get_model(self):
        if not self.is_loaded:
            raise RuntimeError(
                "LLaMA-Mesh is not loaded. You are not able to use this tool."
            )
        return self._model

    @classmethod
    def instance(cls):
        return cls()


def llama_mesh_generate(context: Any, object_name: str, mesh_data: str):
    try:
        import bmesh

        view_area_3d = next(
            (area for area in context.screen.areas if area.type == "VIEW_3D"), None
        )
        if view_area_3d is None:
            raise RuntimeError("View 3D area not found")

        override = context.copy()
        override["area"] = view_area_3d

        with context.temp_override(**override):
            mesh_data_obj = bpy.data.meshes.new(object_name)
            mesh_obj = bpy.data.objects.new(object_name, mesh_data_obj)
            context.collection.objects.link(mesh_obj)
            bm = bmesh.new()

            def add_vertex(x, y, z):
                try:
                    bm.verts.new((x, y, z))
                    bm.verts.ensure_lookup_table()
                    return True
                except ValueError as e:
                    print(f"Error adding vertex: ({x}, {y}, {z}): {e}")
                    return False

            def add_face(a, b, c):
                try:
                    bm.faces.new((bm.verts[a - 1], bm.verts[b - 1], bm.verts[c - 1]))
                    bm.faces.ensure_lookup_table()
                    return True
                except IndexError as e:
                    print(f"IndexError adding face: ({a}, {b}, {c}): {e}")
                    return False
                except ValueError as e:
                    print(f"ValueError adding face: ({a}, {b}, {c}): {e}")
                    return False

            def process_line(line):
                print(line)
                line = line.strip()

                if line.startswith("v "):
                    parts = line.split()
                    if len(parts) == 4:
                        try:
                            x, y, z = map(int, parts[1:])
                            scale = 1 / 64.0
                            add_vertex(
                                x * scale - 0.5, z * scale - 0.5, y * scale - 0.5
                            )
                        except ValueError:
                            pass
                elif line.startswith("f "):
                    parts = line.split()
                    if len(parts) > 1:
                        try:
                            a, b, c = map(int, parts[1:])
                            add_face(a, b, c)
                        except ValueError:
                            pass

            for line in mesh_data.splitlines():
                process_line(line)

            bm.to_mesh(mesh_data_obj)
            mesh_data_obj.update()
            context.view_layer.update()
            bm.free()

            return {"status": "success", "data": object_name}
    except Exception as e:
        print(f"Error in generate_mesh: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class LlamaMeshGenerateTool(BlenderTool):
    name = "llama_mesh_generate"
    description = """
    Use LLaMA-Mesh to generate a 3D mesh from a description.

    This tool is capable of generating low-resolution meshes with a small number of vertices.

    Returns the name of the generated object.
    """
    inputs = {
        "object_name": {
            "type": "string",
            "description": "Name for the generated object",
        },
        "object_description": {
            "type": "string",
            "description": "A short description of the mesh to generate",
        },
        "temperature": {
            "type": "number",
            "description": "Temperature for the model from 0 to 1, where 0 is deterministic and 1 is random",
            "nullable": True,
        },
    }
    output_type = "string"

    def forward(
        self,
        object_name: str,
        object_description: str,
        temperature: Optional[float] = 0.5,
    ):
        model_manager = LlamaMeshModelManager.instance()
        model = model_manager.get_model()

        messages = [
            {
                "role": "system",
                "content": "You are a helpful assistant that can generate 3D meshes from descriptions.",
            },
            {
                "role": "user",
                "content": f"Generate an obj file for the following description: {object_description}",
            },
        ]

        response = model.create_chat_completion(
            messages=messages, stream=True, temperature=temperature
        )

        mesh_data = ""
        for chunk in response:
            try:
                content = chunk["choices"][0]["delta"].get("content", "")
                if content:
                    mesh_data += content
            except Exception as e:
                print(f"Error in process_chunk: {str(e)}")
                pass

        params = {
            "object_name": object_name,
            "mesh_data": mesh_data,
        }
        return self.run_main_thread_func(llama_mesh_generate, params)


def get_mesh_obj_data(context: Any, name: str):
    try:
        obj = bpy.data.objects.get(name)
        if not obj:
            raise ValueError(f"Object with name {name} not found")

        if not obj.type == "MESH":
            raise ValueError(f"Object with name {name} is not a mesh")

        bounding_box = get_aabb(obj)
        mesh = obj.data

        bpy.context.view_layer.objects.active = obj
        bpy.ops.object.mode_set(mode="EDIT")
        bpy.ops.mesh.quads_convert_to_tris(quad_method="BEAUTY", ngon_method="BEAUTY")
        bpy.ops.object.mode_set(mode="OBJECT")

        min_coords = bounding_box[0]
        max_coords = bounding_box[1]

        ranges = [max_coords[i] - min_coords[i] for i in range(3)]
        max_range = max(ranges)

        vertices_with_indices = []
        for i, vertex in enumerate(mesh.vertices):
            world_vertex = obj.matrix_world @ vertex.co
            quantized_vertex = [
                int((world_vertex[0] - min_coords[0]) / max_range * 63),
                int((world_vertex[2] - min_coords[2]) / max_range * 63),
                int((world_vertex[1] - min_coords[1]) / max_range * 63),
            ]
            vertices_with_indices.append((quantized_vertex, i))

        vertices_with_indices.sort(key=lambda x: (x[0][0], x[0][1], x[0][2]))

        old_to_new_indices = {
            old_idx: new_idx
            for new_idx, (_, old_idx) in enumerate(vertices_with_indices)
        }

        obj_lines = []

        for vertex in vertices_with_indices:
            v = vertex[0]
            obj_lines.append(f"v {v[0]} {v[1]} {v[2]}")

        faces = []

        for face in mesh.polygons:
            old_indices = list(face.vertices)
            new_indices = [old_to_new_indices[old_idx] for old_idx in old_indices]
            min_pos = new_indices.index(min(new_indices))
            new_indices = new_indices[min_pos:] + new_indices[:min_pos]
            faces.append(new_indices)

        faces.sort(key=lambda x: (x[0], x[1], x[2]))

        for face in faces:
            obj_lines.append(f"f {face[0] + 1} {face[1] + 1} {face[2] + 1}")

        obj_data = "\n".join(obj_lines)

        return {"status": "success", "data": obj_data}
    except Exception as e:
        print(f"Error in llama_mesh_describe: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class LlamaMeshDescribeTool(BlenderTool):
    name = "llama_mesh_describe"
    description = """
    Use LLaMA-Mesh to describe a 3D object given its mesh data.

    Only accepts objects with fewer than 800 vertices. Use `get_object_info` to check the number of vertices.

    Returns a string describing what the object is.
    """
    inputs = {
        "name": {
            "type": "string",
            "description": "Name of the object to describe",
        },
    }
    output_type = "string"

    def forward(self, name: str):
        try:
            model_manager = LlamaMeshModelManager.instance()
            model = model_manager.get_model()

            obj_data = self.run_main_thread_func(get_mesh_obj_data, {"name": name})

            messages = [
                {
                    "role": "system",
                    "content": """You are a knowledgeable, efficient, and direct AI assistant that can read 3D obj file data.
                    Provide concise answers, focusing only on key information needed.
                    """,
                },
                {
                    "role": "user",
                    "content": f"What is this object?\n{obj_data}",
                },
            ]

            response = model.create_chat_completion(
                messages=messages, stream=False, temperature=0.5
            )

            response = response["choices"][0]["message"]["content"]

            return response
        except Exception as e:
            return f"Error in llama_mesh_describe: {str(e)}"


def hyper3d_get_api_key(context: Any):
    try:
        prefs = bpy.context.preferences.addons[__package__].preferences
        return {"status": "success", "data": prefs.hyper3d_api_key}
    except Exception as e:
        return {"status": "error", "data": str(e)}


def hyper3d_run_api(api_key: str, object_description: str):
    import os
    import tempfile
    import time

    import requests

    files = [
        ("mesh_mode", (None, "Raw")),
        ("prompt", (None, object_description)),
    ]

    response = requests.post(
        "https://hyperhuman.deemos.com/api/v2/rodin",
        headers={"Authorization": f"Bearer {api_key}"},
        files=files,
    )

    data = response.json()

    succeed = data.get("submit_time", False)
    if not succeed:
        raise RuntimeError("Failed to create generate job")

    task_uuid = data["uuid"]
    subscription_key = data["jobs"]["subscription_key"]

    start_time = time.time()
    max_wait_time = 300  # 5 minutes

    print(f"Generation started. Task UUID: {task_uuid}")
    print(f"Waiting up to {max_wait_time} seconds for generation to complete...")

    while True:
        if time.time() - start_time > max_wait_time:
            raise RuntimeError(f"Generation timed out after {max_wait_time} seconds")

        response = requests.post(
            "https://hyperhuman.deemos.com/api/v2/status",
            headers={"Authorization": f"Bearer {api_key}"},
            json={"subscription_key": subscription_key},
        )

        data = response.json()
        status_list = [i["status"] for i in data["jobs"]]
        if all(status == "Done" for status in status_list):
            break

        time.sleep(2)

    print("Generation completed. Downloading result...")

    response = requests.post(
        "https://hyperhuman.deemos.com/api/v2/download",
        headers={"Authorization": f"Bearer {api_key}"},
        json={"task_uuid": task_uuid},
    )

    data = response.json()
    temp_file = None

    for i in data["list"]:
        if i["name"].endswith(".glb"):
            temp_file = tempfile.NamedTemporaryFile(
                delete=False,
                prefix=task_uuid,
                suffix=".glb",
            )

            try:
                response = requests.get(i["url"], stream=True)
                response.raise_for_status()

                for chunk in response.iter_content(chunk_size=8192):
                    temp_file.write(chunk)

                temp_file.close()
            except Exception as e:
                temp_file.close()
                os.unlink(temp_file.name)
                raise e

    return temp_file.name


def hyper3d_generate_object(context: Any, filepath: str, mesh_name: str):
    try:
        existing_objects = set(bpy.data.objects)
        bpy.ops.import_scene.gltf(filepath=filepath)
        bpy.context.view_layer.update()

        imported_objects = list(set(bpy.data.objects) - existing_objects)

        if not imported_objects:
            raise RuntimeError("Error: No objects were imported.")

        mesh_obj = None

        if len(imported_objects) == 1 and imported_objects[0].type == "MESH":
            mesh_obj = imported_objects[0]
            print("Single mesh imported, no cleanup needed.")
        else:
            parent_obj = imported_objects[0]
            if parent_obj.type == "EMPTY" and len(parent_obj.children) == 1:
                potential_mesh = parent_obj.children[0]
                if potential_mesh.type == "MESH":
                    print("GLB structure confirmed: Empty node with one mesh child.")
                    potential_mesh.parent = None

                    bpy.data.objects.remove(parent_obj)
                    print("Removed empty node, keeping only the mesh.")
                    mesh_obj = potential_mesh
                else:
                    raise RuntimeError("Error: Child is not a mesh object.")
            else:
                raise RuntimeError(
                    "Error: Expected an empty node with one mesh child or a single mesh object."
                )

        try:
            if mesh_obj and mesh_obj.name is not None and mesh_name:
                mesh_obj.name = mesh_name
                if mesh_obj.data.name is not None:
                    mesh_obj.data.name = mesh_name
                print(f"Mesh renamed to: {mesh_name}")
        except Exception:
            print("Having issue with renaming, give up renaming.")
        result = {
            "name": mesh_obj.name,
            "type": mesh_obj.type,
            "location": [mesh_obj.location.x, mesh_obj.location.y, mesh_obj.location.z],
            "rotation": [
                mesh_obj.rotation_euler.x,
                mesh_obj.rotation_euler.y,
                mesh_obj.rotation_euler.z,
            ],
            "scale": [mesh_obj.scale.x, mesh_obj.scale.y, mesh_obj.scale.z],
        }

        if mesh_obj.type == "MESH":
            bounding_box = get_aabb(mesh_obj)
            result["world_bounding_box"] = bounding_box

        return {"status": "success", "data": result}
    except Exception as e:
        print(f"Error in hyper3d_generate: {str(e)}")
        traceback.print_exc()
        return {"status": "error", "data": str(e)}


class Hyper3dGenerateObjectTool(BlenderTool):
    name = "hyper3d_generate_object"
    description = """
    Generate a 3D asset using Hyper3D by giving a description of the desired asset, then import the result into Blender.
    The 3D asset has built-in materials.    
    The generated model has a normalized size, so re-scaling after generation may be useful.

    Hyper3D is good at generating 3D models for a single item.
    Don't try to:
    1. Generate the whole scene at once.
    2. Generate terrain.
    3. Generate parts of the item separately and put them together.
    """
    inputs = {
        "object_description": {
            "type": "string",
            "description": "A short description of the object to generate",
        },
        "mesh_name": {
            "type": "string",
            "description": "Name of the mesh to generate",
        },
    }
    output_type = "object"

    def forward(self, object_description: str, mesh_name: str):
        try:
            api_key = self.run_main_thread_func(hyper3d_get_api_key)
            filepath = hyper3d_run_api(api_key, object_description)
            return self.run_main_thread_func(
                hyper3d_generate_object,
                {"filepath": filepath, "mesh_name": mesh_name},
            )
        except Exception as e:
            return f"Error in hyper3d_generate_object: {str(e)}"
