# SPDX-FileCopyrightText: 2010-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later

import os
import traceback

from string import capwords
from collections import defaultdict
from types import ModuleType
from typing import Iterable

import bpy

from .utils.rig import METARIG_DIR, get_resource

from . import feature_set_list


class ARMATURE_MT_rigify_metarigs(bpy.types.Menu):
    """Add -> Armature -> Rigify Meta-Rigs."""

    bl_idname = "ARMATURE_MT_rigify_metarigs"
    bl_label = "Rigify Meta-Rigs"
    bl_description = "Add a Rigify Meta-Rig to the scene"

    def draw(self, context):
        for menu_func in menu_funcs:
            menu_func(self, context)


def _menu_add_rigify_metarigs(self, context):
    self.layout.menu("ARMATURE_MT_rigify_metarigs")


class ArmatureSubMenu(bpy.types.Menu):
    # bl_idname = 'ARMATURE_MT_armature_class'

    operators: list[tuple[str, str]]

    def draw(self, context):
        layout = self.layout
        layout.label(text=self.bl_label)
        for op, name in self.operators:
            text = capwords(name.replace("_", " "))
            layout.operator(op, icon='OUTLINER_OB_ARMATURE', text=text)


def get_metarigs(metarig_table: dict[str, ModuleType | dict],
                 base_dir: str, base_path: list[str], *,
                 path: Iterable[str] = (), nested=False):
    """ Searches for metarig modules, and returns a list of the
        imported modules.
    """

    dir_path = os.path.join(base_dir, *path)

    try:
        files: list[str] = os.listdir(dir_path)
    except FileNotFoundError:
        files = []

    files.sort()

    for f in files:
        is_dir = os.path.isdir(os.path.join(dir_path, f))  # Whether the file is a directory

        # Stop cases
        if f[0] in [".", "_"]:
            continue
        if f.count(".") >= 2 or (is_dir and "." in f):
            print("Warning: %r, filename contains a '.', skipping" % os.path.join(*path, f))
            continue

        if is_dir:  # Check directories
            get_metarigs(metarig_table[f], base_dir, base_path, path=[*path, f], nested=True)
        elif f.endswith(".py"):
            # Check straight-up python files
            f = f[:-3]
            module = get_resource('.'.join([*base_path, *path, f]))
            if nested:
                metarig_table[f] = module
            else:
                metarig_table[METARIG_DIR][f] = module


def make_metarig_add_execute(module):
    """ Create an execute method for a metarig creation operator.
    """
    def execute(_self, context):
        # Add armature object
        bpy.ops.object.armature_add()
        obj = context.active_object
        obj.name = "metarig"
        obj.data.name = "metarig"

        # Remove default bone
        bpy.ops.object.mode_set(mode='EDIT')
        bones = context.active_object.data.edit_bones
        bones.remove(bones[0])

        # Create metarig
        module.create(obj)

        bpy.ops.object.mode_set(mode='OBJECT')
        return {'FINISHED'}
    return execute


def make_metarig_menu_func(bl_idname: str, text: str):
    """ For some reason lambdas don't work for adding multiple menu
        items, so we use this instead to generate the functions.
    """

    def metarig_menu(self, _context):
        self.layout.operator(bl_idname, icon='OUTLINER_OB_ARMATURE', text=text)
    return metarig_menu


def make_submenu_func(bl_idname: str, text: str):
    def metarig_menu(self, _context):
        self.layout.menu(bl_idname, icon='OUTLINER_OB_ARMATURE', text=text)
    return metarig_menu


# Get the metarig modules
def get_internal_metarigs():
    base_rigify_dir = os.path.dirname(__file__)
    base_rigify_path = __name__.split('.')[:-1]

    get_metarigs(metarigs,
                 os.path.join(base_rigify_dir, METARIG_DIR),
                 [*base_rigify_path, METARIG_DIR])


def infinite_default_dict():
    return defaultdict(infinite_default_dict)


metarigs = infinite_default_dict()
metarig_ops = {}
armature_submenus = []
menu_funcs = []


def create_metarig_ops(dic: dict | None = None):
    if dic is None:
        dic = metarigs

    """Create metarig add Operators"""
    for metarig_category in dic:
        if metarig_category == "external":
            create_metarig_ops(dic[metarig_category])
            continue
        if metarig_category not in metarig_ops:
            metarig_ops[metarig_category] = []
        for m in dic[metarig_category].values():
            name = m.__name__.rsplit('.', 1)[1]

            # Dynamically construct an Operator
            op_type = type("Add_" + name + "_Metarig", (bpy.types.Operator,), {})
            op_type.bl_idname = "object.armature_" + name + "_metarig_add"
            op_type.bl_label = "Add " + name.replace("_", " ").capitalize() + " (metarig)"
            op_type.bl_options = {'REGISTER', 'UNDO'}
            op_type.execute = make_metarig_add_execute(m)

            metarig_ops[metarig_category].append((op_type, name))


def create_menu_funcs():
    global menu_funcs
    for mop, name in metarig_ops[METARIG_DIR]:
        text = capwords(name.replace("_", " "))
        menu_funcs += [make_metarig_menu_func(mop.bl_idname, text)]


def create_armature_submenus(dic: dict | None = None):
    if dic is None:
        dic = metarigs
    global menu_funcs
    metarig_categories = list(dic.keys())
    metarig_categories.sort()
    for metarig_category in metarig_categories:
        # Create menu functions
        if metarig_category == "external":
            create_armature_submenus(dic=metarigs["external"])
            continue
        if metarig_category == METARIG_DIR:
            continue

        armature_submenus.append(type('Class_' + metarig_category + '_submenu',
                                      (ArmatureSubMenu,), {}))
        armature_submenus[-1].bl_label = metarig_category
        armature_submenus[-1].bl_idname = 'ARMATURE_MT_%s_class' % metarig_category
        armature_submenus[-1].operators = []
        menu_funcs += [make_submenu_func(armature_submenus[-1].bl_idname, metarig_category)]

        for mop, name in metarig_ops[metarig_category]:
            arm_sub = next((e for e in armature_submenus
                            if e.bl_label == metarig_category),
                           '')
            arm_sub.operators.append((mop.bl_idname, name,))


def init_metarig_menu():
    get_internal_metarigs()
    create_metarig_ops()
    create_menu_funcs()
    create_armature_submenus()


#################
# Registering

def register():
    from bpy.utils import register_class

    for cl in metarig_ops:
        for mop, name in metarig_ops[cl]:
            register_class(mop)

    for arm_sub in armature_submenus:
        register_class(arm_sub)

    register_class(ARMATURE_MT_rigify_metarigs)
    bpy.types.VIEW3D_MT_armature_add.append(_menu_add_rigify_metarigs)


def unregister():
    from bpy.utils import unregister_class

    for cl in metarig_ops:
        for mop, name in metarig_ops[cl]:
            unregister_class(mop)

    for arm_sub in armature_submenus:
        unregister_class(arm_sub)

    unregister_class(ARMATURE_MT_rigify_metarigs)
    bpy.types.VIEW3D_MT_armature_add.remove(_menu_add_rigify_metarigs)


def get_external_metarigs(feature_module_names: list[str]):
    unregister()

    # Clear and fill metarigs public variables
    metarigs.clear()
    get_internal_metarigs()

    for module_name in feature_module_names:
        # noinspection PyBroadException
        try:
            base_dir, base_path = feature_set_list.get_dir_path(module_name, METARIG_DIR)

            get_metarigs(metarigs['external'], base_dir, base_path)
        except Exception:
            print(f"Rigify Error: Could not load feature set '{module_name}' metarigs: "
                  f"exception occurred.\n")
            traceback.print_exc()
            print("")
            feature_set_list.mark_feature_set_exception(module_name)
            continue

    metarig_ops.clear()
    armature_submenus.clear()
    menu_funcs.clear()

    create_metarig_ops()
    create_menu_funcs()
    create_armature_submenus()
    register()
