import bpy
import os
from pathlib import Path
import mathutils

from bpy.types import Operator
from bpy.ops import object
from bpy.path import abspath
from bpy.utils import register_class, unregister_class
from mathutils import Matrix
from typing import Optional

from ...utility import CData, PluginError, ExportUtils, raisePluginError, writeCData, toAlnum
from ...f3d.f3d_parser import importMeshC, getImportData
from ...f3d.f3d_gbi import DLFormat, TextureExportSettings, ScrollMethod, get_F3D_GBI
from ...f3d.f3d_writer import TriangleConverterInfo, removeDL, saveStaticModel, getInfoDict
from ..utility import ootGetObjectPath, ootGetObjectHeaderPath, getOOTScale
from ..model_classes import OOTF3DContext, ootGetIncludedAssetData
from ..texture_array import ootReadTextureArrays
from ..model_classes import OOTModel, OOTGfxFormatter
from ..f3d_writer import ootReadActorScale, writeTextureArraysNew, writeTextureArraysExisting
from .properties import OOTDLImportSettings, OOTDLExportSettings

from ..utility import (
    OOTObjectCategorizer,
    ootDuplicateHierarchy,
    ootCleanupScene,
    ootGetPath,
    addIncludeFiles,
    getOOTScale,
)


class OOTF3DGfxFormatter(OOTGfxFormatter):
    def __init__(self, scrollMethod):
        OOTGfxFormatter.__init__(self, scrollMethod)

    # override the function to give a custom name to the DL array
    def drawToC(self, f3d, gfxList, layer: Optional[str] = None):
        return gfxList.to_c(f3d, name_override=f"{gfxList.name}_{layer.lower()}_dl")


def ootConvertMeshToC(
    originalObj: bpy.types.Object,
    finalTransform: mathutils.Matrix,
    DLFormat: DLFormat,
    saveTextures: bool,
    settings: OOTDLExportSettings,
):
    folderName = settings.folder
    exportPath = bpy.path.abspath(settings.customPath)
    isCustomExport = settings.isCustom
    removeVanillaData = settings.removeVanillaData

    obj_name = toAlnum(originalObj.name)
    assert obj_name is not None

    filename = toAlnum(settings.filename) if settings.isCustomFilename else obj_name
    assert filename is not None

    overlayName = settings.actorOverlayName
    flipbookUses2DArray = settings.flipbookUses2DArray
    flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None

    try:
        obj, allObjs = ootDuplicateHierarchy(originalObj, None, False, OOTObjectCategorizer())

        fModel = OOTModel(obj_name, DLFormat, None)
        triConverterInfo = TriangleConverterInfo(obj, None, fModel.f3d, finalTransform, getInfoDict(obj))
        fMeshes = saveStaticModel(
            triConverterInfo, fModel, obj, finalTransform, fModel.name, not saveTextures, False, "oot"
        )

        # Since we provide a draw layer override, there should only be one fMesh.
        for fMesh in fMeshes.values():
            fMesh.draw.name = obj_name

        ootCleanupScene(originalObj, allObjs)

    except Exception as e:
        ootCleanupScene(originalObj, allObjs)
        raise Exception(str(e))

    data = CData()
    data.header = f"#ifndef {filename.upper()}_H\n" + f"#define {filename.upper()}_H\n\n" + '#include "ultra64.h"\n'

    if bpy.context.scene.fast64.oot.is_globalh_present():
        data.header += '#include "global.h"\n'

    data.source = f'#include "{filename}.h"\n\n'
    if not isCustomExport:
        data.header += f'#include "{folderName}.h"\n\n'
    else:
        data.header += "\n"

    path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, True)
    includeDir = settings.customAssetIncludeDir if settings.isCustom else f"assets/objects/{folderName}"
    exportData = fModel.to_c(
        TextureExportSettings(False, saveTextures, includeDir, path), OOTF3DGfxFormatter(ScrollMethod.Vertex)
    )

    data.append(exportData.all())

    if isCustomExport:
        textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D)
        data.append(textureArrayData)

    data.header += "\n#endif\n"
    writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c"))

    if not isCustomExport:
        writeTextureArraysExisting(bpy.context.scene.ootDecompPath, overlayName, False, flipbookArrayIndex2D, fModel)
        addIncludeFiles(folderName, path, filename)
        if removeVanillaData:
            headerPath = os.path.join(path, folderName + ".h")
            sourcePath = os.path.join(path, folderName + ".c")
            removeDL(sourcePath, headerPath, filename)


class OOT_ImportDL(Operator):
    # set bl_ properties
    bl_idname = "object.oot_import_dl"
    bl_label = "Import DL"
    bl_options = {"REGISTER", "UNDO", "PRESET"}

    # Called on demand (i.e. button press, menu item)
    # Can also be called from operator search menu (Spacebar)
    def execute(self, context):
        obj = None
        if context.mode != "OBJECT":
            object.mode_set(mode="OBJECT")

        try:
            settings: OOTDLImportSettings = context.scene.fast64.oot.DLImportSettings
            name = settings.name
            folderName: str = settings.folder
            importPath = abspath(settings.customPath)
            isCustomImport = settings.isCustom
            basePath = abspath(context.scene.ootDecompPath) if not isCustomImport else os.path.dirname(importPath)
            removeDoubles = settings.removeDoubles
            importNormals = settings.importNormals
            drawLayer = settings.drawLayer
            overlayName = settings.actorOverlayName
            flipbookUses2DArray = settings.flipbookUses2DArray
            flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None

            paths = None

            # Check if folderName exists under assets/objects (if it is committed)
            if not isCustomImport:
                assets_folder = Path(abspath(context.scene.ootDecompPath)) / "assets" / "objects" / folderName
                if assets_folder.exists():
                    paths = list(map(str, assets_folder.glob("*.[ch]")))
                    if not paths:
                        paths = None

            # Otherwise search in extracted/
            if paths is None:
                paths = [
                    ootGetObjectPath(isCustomImport, importPath, folderName, True),
                    ootGetObjectHeaderPath(isCustomImport, importPath, folderName, True),
                ]

            filedata = getImportData(paths)
            f3dContext = OOTF3DContext(get_F3D_GBI(), [name], basePath)

            # Test for "new" (post-ZAPD) assets system.
            is_2025_assets_system = "{\n#include" in filedata
            # The "new" assets system does not extract TLUTs, they are directly applied to the png images.
            # So don't try to apply TLUTs as they can't be found in extracted/ anyway.
            f3dContext.ignore_tlut = is_2025_assets_system

            scale = None
            if not isCustomImport:
                filedata = ootGetIncludedAssetData(basePath, paths, filedata) + filedata

                if overlayName is not None:
                    ootReadTextureArrays(basePath, overlayName, name, f3dContext, False, flipbookArrayIndex2D)
                if settings.autoDetectActorScale:
                    scale = ootReadActorScale(basePath, overlayName, False)

            if scale is None:
                scale = getOOTScale(settings.actorScale)

            obj = importMeshC(
                filedata,
                name,
                scale,
                removeDoubles,
                importNormals,
                drawLayer,
                f3dContext,
            )
            obj.ootActorScale = scale / context.scene.ootBlenderScale

            self.report({"INFO"}, "Success!")
            return {"FINISHED"}

        except Exception as e:
            if context.mode != "OBJECT":
                object.mode_set(mode="OBJECT")
            raisePluginError(self, e)
            return {"CANCELLED"}  # must return a set


class OOT_ExportDL(Operator):
    # set bl_ properties
    bl_idname = "object.oot_export_dl"
    bl_label = "Export DL"
    bl_options = {"REGISTER", "UNDO", "PRESET"}

    # Called on demand (i.e. button press, menu item)
    # Can also be called from operator search menu (Spacebar)
    def execute(self, context):
        with ExportUtils() as export_utils:
            obj = None
            if context.mode != "OBJECT":
                object.mode_set(mode="OBJECT")
            if len(context.selected_objects) == 0:
                raise PluginError("Mesh not selected.")
            obj = context.active_object
            if obj.type != "MESH":
                raise PluginError("Mesh not selected.")

            finalTransform = Matrix.Scale(getOOTScale(obj.ootActorScale), 4)

            try:
                # exportPath, levelName = getPathAndLevel(context.scene.geoCustomExport,
                # 	context.scene.geoExportPath, context.scene.geoLevelName,
                # 	context.scene.geoLevelOption)

                saveTextures = context.scene.saveTextures
                exportSettings = context.scene.fast64.oot.DLExportSettings

                ootConvertMeshToC(
                    obj,
                    finalTransform,
                    DLFormat.Static,
                    saveTextures,
                    exportSettings,
                )

                self.report({"INFO"}, "Success!")
                return {"FINISHED"}

            except Exception as e:
                if context.mode != "OBJECT":
                    object.mode_set(mode="OBJECT")
                raisePluginError(self, e)
                return {"CANCELLED"}  # must return a set


oot_dl_writer_classes = (
    OOT_ImportDL,
    OOT_ExportDL,
)


def f3d_ops_register():
    for cls in oot_dl_writer_classes:
        register_class(cls)


def f3d_ops_unregister():
    for cls in reversed(oot_dl_writer_classes):
        unregister_class(cls)
