# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import random
import string
import time
from copy import deepcopy
from pathlib import Path
from sys import float_info
from typing import Final, Optional, TypeVar, Union

import bpy
from bpy.types import (
    Context,
    Driver,
    FCurve,
    Material,
    Node,
    NodeFrame,
    NodeGroup,
    NodeGroupOutput,
    NodeReroute,
    NodeSocket,
    NodeSocketBool,
    NodeSocketColor,
    NodeSocketFloat,
    NodeSocketFloatAngle,
    NodeSocketFloatFactor,
    NodeSocketFloatPercentage,
    NodeSocketFloatTime,
    NodeSocketFloatUnsigned,
    NodeSocketInt,
    NodeSocketIntFactor,
    NodeSocketIntPercentage,
    NodeSocketIntUnsigned,
    NodeSocketString,
    NodeSocketVector,
    NodeSocketVectorAcceleration,
    NodeSocketVectorDirection,
    NodeSocketVectorEuler,
    NodeSocketVectorTranslation,
    NodeSocketVectorVelocity,
    NodeSocketVectorXYZ,
    NodeTree,
    ShaderNodeAmbientOcclusion,
    ShaderNodeAttribute,
    ShaderNodeBevel,
    ShaderNodeBsdfAnisotropic,
    ShaderNodeBsdfGlass,
    ShaderNodeBsdfHair,
    ShaderNodeBsdfHairPrincipled,
    ShaderNodeBsdfPrincipled,
    ShaderNodeBsdfRefraction,
    ShaderNodeBsdfToon,
    ShaderNodeBump,
    ShaderNodeClamp,
    ShaderNodeCustomGroup,
    ShaderNodeDisplacement,
    ShaderNodeGroup,
    ShaderNodeMapping,
    ShaderNodeMapRange,
    ShaderNodeMath,
    ShaderNodeMixRGB,
    ShaderNodeNormalMap,
    ShaderNodeOutputAOV,
    ShaderNodeOutputLight,
    ShaderNodeOutputLineStyle,
    ShaderNodeOutputMaterial,
    ShaderNodeOutputWorld,
    ShaderNodeScript,
    ShaderNodeSubsurfaceScattering,
    ShaderNodeTangent,
    ShaderNodeTexBrick,
    ShaderNodeTexCoord,
    ShaderNodeTexEnvironment,
    ShaderNodeTexGradient,
    ShaderNodeTexIES,
    ShaderNodeTexImage,
    ShaderNodeTexMagic,
    ShaderNodeTexNoise,
    ShaderNodeTexSky,
    ShaderNodeTexVoronoi,
    ShaderNodeTexWave,
    ShaderNodeTexWhiteNoise,
    ShaderNodeUVAlongStroke,
    ShaderNodeUVMap,
    ShaderNodeVectorDisplacement,
    ShaderNodeVectorMath,
    ShaderNodeVectorRotate,
    ShaderNodeVectorTransform,
    ShaderNodeVertexColor,
    ShaderNodeWireframe,
)
from mathutils import Color, Euler, Vector

from . import convert
from .char import INTERNAL_NAME_PREFIX
from .gl import GL_CLAMP_TO_EDGE, GL_LINEAR, GL_NEAREST, GL_REPEAT
from .logger import get_logger
from .version import get_addon_version
from .workspace import wm_append_without_library

logger = get_logger(__name__)

LAST_MODIFIED_VERSION: Final = (3, 16, 1)

BOOL_SOCKET_CLASSES: Final = (NodeSocketBool,)
FLOAT_SOCKET_CLASSES: Final = (
    NodeSocketFloat,
    NodeSocketFloatAngle,
    NodeSocketFloatFactor,
    NodeSocketFloatPercentage,
    NodeSocketFloatTime,
    NodeSocketFloatUnsigned,
)
INT_SOCKET_CLASSES: Final = (
    NodeSocketInt,
    NodeSocketIntFactor,
    NodeSocketIntPercentage,
    NodeSocketIntUnsigned,
)
SCALAR_SOCKET_CLASSES: Final = (
    *BOOL_SOCKET_CLASSES,
    *FLOAT_SOCKET_CLASSES,
    *INT_SOCKET_CLASSES,
)
VECTOR_SOCKET_CLASSES: Final = (
    NodeSocketVector,
    NodeSocketVectorAcceleration,
    NodeSocketVectorDirection,
    NodeSocketVectorEuler,
    NodeSocketVectorTranslation,
    NodeSocketVectorVelocity,
    NodeSocketVectorXYZ,
)
COLOR_SOCKET_CLASSES: Final = (NodeSocketColor,)
STRING_SOCKET_CLASSES: Final = (NodeSocketString,)

MTOON1_AUTO_SETUP_GROUP_NAME: Final = "MToon"
MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY: Final = (
    "VRM Add-on MToon1 Auto Setup Placeholder"
)

OUTLINE_GEOMETRY_GROUP_NAME: Final = "VRM Add-on MToon 1.0 Outline Geometry Revision 1"

UV_GROUP_NAME: Final = "VRM Add-on MToon 1.0 UV Revision 1"
UV_GROUP_UV_SCALE_X_LABEL: Final = "UV Scale X"
UV_GROUP_UV_SCALE_X_DEFAULT: Final = 1
UV_GROUP_UV_SCALE_Y_LABEL: Final = "UV Scale Y"
UV_GROUP_UV_SCALE_Y_DEFAULT: Final = 1
UV_GROUP_UV_OFFSET_X_LABEL: Final = "UV Offset X"
UV_GROUP_UV_OFFSET_X_DEFAULT: Final = 0
UV_GROUP_UV_OFFSET_Y_LABEL: Final = "UV Offset Y"
UV_GROUP_UV_OFFSET_Y_DEFAULT: Final = 0
UV_GROUP_WRAP_S_LABEL: Final = "Wrap S"
UV_GROUP_WRAP_S_DEFAULT: Final = GL_REPEAT
UV_GROUP_WRAP_T_LABEL: Final = "Wrap T"
UV_GROUP_WRAP_T_DEFAULT: Final = GL_REPEAT
UV_GROUP_IMAGE_WIDTH_LABEL: Final = "Image Width"
UV_GROUP_IMAGE_HEIGHT_LABEL: Final = "Image Height"

UV_ANIMATION_GROUP_NAME: Final = "VRM Add-on MToon 1.0 UV Animation Revision 1"
UV_ANIMATION_GROUP_TRANSLATE_X_LABEL: Final = "Translate X"
UV_ANIMATION_GROUP_TRANSLATE_X_DEFAULT: Final = 0.0
UV_ANIMATION_GROUP_TRANSLATE_Y_LABEL: Final = "Translate Y"
UV_ANIMATION_GROUP_TRANSLATE_Y_DEFAULT: Final = 0.0
UV_ANIMATION_GROUP_ROTATION_LABEL: Final = "Rotation"
UV_ANIMATION_GROUP_ROTATION_DEFAULT: Final = 0.0
UV_ANIMATION_GROUP_FPS_NODE_NAME: Final = "Scene.Render.Fps"
UV_ANIMATION_GROUP_FPS_BASE_NODE_NAME: Final = "Scene.Render.FpsBase"
UV_ANIMATION_GROUP_FRAME_CURRENT_NODE_NAME: Final = "Scene.FrameCurrent"

NORMAL_GROUP_NAME: Final = "VRM Add-on MToon 1.0 Normal Revision 1"
NORMAL_GROUP_SCALE_LABEL: Final = "Normal Map Texture Scale"
NORMAL_GROUP_SCALE_DEFAULT: Final = 1.0
NORMAL_GROUP_IS_OUTLINE_LABEL: Final = "Is Outline"

MATCAP_UV_GROUP_NAME: Final = "VRM Add-on MToon 1.0 MatCap UV Revision 1"

OUTPUT_GROUP_NAME: Final = "VRM Add-on MToon 1.0 Output Revision 1"
OUTPUT_GROUP_DOUBLE_SIDED_LABEL: Final = "Double Sided"
OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_LABEL: Final = "Transparent With Z-Write"
OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_DEFAULT: Final = False
OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_LABEL: Final = "GI Equalization Factor"
OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_DEFAULT: Final = 0.9
OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_LABEL: Final = "Render Queue Offset Number"
OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_MIN: Final = -9
OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_DEFAULT: Final = 0
OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_MAX: Final = 9
OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_LABEL: Final = "Lit Color"
OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_LABEL: Final = "Lit Color Alpha"
OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_DEFAULT: Final = (1.0, 1.0, 1.0)
OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_DEFAULT: Final = 1.0
OUTPUT_GROUP_SHADE_COLOR_FACTOR_LABEL: Final = "Shade Color"
OUTPUT_GROUP_SHADE_COLOR_FACTOR_DEFAULT: Final = (1.0, 1.0, 1.0)
OUTPUT_GROUP_SHADING_TOONY_FACTOR_LABEL: Final = "Shading Toony"
OUTPUT_GROUP_SHADING_TOONY_FACTOR_DEFAULT: Final = 0.9
OUTPUT_GROUP_SHADING_SHIFT_FACTOR_LABEL: Final = "Shading Shift"
OUTPUT_GROUP_SHADING_SHIFT_FACTOR_DEFAULT: Final = -0.2
OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_LABEL: Final = "Shading Shift Texture Scale"
OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_DEFAULT: Final = 1.0
OUTPUT_GROUP_EMISSIVE_FACTOR_LABEL: Final = "Emissive Factor"
OUTPUT_GROUP_EMISSIVE_FACTOR_DEFAULT: Final = (0, 0, 0)
OUTPUT_GROUP_EMISSIVE_STRENGTH_LABEL: Final = "Emissive Strength"
OUTPUT_GROUP_EMISSIVE_STRENGTH_DEFAULT: Final = 1.0
OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_LABEL: Final = "Parametric Rim Color"
OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_DEFAULT: Final = (0, 0, 0)
OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_LABEL: Final = "Parametric Rim Fresnel Power"
OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_DEFAULT: Final = 1.0
OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_LABEL: Final = "Parametric Rim Lift"
OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_DEFAULT: Final = 1.0
OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_LABEL: Final = "Rim LightingMix"
OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_DEFAULT: Final = 0
OUTPUT_GROUP_IS_OUTLINE_LABEL: Final = NORMAL_GROUP_IS_OUTLINE_LABEL
OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_LABEL: Final = "Outline LightingMix"
OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_DEFAULT: Final = 1.0
OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_LABEL: Final = "Outline Color"
OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_DEFAULT: Final = (0, 0, 0)
OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_LABEL: Final = "Outline Width"
OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_DEFAULT: Final = 0
OUTPUT_GROUP_OUTLINE_WIDTH_MODE_LABEL: Final = "Outline Width Mode"
OUTPUT_GROUP_OUTLINE_WIDTH_MODE_MIN: Final = 0
OUTPUT_GROUP_OUTLINE_WIDTH_MODE_DEFAULT: Final = 0
OUTPUT_GROUP_OUTLINE_WIDTH_MODE_MAX: Final = 2
OUTPUT_GROUP_MATCAP_FACTOR_LABEL: Final = "MatCap Factor"
OUTPUT_GROUP_MATCAP_FACTOR_DEFAULT: Final = (1, 1, 1)
OUTPUT_GROUP_ALPHA_MODE_LABEL: Final = "Alpha Mode"
OUTPUT_GROUP_ALPHA_CUTOFF_LABEL: Final = "Alpha Cutoff"
OUTPUT_GROUP_ALPHA_CUTOFF_DEFAULT: Final = 0.5

SHADER_NODE_GROUP_NAMES: Final = (
    MATCAP_UV_GROUP_NAME,
    UV_GROUP_NAME,
    UV_ANIMATION_GROUP_NAME,
    NORMAL_GROUP_NAME,
    OUTPUT_GROUP_NAME,
)


def template_name(name: str) -> str:
    return INTERNAL_NAME_PREFIX + name + " Template"


def backup_name(name: str, backup_suffix: str) -> str:
    return name.removesuffix(" Template") + backup_suffix


def generate_backup_suffix() -> str:
    # There is a possibility of duplicates in very rare cases. To be exact, duplicates
    # must be regenerated if any are found.
    return " " + "".join(
        random.SystemRandom().choice(string.ascii_letters + string.digits)
        for _ in range(8)  # Be careful not to exceed a total of 64 characters
    )


def get_mtoon1_auto_setup_shader_node_group(context: Context) -> Optional[NodeTree]:
    node_group = context.blend_data.node_groups.get(MTOON1_AUTO_SETUP_GROUP_NAME)
    if not node_group:
        return None
    if node_group.get(MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY):
        return node_group
    return None


def add_mtoon1_auto_setup_shader_node_group(context: Context) -> None:
    blend_path = Path(__file__).with_name("mtoon_auto_setup.blend")
    node_tree_path = str(blend_path) + "/NodeTree"

    if get_mtoon1_auto_setup_shader_node_group(context):
        return

    wm_append_without_library(
        context,
        blend_path,
        append_filepath=node_tree_path + "/" + MTOON1_AUTO_SETUP_GROUP_NAME,
        append_filename=MTOON1_AUTO_SETUP_GROUP_NAME,
        append_directory=node_tree_path,
    )


def remove_mtoon1_auto_setup_shader_node_group(context: Context) -> None:
    node_group = get_mtoon1_auto_setup_shader_node_group(context)
    if not node_group or node_group.users:
        return
    context.blend_data.node_groups.remove(node_group)


def load_mtoon1_node_group(
    context: Context,
    blend_file_path: Path,
    node_group_name: str,
    node_group_type: str,
    node_tree_type: str,
    *,
    reset_node_groups: bool,
) -> None:
    from ..editor.extension import get_node_tree_extension

    start_time = time.perf_counter()

    if not blend_file_path.exists():
        logger.error("File not found: %s", blend_file_path)
        return

    if not reset_node_groups:
        checking_node_group = context.blend_data.node_groups.get(node_group_name)
        if checking_node_group:
            if (
                tuple(get_node_tree_extension(checking_node_group).addon_version)
                >= LAST_MODIFIED_VERSION
            ):
                return
            if checking_node_group.type != node_tree_type:
                message = (
                    f'Node Group "{node_group_name}" already exists'
                    + f' with different type "{checking_node_group.type}"'
                )
                raise TypeError(message)

    backup_suffix = generate_backup_suffix()

    template_node_group_name = template_name(node_group_name)
    old_template_node_group = context.blend_data.node_groups.get(
        template_node_group_name
    )
    if old_template_node_group:
        logger.error('Node Group "%s" already exists', template_node_group_name)
        old_template_node_group.name = backup_name(
            old_template_node_group.name, backup_suffix
        )

    node_tree_path = str(blend_file_path) + "/NodeTree"
    node_tree_append_result = wm_append_without_library(
        context,
        blend_file_path,
        append_filepath=(node_tree_path + "/" + template_node_group_name),
        append_filename=template_node_group_name,
        append_directory=node_tree_path,
    )
    if node_tree_append_result != {"FINISHED"}:
        raise RuntimeError(
            "Failed to append MToon 1.0 template node group "
            + f'"{template_node_group_name}": {node_tree_append_result}'
        )

    template_node_group = None
    try:
        template_node_group = context.blend_data.node_groups.get(
            template_node_group_name
        )
        if not template_node_group:
            raise ValueError("No " + template_node_group_name)

        node_group = context.blend_data.node_groups.get(node_group_name)
        if not node_group:
            node_group = context.blend_data.node_groups.new(
                node_group_name, node_group_type
            )
            clear_node_tree(node_group, clear_inputs_outputs=True)
        copy_node_tree(context, template_node_group, node_group)
        get_node_tree_extension(node_group).addon_version = get_addon_version()
    finally:
        if template_node_group:
            if template_node_group.users:
                logger.warning(
                    'Failed to remove "%s" with %d users while loading mtoon shader',
                    template_node_group.name,
                    template_node_group.users,
                )
            else:
                context.blend_data.node_groups.remove(template_node_group)

        # Logically, it may have already been removed, so retrieve it again
        old_template_node_group = context.blend_data.node_groups.get(
            backup_name(template_node_group_name, backup_suffix)
        )
        if old_template_node_group:
            old_template_node_group.name = template_node_group_name

    end_time = time.perf_counter()
    logger.debug(
        'Loaded NodeTree "%s": %.9f seconds', node_group_name, end_time - start_time
    )


def load_mtoon1_outline_geometry_node_group(
    context: Context, *, reset_node_groups: bool
) -> None:
    if bpy.app.version < (3, 3):
        return
    load_mtoon1_node_group(
        context,
        Path(__file__).with_name("mtoon1_outline.blend"),
        OUTLINE_GEOMETRY_GROUP_NAME,
        "GeometryNodeTree",
        "GEOMETRY",
        reset_node_groups=reset_node_groups,
    )


def load_mtoon1_shader_node_groups(
    context: Context, *, reset_node_groups: bool
) -> None:
    for shader_node_group_name in SHADER_NODE_GROUP_NAMES:
        load_mtoon1_node_group(
            context,
            Path(__file__).with_name("mtoon1.blend"),
            shader_node_group_name,
            "ShaderNodeTree",
            "SHADER",
            reset_node_groups=reset_node_groups,
        )
    setup_frame_count_driver(context)


def load_mtoon1_shader(
    context: Context,
    material: Material,
    *,
    reset_node_groups: bool,
) -> None:
    from ..editor.extension import get_material_extension

    if bpy.app.version < (5, 0, 0) and not material.use_nodes:
        material.use_nodes = True

    load_mtoon1_outline_geometry_node_group(
        context, reset_node_groups=reset_node_groups
    )
    load_mtoon1_shader_node_groups(context, reset_node_groups=reset_node_groups)

    start_time = time.perf_counter()

    backup_suffix = generate_backup_suffix()

    # Back up if there are materials with the same name as the one being appended.
    # In the future, this will be replaced with append(do_reuse_local_id=True).
    template_material_name = template_name("VRM Add-on MToon 1.0")
    old_material = context.blend_data.materials.get(template_material_name)
    if old_material:
        logger.error('Material "%s" already exists', template_material_name)
        old_material.name = backup_name(old_material.name, backup_suffix)

    # When appending a Material, NodeTree is also appended simultaneously.
    # Back up if NodeTree with the same name exists.
    for shader_node_group_name in SHADER_NODE_GROUP_NAMES:
        name = template_name(shader_node_group_name)
        old_template_group = context.blend_data.node_groups.get(name)
        if old_template_group:
            logger.error('Node Group "%s" already exists', name)
            old_template_group.name = backup_name(
                old_template_group.name, backup_suffix
            )

    blend_file_path = Path(__file__).with_name("mtoon1.blend")
    material_path = str(blend_file_path) + "/Material"
    material_append_result = wm_append_without_library(
        context,
        blend_file_path,
        append_filepath=material_path + "/" + template_material_name,
        append_filename=template_material_name,
        append_directory=material_path,
        # do_reuse_local_id=True
    )
    if material_append_result != {"FINISHED"}:
        raise RuntimeError(
            "Failed to append MToon 1.0 template material: "
            + f"{material_append_result}"
        )

    template_material = None
    try:
        template_material = context.blend_data.materials.get(template_material_name)
        if not template_material:
            raise ValueError("No " + template_material_name)
        template_material_node_tree = template_material.node_tree
        material_node_tree = material.node_tree
        if template_material_node_tree is None:
            logger.error("MToon template material node tree is None")
        elif material_node_tree is None:
            logger.error("MToon copy target material node tree is None")
        else:
            copy_node_tree(context, template_material_node_tree, material_node_tree)
    finally:
        if template_material:
            if template_material.users:
                logger.warning(
                    'Failed to remove "%s" with %d users while loading mtoon shader',
                    template_material.name,
                    template_material.users,
                )
            else:
                context.blend_data.materials.remove(template_material)

        # Remove NodeTree that was appended simultaneously when appending Material
        for shader_node_group_name in SHADER_NODE_GROUP_NAMES:
            shader_node_group_template_name = template_name(shader_node_group_name)
            template_group = context.blend_data.node_groups.get(
                shader_node_group_template_name
            )
            if template_group:
                if template_group.users:
                    logger.warning(
                        'Failed to remove "%s" with %d users'
                        " while loading mtoon shader",
                        template_group.name,
                        template_group.users,
                    )
                else:
                    context.blend_data.node_groups.remove(template_group)

        # Logically, it may have already been removed, so retrieve it again
        old_material = context.blend_data.materials.get(
            backup_name(template_material_name, backup_suffix)
        )
        if old_material:
            old_material.name = template_material_name

        # Restore the backed up NodeTree
        for shader_node_group_name in SHADER_NODE_GROUP_NAMES:
            name = template_name(shader_node_group_name)
            old_template_group = context.blend_data.node_groups.get(
                backup_name(name, backup_suffix)
            )
            if old_template_group:
                old_template_group.name = name

    ext = get_material_extension(material)
    ext.mtoon1.setup_drivers()

    end_time = time.perf_counter()
    logger.debug(
        'Loaded Material "%s": %.9f seconds', material.name, end_time - start_time
    )


def copy_socket(from_socket: NodeSocket, to_socket: NodeSocket) -> None:
    if (
        bpy.app.version < (5, 0)
        and to_socket.display_shape != from_socket.display_shape
    ):
        to_socket.display_shape = from_socket.display_shape
    if to_socket.enabled != from_socket.enabled:
        to_socket.enabled = from_socket.enabled
    if to_socket.hide != from_socket.hide:
        to_socket.hide = from_socket.hide
    if to_socket.hide_value != from_socket.hide_value:
        to_socket.hide_value = from_socket.hide_value
    if to_socket.link_limit != from_socket.link_limit:
        to_socket.link_limit = from_socket.link_limit
    if to_socket.name != from_socket.name:
        to_socket.name = from_socket.name
    if to_socket.show_expanded != from_socket.show_expanded:
        to_socket.show_expanded = from_socket.show_expanded
    if to_socket.type != from_socket.type:
        to_socket.type = from_socket.type


def copy_socket_interface(
    from_socket: "bpy.types.NodeSocketInterface",
    to_socket: "bpy.types.NodeSocketInterface",
) -> None:
    # bpy.app.version < (4,)
    from bpy.types import (
        NodeSocketInterfaceColor,
        NodeSocketInterfaceFloat,
        NodeSocketInterfaceFloatAngle,
        NodeSocketInterfaceFloatDistance,
        NodeSocketInterfaceFloatFactor,
        NodeSocketInterfaceFloatPercentage,
        NodeSocketInterfaceFloatTime,
        NodeSocketInterfaceFloatUnsigned,
        NodeSocketInterfaceVector,
        NodeSocketInterfaceVectorAcceleration,
        NodeSocketInterfaceVectorDirection,
        NodeSocketInterfaceVectorEuler,
        NodeSocketInterfaceVectorTranslation,
        NodeSocketInterfaceVectorVelocity,
        NodeSocketInterfaceVectorXYZ,
    )

    if bpy.app.version >= (3, 0, 0):
        to_socket.attribute_domain = from_socket.attribute_domain
        to_socket.bl_label = from_socket.bl_label
    to_socket.description = from_socket.description
    to_socket.name = from_socket.name
    to_socket.hide_value = from_socket.hide_value

    float_classes = (
        NodeSocketInterfaceFloat,
        NodeSocketInterfaceFloatAngle,
        NodeSocketInterfaceFloatDistance,
        NodeSocketInterfaceFloatFactor,
        NodeSocketInterfaceFloatPercentage,
        NodeSocketInterfaceFloatTime,
        NodeSocketInterfaceFloatUnsigned,
    )
    color_classes = (NodeSocketInterfaceColor,)
    vector_classes = (
        NodeSocketInterfaceVector,
        NodeSocketInterfaceVectorAcceleration,
        NodeSocketInterfaceVectorDirection,
        NodeSocketInterfaceVectorEuler,
        NodeSocketInterfaceVectorTranslation,
        NodeSocketInterfaceVectorVelocity,
        NodeSocketInterfaceVectorXYZ,
    )

    if isinstance(from_socket, float_classes) and isinstance(to_socket, float_classes):
        to_socket.default_value = from_socket.default_value
        to_socket.min_value = from_socket.min_value
        to_socket.max_value = from_socket.max_value
    elif isinstance(from_socket, color_classes) and isinstance(
        to_socket, color_classes
    ):
        to_socket.default_value = deepcopy(
            (
                from_socket.default_value[0],
                from_socket.default_value[1],
                from_socket.default_value[2],
                from_socket.default_value[3],
            )
        )
    elif isinstance(from_socket, vector_classes) and isinstance(
        to_socket, vector_classes
    ):
        to_socket.default_value = deepcopy(
            (
                from_socket.default_value[0],
                from_socket.default_value[1],
                from_socket.default_value[2],
            )
        )
        to_socket.min_value = from_socket.min_value
        to_socket.max_value = from_socket.max_value


def copy_socket_default_value(
    from_socket: NodeSocket,
    to_socket: NodeSocket,
) -> None:
    if isinstance(from_socket, SCALAR_SOCKET_CLASSES):
        if isinstance(to_socket, BOOL_SOCKET_CLASSES):
            default_bool_value = bool(from_socket.default_value)
            if to_socket.default_value != default_bool_value:
                to_socket.default_value = default_bool_value
        elif isinstance(to_socket, FLOAT_SOCKET_CLASSES):
            default_float_value = float(from_socket.default_value)
            if to_socket.default_value != default_float_value:
                to_socket.default_value = default_float_value
        elif isinstance(to_socket, INT_SOCKET_CLASSES):
            default_int_value = int(from_socket.default_value)
            if to_socket.default_value != default_int_value:
                to_socket.default_value = default_int_value
    elif isinstance(from_socket, VECTOR_SOCKET_CLASSES) and isinstance(
        to_socket, VECTOR_SOCKET_CLASSES
    ):
        default_vector_value = (
            from_socket.default_value[0],
            from_socket.default_value[1],
            from_socket.default_value[2],
        )
        if tuple(to_socket.default_value) != default_vector_value:
            to_socket.default_value = default_vector_value
    elif isinstance(from_socket, COLOR_SOCKET_CLASSES) and isinstance(
        to_socket, COLOR_SOCKET_CLASSES
    ):
        default_color_value = (
            from_socket.default_value[0],
            from_socket.default_value[1],
            from_socket.default_value[2],
            from_socket.default_value[3],
        )
        if tuple(to_socket.default_value) != default_color_value:
            to_socket.default_value = default_color_value
    elif isinstance(from_socket, STRING_SOCKET_CLASSES) and isinstance(
        to_socket, STRING_SOCKET_CLASSES
    ):
        default_string_value = from_socket.default_value
        if to_socket.default_value != default_string_value:
            to_socket.default_value = default_string_value


def copy_node_socket_default_value(
    from_node: Node,
    to_node: Node,
) -> None:
    for index, from_input in enumerate(from_node.inputs):
        if 0 <= index < len(to_node.inputs):
            to_input = to_node.inputs[index]
            copy_socket_default_value(from_input, to_input)
    for index, from_output in enumerate(from_node.outputs):
        if 0 <= index < len(to_node.outputs):
            to_output = to_node.outputs[index]
            copy_socket_default_value(from_output, to_output)


def copy_shader_node_group(
    context: Context,
    from_node: ShaderNodeGroup,
    to_node: ShaderNodeGroup,
) -> None:
    node_tree = from_node.node_tree
    if not node_tree:
        logger.error("No node_tree in ShaderNodeGroup: %s", from_node.name)
        return

    for shader_node_group_name in SHADER_NODE_GROUP_NAMES:
        shader_node_group_template_name = template_name(shader_node_group_name)
        if not node_tree.name.startswith(shader_node_group_template_name):
            continue

        group = context.blend_data.node_groups.get(shader_node_group_name)
        if not group:
            logger.error('"%s" Not Found', shader_node_group_name)
            continue

        to_node.node_tree = group
        return

    logger.error(
        "Importing ShaderNodeGroup doesn't be supported yet: %s", node_tree.name
    )


def copy_node(
    context: Context,
    from_node: Node,
    to_node: Node,
    from_to: dict[Node, Node],
) -> None:
    # ruff: noqa: SIM102  Disable SIM102 only in this function to improve if statement readability

    from_node_color = (
        from_node.color[0],
        from_node.color[1],
        from_node.color[2],
    )
    if tuple(to_node.color) != from_node_color:
        to_node.color = from_node_color

    if to_node.height != from_node.height:
        to_node.height = from_node.height

    if to_node.hide != from_node.hide:
        to_node.hide = from_node.hide

    for index, from_input in enumerate(from_node.inputs):
        if 0 <= index < len(to_node.inputs):
            to_input = to_node.inputs[index]
            copy_socket(from_input, to_input)

    if to_node.label != from_node.label:
        to_node.label = from_node.label

    from_node_location = (
        from_node.location[0],
        from_node.location[1],
    )
    if tuple(to_node.location) != from_node_location:
        to_node.location = from_node_location

    if to_node.mute != from_node.mute:
        to_node.mute = from_node.mute

    if to_node.name != from_node.name:
        to_node.name = from_node.name

    for index, from_output in enumerate(from_node.outputs):
        if 0 <= index < len(to_node.outputs):
            to_output = to_node.outputs[index]
            copy_socket(from_output, to_output)

    from_node_parent = from_node.parent
    if from_node_parent:
        to_node_parent = from_to.get(from_node_parent)
        if to_node.parent != to_node_parent:
            to_node.parent = to_node_parent

    if to_node.select:
        to_node.select = False

    if to_node.show_options != from_node.show_options:
        to_node.show_options = from_node.show_options

    if to_node.show_preview != from_node.show_preview:
        to_node.show_preview = from_node.show_preview

    if to_node.show_texture:
        to_node.show_texture = False

    if to_node.use_custom_color != from_node.use_custom_color:
        to_node.use_custom_color = from_node.use_custom_color

    if to_node.width != from_node.width:
        to_node.width = from_node.width

    if isinstance(from_node, NodeFrame) and isinstance(to_node, NodeFrame):
        if to_node.shrink != from_node.shrink:
            to_node.shrink = from_node.shrink
        if to_node.label_size != from_node.label_size:
            to_node.label_size = from_node.label_size
        if to_node.text != from_node.text:
            to_node.text = from_node.text
    if isinstance(from_node, NodeGroup):
        logger.error("Importing NodeGroup doesn't be supported yet")
    if isinstance(from_node, NodeGroupOutput) and isinstance(to_node, NodeGroupOutput):
        if to_node.is_active_output != from_node.is_active_output:
            to_node.is_active_output = from_node.is_active_output
    if isinstance(from_node, ShaderNodeWireframe) and isinstance(
        to_node, ShaderNodeWireframe
    ):
        if to_node.use_pixel_size != from_node.use_pixel_size:
            to_node.use_pixel_size = from_node.use_pixel_size
    if isinstance(from_node, ShaderNodeVertexColor) and isinstance(
        to_node, ShaderNodeVertexColor
    ):
        if to_node.layer_name != from_node.layer_name:
            to_node.layer_name = from_node.layer_name
    if isinstance(from_node, ShaderNodeVectorTransform) and isinstance(
        to_node, ShaderNodeVectorTransform
    ):
        if to_node.convert_from != from_node.convert_from:
            to_node.convert_from = from_node.convert_from
        if to_node.convert_to != from_node.convert_to:
            to_node.convert_to = from_node.convert_to
        if to_node.vector_type != from_node.vector_type:
            to_node.vector_type = from_node.vector_type
    if isinstance(from_node, ShaderNodeVectorRotate) and isinstance(
        to_node, ShaderNodeVectorRotate
    ):
        if to_node.invert != from_node.invert:
            to_node.invert = from_node.invert
        if to_node.rotation_type != from_node.rotation_type:
            to_node.rotation_type = from_node.rotation_type
    if isinstance(from_node, ShaderNodeVectorMath) and isinstance(
        to_node, ShaderNodeVectorMath
    ):
        if to_node.operation != from_node.operation:
            to_node.operation = from_node.operation
    if isinstance(from_node, ShaderNodeVectorDisplacement) and isinstance(
        to_node, ShaderNodeVectorDisplacement
    ):
        if to_node.space != from_node.space:
            to_node.space = from_node.space
    if isinstance(from_node, ShaderNodeUVMap) and isinstance(to_node, ShaderNodeUVMap):
        if to_node.from_instancer != from_node.from_instancer:
            to_node.from_instancer = from_node.from_instancer
        if to_node.uv_map != from_node.uv_map:
            to_node.uv_map = from_node.uv_map
    if isinstance(from_node, ShaderNodeUVAlongStroke) and isinstance(
        to_node, ShaderNodeUVAlongStroke
    ):
        if to_node.use_tips != from_node.use_tips:
            to_node.use_tips = from_node.use_tips
    if isinstance(from_node, ShaderNodeTexWhiteNoise) and isinstance(
        to_node, ShaderNodeTexWhiteNoise
    ):
        if to_node.noise_dimensions != from_node.noise_dimensions:
            to_node.noise_dimensions = from_node.noise_dimensions
    if isinstance(from_node, ShaderNodeTexWave) and isinstance(
        to_node, ShaderNodeTexWave
    ):
        # incomplete
        if to_node.bands_direction != from_node.bands_direction:
            to_node.bands_direction = from_node.bands_direction
        from_node_color_mapping_blend_color = (
            from_node.color_mapping.blend_color[0],
            from_node.color_mapping.blend_color[1],
            from_node.color_mapping.blend_color[2],
        )
        if (
            tuple(to_node.color_mapping.blend_color)
            != from_node_color_mapping_blend_color
        ):
            to_node.color_mapping.blend_color = from_node_color_mapping_blend_color
        if to_node.color_mapping.blend_type != from_node.color_mapping.blend_type:
            to_node.color_mapping.blend_type = from_node.color_mapping.blend_type
        if to_node.color_mapping.brightness != from_node.color_mapping.brightness:
            to_node.color_mapping.brightness = from_node.color_mapping.brightness
        if to_node.color_mapping.color_ramp.color_mode != (
            from_node.color_mapping.color_ramp.color_mode
        ):
            to_node.color_mapping.color_ramp.color_mode = (
                from_node.color_mapping.color_ramp.color_mode
            )
        if to_node.color_mapping.contrast != from_node.color_mapping.contrast:
            to_node.color_mapping.contrast = from_node.color_mapping.contrast
        if to_node.color_mapping.saturation != from_node.color_mapping.saturation:
            to_node.color_mapping.saturation = from_node.color_mapping.saturation
        if (
            to_node.color_mapping.use_color_ramp
            != from_node.color_mapping.use_color_ramp
        ):
            to_node.color_mapping.use_color_ramp = (
                from_node.color_mapping.use_color_ramp
            )
        if to_node.rings_direction != from_node.rings_direction:
            to_node.rings_direction = from_node.rings_direction
        if to_node.wave_profile != from_node.wave_profile:
            to_node.wave_profile = from_node.wave_profile
        if to_node.wave_type != from_node.wave_type:
            to_node.wave_type = from_node.wave_type
    if isinstance(from_node, ShaderNodeTexVoronoi) and isinstance(
        to_node, ShaderNodeTexVoronoi
    ):
        if to_node.distance != from_node.distance:
            to_node.distance = from_node.distance
        if to_node.feature != from_node.feature:
            to_node.feature = from_node.feature
        if to_node.voronoi_dimensions != from_node.voronoi_dimensions:
            to_node.voronoi_dimensions = from_node.voronoi_dimensions
    if isinstance(from_node, ShaderNodeTexSky) and isinstance(
        to_node, ShaderNodeTexSky
    ):
        if to_node.ground_albedo != from_node.ground_albedo:
            to_node.ground_albedo = from_node.ground_albedo
        if to_node.sky_type != from_node.sky_type:
            to_node.sky_type = from_node.sky_type
        from_node_sun_direction = (
            from_node.sun_direction[0],
            from_node.sun_direction[1],
            from_node.sun_direction[2],
        )
        if tuple(to_node.sun_direction) != from_node_sun_direction:
            to_node.sun_direction = from_node_sun_direction
        if to_node.turbidity != from_node.turbidity:
            to_node.turbidity = from_node.turbidity
    if isinstance(from_node, ShaderNodeTexNoise) and isinstance(
        to_node, ShaderNodeTexNoise
    ):
        if to_node.noise_dimensions != from_node.noise_dimensions:
            to_node.noise_dimensions = from_node.noise_dimensions
    if isinstance(from_node, ShaderNodeTexMagic) and isinstance(
        to_node, ShaderNodeTexMagic
    ):
        if to_node.turbulence_depth != from_node.turbulence_depth:
            to_node.turbulence_depth = from_node.turbulence_depth
    if isinstance(from_node, ShaderNodeTexImage) and isinstance(
        to_node, ShaderNodeTexImage
    ):
        if to_node.extension != from_node.extension:
            to_node.extension = from_node.extension
        # to_node.image = from_node.image
        if to_node.interpolation != from_node.interpolation:
            to_node.interpolation = from_node.interpolation
        if to_node.projection != from_node.projection:
            to_node.projection = from_node.projection
        if to_node.projection_blend != from_node.projection_blend:
            to_node.projection_blend = from_node.projection_blend
        if to_node.name == "Mtoon1BaseColorTexture.Image":
            if not to_node.select:
                to_node.select = True
            if not to_node.show_texture:
                to_node.show_texture = True
    if isinstance(from_node, ShaderNodeTexIES) and isinstance(
        to_node, ShaderNodeTexIES
    ):
        if to_node.filepath != from_node.filepath:
            to_node.filepath = from_node.filepath
        if to_node.ies != from_node.ies:
            to_node.ies = from_node.ies
        if to_node.mode != from_node.mode:
            to_node.mode = from_node.mode
    if isinstance(from_node, ShaderNodeTexGradient) and isinstance(
        to_node, ShaderNodeTexGradient
    ):
        if to_node.gradient_type != from_node.gradient_type:
            to_node.gradient_type = from_node.gradient_type
    if isinstance(from_node, ShaderNodeTexEnvironment) and isinstance(
        to_node, ShaderNodeTexEnvironment
    ):
        if to_node.image != from_node.image:
            to_node.image = from_node.image
        if to_node.interpolation != from_node.interpolation:
            to_node.interpolation = from_node.interpolation
        if to_node.projection != from_node.projection:
            to_node.projection = from_node.projection
    if isinstance(from_node, ShaderNodeTexCoord) and isinstance(
        to_node, ShaderNodeTexCoord
    ):
        if to_node.from_instancer != from_node.from_instancer:
            to_node.from_instancer = from_node.from_instancer
        if to_node.object != from_node.object:
            to_node.object = from_node.object
    if isinstance(from_node, ShaderNodeTexBrick) and isinstance(
        to_node, ShaderNodeTexBrick
    ):
        if to_node.offset != from_node.offset:
            to_node.offset = from_node.offset
        if to_node.offset_frequency != from_node.offset_frequency:
            to_node.offset_frequency = from_node.offset_frequency
        if to_node.squash != from_node.squash:
            to_node.squash = from_node.squash
        if to_node.squash_frequency != from_node.squash_frequency:
            to_node.squash_frequency = from_node.squash_frequency
    if isinstance(from_node, ShaderNodeTangent) and isinstance(
        to_node, ShaderNodeTangent
    ):
        if to_node.axis != from_node.axis:
            to_node.axis = from_node.axis
        if to_node.direction_type != from_node.direction_type:
            to_node.direction_type = from_node.direction_type
        if to_node.uv_map != from_node.uv_map:
            to_node.uv_map = from_node.uv_map
    if isinstance(from_node, ShaderNodeSubsurfaceScattering) and isinstance(
        to_node, ShaderNodeSubsurfaceScattering
    ):
        if to_node.falloff != from_node.falloff:
            to_node.falloff = from_node.falloff
    if isinstance(from_node, ShaderNodeScript) and isinstance(
        to_node, ShaderNodeScript
    ):
        if to_node.bytecode != from_node.bytecode:
            to_node.bytecode = from_node.bytecode
        if to_node.bytecode_hash != from_node.bytecode_hash:
            to_node.bytecode_hash = from_node.bytecode_hash
        if to_node.filepath != from_node.filepath:
            to_node.filepath = from_node.filepath
        if to_node.mode != from_node.mode:
            to_node.mode = from_node.mode
        if to_node.script != from_node.script:
            to_node.script = from_node.script
        if to_node.use_auto_update != from_node.use_auto_update:
            to_node.use_auto_update = from_node.use_auto_update
    if isinstance(from_node, ShaderNodeOutputWorld) and isinstance(
        to_node, ShaderNodeOutputWorld
    ):
        if to_node.is_active_output != from_node.is_active_output:
            to_node.is_active_output = from_node.is_active_output
        if to_node.target != from_node.target:
            to_node.target = from_node.target
    if isinstance(from_node, ShaderNodeOutputMaterial) and isinstance(
        to_node, ShaderNodeOutputMaterial
    ):
        if to_node.is_active_output != from_node.is_active_output:
            to_node.is_active_output = from_node.is_active_output
        if to_node.target != from_node.target:
            to_node.target = from_node.target
    if isinstance(from_node, ShaderNodeOutputLineStyle) and isinstance(
        to_node, ShaderNodeOutputLineStyle
    ):
        if to_node.blend_type != from_node.blend_type:
            to_node.blend_type = from_node.blend_type
        if to_node.is_active_output != from_node.is_active_output:
            to_node.is_active_output = from_node.is_active_output
        if to_node.target != from_node.target:
            to_node.target = from_node.target
        if to_node.use_alpha != from_node.use_alpha:
            to_node.use_alpha = from_node.use_alpha
        if to_node.use_clamp != from_node.use_clamp:
            to_node.use_clamp = from_node.use_clamp
    if isinstance(from_node, ShaderNodeOutputLight) and isinstance(
        to_node, ShaderNodeOutputLight
    ):
        if to_node.is_active_output != from_node.is_active_output:
            to_node.is_active_output = from_node.is_active_output
        if to_node.target != from_node.target:
            to_node.target = from_node.target
    if isinstance(from_node, ShaderNodeOutputAOV) and isinstance(
        to_node, ShaderNodeOutputAOV
    ):
        if to_node.name != from_node.name:
            to_node.name = from_node.name
    if isinstance(from_node, ShaderNodeNormalMap) and isinstance(
        to_node, ShaderNodeNormalMap
    ):
        if to_node.space != from_node.space:
            to_node.space = from_node.space
        if to_node.uv_map != from_node.uv_map:
            to_node.uv_map = from_node.uv_map
    if isinstance(from_node, ShaderNodeMixRGB) and isinstance(
        to_node, ShaderNodeMixRGB
    ):
        if to_node.blend_type != from_node.blend_type:
            to_node.blend_type = from_node.blend_type
        if to_node.use_alpha != from_node.use_alpha:
            to_node.use_alpha = from_node.use_alpha
        if to_node.use_clamp != from_node.use_clamp:
            to_node.use_clamp = from_node.use_clamp
    if isinstance(from_node, ShaderNodeMath) and isinstance(to_node, ShaderNodeMath):
        if to_node.operation != from_node.operation:
            to_node.operation = from_node.operation
        if to_node.use_clamp != from_node.use_clamp:
            to_node.use_clamp = from_node.use_clamp
    if isinstance(from_node, ShaderNodeMapping) and isinstance(
        to_node, ShaderNodeMapping
    ):
        if to_node.vector_type != from_node.vector_type:
            to_node.vector_type = from_node.vector_type
    if isinstance(from_node, ShaderNodeMapRange) and isinstance(
        to_node, ShaderNodeMapRange
    ):
        if to_node.clamp != from_node.clamp:
            to_node.clamp = from_node.clamp
        if to_node.interpolation_type != from_node.interpolation_type:
            to_node.interpolation_type = from_node.interpolation_type
    if isinstance(from_node, ShaderNodeGroup) and isinstance(to_node, ShaderNodeGroup):
        copy_shader_node_group(context, from_node, to_node)
    if isinstance(from_node, ShaderNodeDisplacement) and isinstance(
        to_node, ShaderNodeDisplacement
    ):
        if to_node.space != from_node.space:
            to_node.space = from_node.space
    if isinstance(from_node, ShaderNodeCustomGroup) and isinstance(
        to_node, ShaderNodeCustomGroup
    ):
        # to_node.node_tree = from_node.node_tree
        logger.error("Importing ShaderNodeCustomGroup doesn't be supported yet")
    if isinstance(from_node, ShaderNodeClamp) and isinstance(to_node, ShaderNodeClamp):
        if to_node.clamp_type != from_node.clamp_type:
            to_node.clamp_type = from_node.clamp_type
    if isinstance(from_node, ShaderNodeBump) and isinstance(to_node, ShaderNodeBump):
        if to_node.invert != from_node.invert:
            to_node.invert = from_node.invert
    if isinstance(from_node, ShaderNodeBsdfToon) and isinstance(
        to_node, ShaderNodeBsdfToon
    ):
        if to_node.component != from_node.component:
            to_node.component = from_node.component
    if isinstance(from_node, ShaderNodeBsdfRefraction) and isinstance(
        to_node, ShaderNodeBsdfRefraction
    ):
        if to_node.distribution != from_node.distribution:
            to_node.distribution = from_node.distribution
    if isinstance(from_node, ShaderNodeBsdfPrincipled) and isinstance(
        to_node, ShaderNodeBsdfPrincipled
    ):
        if to_node.distribution != from_node.distribution:
            to_node.distribution = from_node.distribution
        if to_node.subsurface_method != from_node.subsurface_method:
            to_node.subsurface_method = from_node.subsurface_method
    if isinstance(from_node, ShaderNodeBsdfHairPrincipled) and isinstance(
        to_node, ShaderNodeBsdfHairPrincipled
    ):
        if to_node.parametrization != from_node.parametrization:
            to_node.parametrization = from_node.parametrization
    if isinstance(from_node, ShaderNodeBsdfHair) and isinstance(
        to_node, ShaderNodeBsdfHair
    ):
        if to_node.component != from_node.component:
            to_node.component = from_node.component
    if isinstance(from_node, ShaderNodeBsdfGlass) and isinstance(
        to_node, ShaderNodeBsdfGlass
    ):
        if to_node.distribution != from_node.distribution:
            to_node.distribution = from_node.distribution
    if isinstance(from_node, ShaderNodeBsdfAnisotropic) and isinstance(
        to_node, ShaderNodeBsdfAnisotropic
    ):
        if to_node.distribution != from_node.distribution:
            to_node.distribution = from_node.distribution
    if isinstance(from_node, ShaderNodeBevel) and isinstance(to_node, ShaderNodeBevel):
        if to_node.samples != from_node.samples:
            to_node.samples = from_node.samples
    if isinstance(from_node, ShaderNodeAttribute) and isinstance(
        to_node, ShaderNodeAttribute
    ):
        if to_node.attribute_name != from_node.attribute_name:
            to_node.attribute_name = from_node.attribute_name
    if isinstance(from_node, ShaderNodeAmbientOcclusion) and isinstance(
        to_node, ShaderNodeAmbientOcclusion
    ):
        if to_node.inside != from_node.inside:
            to_node.inside = from_node.inside
        if to_node.only_local != from_node.only_local:
            to_node.only_local = from_node.only_local
        if to_node.samples != from_node.samples:
            to_node.samples = from_node.samples

    if bpy.app.version >= (3, 3):
        from bpy.types import (
            GeometryNodeDeleteGeometry,
            GeometryNodeExtrudeMesh,
            GeometryNodeSeparateGeometry,
            GeometryNodeSwitch,
            ShaderNodeCombineColor,
            ShaderNodeSeparateColor,
        )

        if isinstance(from_node, ShaderNodeCombineColor) and isinstance(
            to_node, ShaderNodeCombineColor
        ):
            if to_node.mode != from_node.mode:
                to_node.mode = from_node.mode
        if isinstance(from_node, ShaderNodeSeparateColor) and isinstance(
            to_node, ShaderNodeSeparateColor
        ):
            if to_node.mode != from_node.mode:
                to_node.mode = from_node.mode

        if isinstance(from_node, GeometryNodeSwitch) and isinstance(
            to_node, GeometryNodeSwitch
        ):
            if to_node.input_type != from_node.input_type:
                to_node.input_type = from_node.input_type
        if isinstance(from_node, GeometryNodeExtrudeMesh) and isinstance(
            to_node, GeometryNodeExtrudeMesh
        ):
            if to_node.mode != from_node.mode:
                to_node.mode = from_node.mode
        if isinstance(from_node, GeometryNodeDeleteGeometry) and isinstance(
            to_node, GeometryNodeDeleteGeometry
        ):
            if to_node.domain != from_node.domain:
                to_node.domain = from_node.domain
            if to_node.mode != from_node.mode:
                to_node.mode = from_node.mode
        if isinstance(from_node, GeometryNodeSeparateGeometry) and isinstance(
            to_node, GeometryNodeSeparateGeometry
        ):
            if to_node.domain != from_node.domain:
                to_node.domain = from_node.domain

    if bpy.app.version >= (3, 4):
        from bpy.types import ShaderNodeMix

        if isinstance(from_node, ShaderNodeMix) and isinstance(to_node, ShaderNodeMix):
            if to_node.blend_type != from_node.blend_type:
                to_node.blend_type = from_node.blend_type
            if to_node.clamp_factor != from_node.clamp_factor:
                to_node.clamp_factor = from_node.clamp_factor
            if to_node.clamp_result != from_node.clamp_result:
                to_node.clamp_result = from_node.clamp_result
            if to_node.data_type != from_node.data_type:
                to_node.data_type = from_node.data_type
            if to_node.factor_mode != from_node.factor_mode:
                to_node.factor_mode = from_node.factor_mode

    if bpy.app.version < (4, 0):
        from bpy.types import ShaderNodeBsdfGlossy

        if to_node.width_hidden != from_node.width_hidden:
            to_node.width_hidden = from_node.width_hidden
        if isinstance(from_node, ShaderNodeBsdfGlossy) and isinstance(
            to_node, ShaderNodeBsdfGlossy
        ):
            if to_node.distribution != from_node.distribution:
                to_node.distribution = from_node.distribution

    if bpy.app.version < (4, 1):
        from bpy.types import ShaderNodeTexMusgrave

        if isinstance(from_node, ShaderNodeTexMusgrave) and isinstance(
            to_node, ShaderNodeTexMusgrave
        ):
            if to_node.musgrave_dimensions != from_node.musgrave_dimensions:
                to_node.musgrave_dimensions = from_node.musgrave_dimensions
            if to_node.musgrave_type != from_node.musgrave_type:
                to_node.musgrave_type = from_node.musgrave_type

    if bpy.app.version < (5, 0):
        from bpy.types import ShaderNodeTexPointDensity

        if isinstance(from_node, ShaderNodeTexPointDensity) and isinstance(
            to_node, ShaderNodeTexPointDensity
        ):
            if to_node.interpolation != from_node.interpolation:
                to_node.interpolation = from_node.interpolation
            if to_node.object != from_node.object:
                to_node.object = from_node.object
            if to_node.particle_color_source != from_node.particle_color_source:
                to_node.particle_color_source = from_node.particle_color_source
            if to_node.point_source != from_node.point_source:
                to_node.point_source = from_node.point_source
            if to_node.radius != from_node.radius:
                to_node.radius = from_node.radius
            if to_node.resolution != from_node.resolution:
                to_node.resolution = from_node.resolution
            if to_node.space != from_node.space:
                to_node.space = from_node.space
            if to_node.vertex_attribute_name != from_node.vertex_attribute_name:
                to_node.vertex_attribute_name = from_node.vertex_attribute_name
            if to_node.vertex_color_source != from_node.vertex_color_source:
                to_node.vertex_color_source = from_node.vertex_color_source


def clear_node_tree(
    node_tree: Optional[NodeTree], *, clear_inputs_outputs: bool = False
) -> None:
    if node_tree is None:
        return

    # node_tree.links.clear()
    while node_tree.links:
        node_tree.links.remove(node_tree.links[0])

    # node_tree.nodes.clear()
    while node_tree.nodes:
        node_tree.nodes.remove(node_tree.nodes[0])

    if not clear_inputs_outputs:
        return

    if bpy.app.version < (4, 0):
        # node_tree.inputs.clear()
        while node_tree.inputs:
            node_tree.inputs.remove(node_tree.inputs[0])

        # node_tree.outputs.clear()
        while node_tree.outputs:
            node_tree.outputs.remove(node_tree.outputs[0])

        return

    node_tree.interface.clear()


def copy_node_tree_inputs_outputs(
    from_node_tree: NodeTree, to_node_tree: NodeTree
) -> None:
    # bpy.app.version < (4,)
    from bpy.types import NodeSocketInterfaceStandard

    while len(to_node_tree.inputs) > len(from_node_tree.inputs):
        to_node_tree.inputs.remove(to_node_tree.inputs[-1])
    for index, from_input in enumerate(from_node_tree.inputs):
        to_input = None
        if isinstance(from_input, NodeSocketInterfaceStandard) and 0 <= index < len(
            to_node_tree.inputs
        ):
            to_input = to_node_tree.inputs[index]
            if (
                isinstance(to_input, NodeSocketInterfaceStandard)
                and to_input.type != from_input.type
            ):
                to_input = None
                while len(to_node_tree.inputs) > index:
                    to_node_tree.inputs.remove(to_node_tree.inputs[-1])
        if not to_input:
            to_input = to_node_tree.inputs.new(
                from_input.bl_socket_idname, from_input.name
            )
        copy_socket_interface(from_input, to_input)

    while len(to_node_tree.outputs) > len(from_node_tree.outputs):
        to_node_tree.outputs.remove(to_node_tree.outputs[-1])
    for index, from_output in enumerate(from_node_tree.outputs):
        to_output = None
        if isinstance(from_output, NodeSocketInterfaceStandard) and 0 <= index < len(
            to_node_tree.outputs
        ):
            to_output = to_node_tree.outputs[index]
            if (
                isinstance(to_output, NodeSocketInterfaceStandard)
                and to_output.type != from_output.type
            ):
                to_output = None
                while len(to_node_tree.outputs) > index:
                    to_node_tree.outputs.remove(to_node_tree.outputs[-1])
        if not to_output:
            to_output = to_node_tree.outputs.new(
                from_output.bl_socket_idname, from_output.name
            )
        copy_socket_interface(from_output, to_output)


def copy_node_tree_interface_socket(
    from_socket: "bpy.types.NodeTreeInterfaceSocket",
    to_socket: "bpy.types.NodeTreeInterfaceSocket",
) -> None:
    from bpy.types import (
        NodeTreeInterfaceSocketColor,
        NodeTreeInterfaceSocketFloat,
        NodeTreeInterfaceSocketFloatAngle,
        NodeTreeInterfaceSocketFloatDistance,
        NodeTreeInterfaceSocketFloatFactor,
        NodeTreeInterfaceSocketFloatPercentage,
        NodeTreeInterfaceSocketFloatTime,
        NodeTreeInterfaceSocketFloatTimeAbsolute,
        NodeTreeInterfaceSocketFloatUnsigned,
        NodeTreeInterfaceSocketInt,
        NodeTreeInterfaceSocketIntFactor,
        NodeTreeInterfaceSocketIntPercentage,
        NodeTreeInterfaceSocketVector,
        NodeTreeInterfaceSocketVectorAcceleration,
        NodeTreeInterfaceSocketVectorDirection,
        NodeTreeInterfaceSocketVectorEuler,
        NodeTreeInterfaceSocketVectorTranslation,
        NodeTreeInterfaceSocketVectorVelocity,
        NodeTreeInterfaceSocketVectorXYZ,
    )

    float_classes = (
        NodeTreeInterfaceSocketFloat,
        NodeTreeInterfaceSocketFloatAngle,
        NodeTreeInterfaceSocketFloatDistance,
        NodeTreeInterfaceSocketFloatFactor,
        NodeTreeInterfaceSocketFloatPercentage,
        NodeTreeInterfaceSocketFloatTime,
        NodeTreeInterfaceSocketFloatTimeAbsolute,
        NodeTreeInterfaceSocketFloatUnsigned,
    )
    int_classes = (
        NodeTreeInterfaceSocketInt,
        NodeTreeInterfaceSocketIntFactor,
        NodeTreeInterfaceSocketIntPercentage,
    )
    color_classes = (NodeTreeInterfaceSocketColor,)
    vector_classes = (
        NodeTreeInterfaceSocketVector,
        NodeTreeInterfaceSocketVectorAcceleration,
        NodeTreeInterfaceSocketVectorDirection,
        NodeTreeInterfaceSocketVectorEuler,
        NodeTreeInterfaceSocketVectorTranslation,
        NodeTreeInterfaceSocketVectorVelocity,
        NodeTreeInterfaceSocketVectorXYZ,
    )

    to_socket.attribute_domain = from_socket.attribute_domain
    to_socket.default_attribute_name = from_socket.default_attribute_name
    to_socket.hide_value = from_socket.hide_value
    to_socket.hide_in_modifier = (
        True  # For internal use only, always hide from modifier
    )
    to_socket.force_non_field = from_socket.force_non_field

    if isinstance(from_socket, float_classes) and isinstance(to_socket, float_classes):
        to_socket.default_value = from_socket.default_value
        to_socket.min_value = from_socket.min_value
        to_socket.max_value = from_socket.max_value
    elif isinstance(from_socket, float_classes) and isinstance(to_socket, int_classes):
        max_int = 1_000_000_000
        min_int = -1_000_000_000
        to_socket.default_value = int(
            min(max(min_int, from_socket.default_value), max_int)
        )
        to_socket.min_value = int(min(max(min_int, from_socket.min_value), max_int))
        to_socket.max_value = int(min(max(min_int, from_socket.max_value), max_int))
    elif isinstance(from_socket, color_classes) and isinstance(
        to_socket, color_classes
    ):
        to_socket.default_value = deepcopy(
            (
                from_socket.default_value[0],
                from_socket.default_value[1],
                from_socket.default_value[2],
                from_socket.default_value[3],
            )
        )
    elif isinstance(from_socket, vector_classes) and isinstance(
        to_socket, vector_classes
    ):
        if isinstance(to_socket, NodeTreeInterfaceSocketVector):
            to_socket.default_value = deepcopy(
                (
                    from_socket.default_value[0],
                    from_socket.default_value[1],
                    from_socket.default_value[2],
                )
            )
        elif isinstance(to_socket, NodeTreeInterfaceSocketVectorEuler):
            if isinstance(from_socket, NodeTreeInterfaceSocketVectorEuler):
                to_socket.default_value = from_socket.default_value.copy()
            else:
                to_socket.default_value = Euler(
                    deepcopy(
                        (
                            from_socket.default_value[0],
                            from_socket.default_value[1],
                            from_socket.default_value[2],
                        )
                    ),
                    "XYZ",
                )
        elif isinstance(from_socket, NodeTreeInterfaceSocketVectorEuler):
            to_socket.default_value = Vector(
                (
                    from_socket.default_value.x,
                    from_socket.default_value.y,
                    from_socket.default_value.z,
                )
            )
        else:
            to_socket.default_value = Vector(
                (
                    from_socket.default_value[0],
                    from_socket.default_value[1],
                    from_socket.default_value[2],
                )
            )

        to_socket.min_value = from_socket.min_value
        to_socket.max_value = from_socket.max_value


def copy_node_tree_interface(from_node_tree: NodeTree, to_node_tree: NodeTree) -> None:
    from bpy.types import (
        NodeTreeInterfaceSocket,
        NodeTreeInterfaceSocketFloatFactor,
    )

    socket_type_to_group_name_to_socket_names: dict[str, dict[str, list[str]]] = {
        "NodeSocketBool": {
            OUTPUT_GROUP_NAME: [
                OUTPUT_GROUP_DOUBLE_SIDED_LABEL,
                OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_LABEL,
                OUTPUT_GROUP_IS_OUTLINE_LABEL,
            ],
            NORMAL_GROUP_NAME: [
                NORMAL_GROUP_IS_OUTLINE_LABEL,
            ],
        },
        "NodeSocketInt": {
            OUTPUT_GROUP_NAME: [
                OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_LABEL,
                OUTPUT_GROUP_OUTLINE_WIDTH_MODE_LABEL,
            ],
            UV_GROUP_NAME: [
                UV_GROUP_WRAP_S_LABEL,
                UV_GROUP_WRAP_T_LABEL,
                UV_GROUP_IMAGE_WIDTH_LABEL,
                UV_GROUP_IMAGE_HEIGHT_LABEL,
            ],
        },
    }

    to_items_index = 0

    for from_item in from_node_tree.interface.items_tree:
        if from_item.item_type != "SOCKET" or not isinstance(
            from_item, NodeTreeInterfaceSocket
        ):
            continue
        from_socket_type = from_item.socket_type
        if not from_socket_type:
            logger.error(
                'Socket source "%s" has empty socket_type. type=%s',
                from_item.name,
                type(from_item).__name__,
            )
            if isinstance(from_item, NodeTreeInterfaceSocketFloatFactor):
                from_socket_type = "NodeSocketFloat"
            else:
                continue

        translated_from_socket_type = from_socket_type
        if bpy.app.version >= (4, 1):
            for (
                socket_type,
                group_name_to_socket_names,
            ) in socket_type_to_group_name_to_socket_names.items():
                socket_names = group_name_to_socket_names.get(to_node_tree.name)
                if not socket_names:
                    continue
                if from_item.name in socket_names:
                    translated_from_socket_type = socket_type
                    break

        to_socket: Optional[NodeTreeInterfaceSocket] = None
        while len(to_node_tree.interface.items_tree) > to_items_index:
            to_item = list(to_node_tree.interface.items_tree.values())[to_items_index]
            if (
                to_item.item_type != "SOCKET"
                or not isinstance(to_item, NodeTreeInterfaceSocket)
                or to_item.in_out != from_item.in_out
            ):
                to_node_tree.interface.remove(to_item)
                continue

            if not to_item.socket_type:
                logger.error(
                    'Socket destination "%s" has empty socket_type. type=%s',
                    to_item.name,
                    type(to_item).__name__,
                )
                if isinstance(to_item, NodeTreeInterfaceSocketFloatFactor):
                    to_item.socket_type = "NodeSocketFloat"

            if to_item.socket_type not in [
                from_socket_type,
                translated_from_socket_type,
            ]:
                to_node_tree.interface.remove(to_item)
                continue

            if to_item.name != from_item.name:
                to_item.name = from_item.name
            if to_item.description != from_item.description:
                to_item.description = from_item.description
            to_socket = to_item
            break

        if to_socket is None:
            to_socket = to_node_tree.interface.new_socket(
                from_item.name,
                description=from_item.description,
                in_out=from_item.in_out,
                socket_type=translated_from_socket_type,
            )

        copy_node_tree_interface_socket(from_item, to_socket)
        to_items_index += 1

    while len(to_node_tree.interface.items_tree) > to_items_index:
        to_node_tree.interface.remove(
            list(to_node_tree.interface.items_tree.values())[-1]
        )


def copy_node_tree(
    context: Context, from_node_tree: NodeTree, to_node_tree: NodeTree
) -> None:
    clear_node_tree(to_node_tree, clear_inputs_outputs=False)

    if bpy.app.version < (4, 0):
        copy_node_tree_inputs_outputs(from_node_tree, to_node_tree)
    else:
        copy_node_tree_interface(from_node_tree, to_node_tree)

    from_to: dict[Node, Node] = {}

    # Use sorted because glTF add-on uses index-aware logic.
    # https://github.com/KhronosGroup/glTF-Blender-IO/blob/86453b20c28d7c78a3f9106911df69fd0138fb2a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_search_node_tree.py#L806
    for from_node in sorted(from_node_tree.nodes, key=lambda n: n.name):
        to_node = to_node_tree.nodes.new(from_node.bl_idname)
        from_to[from_node] = to_node

    for from_node, to_node in from_to.items():
        copy_node(context, from_node, to_node, from_to)

    for from_link in from_node_tree.links:
        if not from_link.is_valid:
            continue

        input_socket_index = next(
            (
                i
                for i, s in enumerate(from_link.to_node.inputs)
                if s == from_link.to_socket
            ),
            None,
        )
        if input_socket_index is None:
            continue
        input_node = from_to.get(from_link.to_node)
        if input_node is None:
            continue
        if not 0 <= input_socket_index < len(input_node.inputs):
            logger.error(
                "Input socket out of range: %d < %d",
                input_socket_index,
                len(input_node.inputs),
            )
            continue
        input_socket = input_node.inputs[input_socket_index]
        if not input_socket:
            logger.error("No input socket: %s", from_link.to_socket.name)
            continue

        output_socket_index = next(
            (
                i
                for i, s in enumerate(from_link.from_node.outputs)
                if s == from_link.from_socket
            ),
            None,
        )
        if output_socket_index is None:
            continue
        output_node = from_to.get(from_link.from_node)
        if output_node is None:
            continue
        if not 0 <= output_socket_index < len(output_node.outputs):
            logger.error(
                "Output socket out of range: %d < %d",
                output_socket_index,
                len(output_node.outputs),
            )
            continue
        output_socket = output_node.outputs[output_socket_index]
        if not output_socket:
            logger.error("No output socket: %s", from_link.from_socket.name)
            continue

        to_node_tree.links.new(input_socket, output_socket)

    for from_node, to_node in from_to.items():
        copy_node_socket_default_value(from_node, to_node)

    for from_node, to_node in from_to.items():
        # The location of the node can be completely restored by setting the location
        # again with a consistent parent-child relationship.
        from_node_location = (
            from_node.location[0],
            from_node.location[1],
        )
        if tuple(to_node.location) != from_node_location:
            to_node.location = from_node_location


def get_image_name_and_sampler_type(
    shader_node: Node, input_socket_name: str
) -> Optional[tuple[str, int, int]]:
    if (
        input_socket_name == "NormalmapTexture"
        and "NormalmapTexture" not in shader_node.inputs
        and "NomalmapTexture" in shader_node.inputs
    ):
        input_socket_name = "NomalmapTexture"

    socket = shader_node.inputs.get(input_socket_name)
    if not socket:
        return None

    links = socket.links
    if not links:
        return None

    from_node = links[0].from_node
    if not from_node:
        return None

    if not isinstance(from_node, ShaderNodeTexImage):
        return None

    image = from_node.image
    if not image:
        return None

    image_name = image.name
    if not image_name:
        return None

    # blender is ('Linear', 'Closest', 'Cubic', 'Smart')
    # glTF is Linear, Closest
    filter_type = GL_NEAREST if from_node.interpolation == "Closest" else GL_LINEAR

    # blender is ('REPEAT', 'EXTEND', 'CLIP')
    # glTF is CLAMP_TO_EDGE, MIRRORED_REPEAT, REPEAT
    wrap_type = GL_REPEAT if from_node.extension == "REPEAT" else GL_CLAMP_TO_EDGE

    return image_name, wrap_type, filter_type


def get_float_value(
    shader_node: Node,
    input_socket_name: str,
    min_value: float = -float_info.max,
    max_value: float = float_info.max,
) -> Optional[float]:
    socket = shader_node.inputs.get(input_socket_name)
    if not socket:
        return None

    default_value = convert.float_or_none(
        getattr(socket, "default_value", None), min_value, max_value
    )

    links = socket.links
    if not links:
        return default_value

    from_node = links[0].from_node
    if not from_node:
        return default_value

    outputs = from_node.outputs
    if not outputs:
        return default_value

    return convert.float_or_none(
        getattr(outputs[0], "default_value", None), min_value, max_value
    )


def get_float_value_or(
    shader_node: Node,
    input_socket_name: str,
    min_value: float = -float_info.max,
    max_value: float = float_info.max,
    default_value: float = 0.0,
) -> float:
    float_value = get_float_value(shader_node, input_socket_name, min_value, max_value)
    if float_value is None:
        return default_value
    return float_value


def rgba_or_none(
    vs: object, min_value: float = -float_info.max, max_value: float = float_info.max
) -> Optional[tuple[float, float, float, float]]:
    default_alpha_value = max(min_value, min(1.0, max_value))
    if isinstance(vs, Color):
        return (
            max(min_value, min(vs.r, max_value)),
            max(min_value, min(vs.g, max_value)),
            max(min_value, min(vs.b, max_value)),
            default_alpha_value,
        )

    iterator = convert.iterator_or_none(vs)
    if iterator is None:
        return None

    rgba: list[float] = []
    for v in iterator:
        f = convert.float_or_none(v, min_value, max_value)
        if f is None:
            return None
        rgba.append(f)
        if len(rgba) > 4:
            return None

    if len(rgba) == 3:
        rgba.append(default_alpha_value)
    if len(rgba) != 4:
        return None

    return (rgba[0], rgba[1], rgba[2], rgba[3])


def get_rgba_value(
    shader_node: Node,
    input_socket_name: str,
    min_value: float = -float_info.max,
    max_value: float = float_info.max,
) -> Optional[tuple[float, float, float, float]]:
    socket = shader_node.inputs.get(input_socket_name)
    if not socket:
        return None

    default_value = rgba_or_none(
        getattr(socket, "default_value", None), min_value, max_value
    )

    links = socket.links
    if not links:
        return default_value

    from_node = links[0].from_node
    if not from_node:
        return default_value

    outputs = from_node.outputs
    if not outputs:
        return default_value

    return rgba_or_none(
        getattr(outputs[0], "default_value", None), min_value, max_value
    )


def get_rgba_value_or(
    shader_node: Node,
    input_socket_name: str,
    min_value: float = -float_info.max,
    max_value: float = float_info.max,
    default_value: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
) -> tuple[float, float, float, float]:
    rgba_value = get_rgba_value(shader_node, input_socket_name, min_value, max_value)
    if rgba_value is None:
        return default_value
    return rgba_value


def rgb_or_none(
    vs: object, min_value: float = -float_info.max, max_value: float = float_info.max
) -> Optional[tuple[float, float, float]]:
    if isinstance(vs, Color):
        return (
            max(min_value, min(vs.r, max_value)),
            max(min_value, min(vs.g, max_value)),
            max(min_value, min(vs.b, max_value)),
        )

    iterator = convert.iterator_or_none(vs)
    if iterator is None:
        return None

    rgb: list[float] = []
    for v in iterator:
        f = convert.float_or_none(v, min_value, max_value)
        if f is None:
            return None
        rgb.append(f)
        if len(rgb) > 4:
            return None

    if len(rgb) == 4:
        rgb.pop()
    if len(rgb) != 3:
        return None

    return (rgb[0], rgb[1], rgb[2])


def get_rgb_value(
    shader_node: Node,
    input_socket_name: str,
    min_value: float = -float_info.max,
    max_value: float = float_info.max,
) -> Optional[tuple[float, float, float]]:
    socket = shader_node.inputs.get(input_socket_name)
    if not socket:
        return None

    default_value = rgb_or_none(
        getattr(socket, "default_value", None), min_value, max_value
    )

    links = socket.links
    if not links:
        return default_value

    from_node = links[0].from_node
    if not from_node:
        return default_value

    outputs = from_node.outputs
    if not outputs:
        return default_value

    return rgb_or_none(getattr(outputs[0], "default_value", None), min_value, max_value)


def get_rgb_value_or(
    shader_node: Node,
    input_socket_name: str,
    min_value: float = -float_info.max,
    max_value: float = float_info.max,
    default: tuple[float, float, float] = (0.0, 0.0, 0.0),
) -> tuple[float, float, float]:
    rgb_value = get_rgb_value(shader_node, input_socket_name, min_value, max_value)
    if rgb_value is None:
        return default
    return rgb_value


def setup_frame_count_driver(context: Context) -> None:
    if bpy.app.version < (3, 6):
        return

    node_group = context.blend_data.node_groups.get(UV_ANIMATION_GROUP_NAME)
    if not node_group:
        return
    animation_data = node_group.animation_data
    if not animation_data:
        animation_data = node_group.animation_data_create()
        if not animation_data:
            logger.error(
                'Failed to create anomation data for node group "%s"', node_group.name
            )
            return
    for data_path, target_data_path, enabled in [
        (
            f'nodes["{UV_ANIMATION_GROUP_FPS_BASE_NODE_NAME}"]'
            + ".outputs[0]"
            + ".default_value",
            "render.fps_base",
            True,
        ),
        (
            f'nodes["{UV_ANIMATION_GROUP_FPS_NODE_NAME}"]'
            + ".outputs[0]"
            + ".default_value",
            "render.fps",
            True,
        ),
        (
            f'nodes["{UV_ANIMATION_GROUP_FRAME_CURRENT_NODE_NAME}"]'
            + ".outputs[0]"
            + ".default_value",
            "frame_current",
            False,
        ),
    ]:
        fcurve: Optional[Union[FCurve, list[FCurve]]] = next(
            iter(
                fcurve
                for fcurve in animation_data.drivers
                if fcurve.data_path == data_path
            ),
            None,
        )
        if not enabled:
            if fcurve is not None:
                node_group.driver_remove(data_path)
            continue
        if fcurve is None:
            try:
                fcurve = node_group.driver_add(data_path)
            except TypeError:
                continue
        if not isinstance(fcurve, FCurve):
            logger.error(
                'Failed to get fcurve "%s" for node tree "%s"',
                data_path,
                node_group.name,
            )
            continue
        if fcurve.array_index != 0:
            fcurve.array_index = 0
        driver = fcurve.driver
        if not isinstance(driver, Driver):
            logger.error('Failed to get driver for fcurve "%s"', data_path)
            continue
        if driver.type != "SUM":
            driver.type = "SUM"
        if not driver.variables:
            driver.variables.new()
        while len(driver.variables) > 1:
            driver.variables.remove(driver.variables[-1])
        variable = driver.variables[0]
        if not variable.targets:
            logger.error(
                'No targets in variable for fcurve "%s" in node_tree "%s"',
                data_path,
                node_group.name,
            )
            continue
        target = variable.targets[0]
        if variable.type != "CONTEXT_PROP":
            variable.type = "CONTEXT_PROP"
        if target.context_property != "ACTIVE_SCENE":
            target.context_property = "ACTIVE_SCENE"
        if target.data_path != target_data_path:
            target.data_path = target_data_path


__NodeType = TypeVar("__NodeType", bound=Node)


def search_input_node(
    node_socket: NodeSocket, node_type: type[__NodeType]
) -> Optional[__NodeType]:
    searching_node_sockets = [node_socket]
    while searching_node_sockets:
        searching_node_socket = searching_node_sockets.pop()
        for link in searching_node_socket.links:
            if not link.is_valid or link.is_muted:
                continue

            from_node = link.from_node
            if from_node.mute:
                continue

            if isinstance(from_node, node_type):
                return from_node

            if not isinstance(from_node, NodeReroute):
                continue

            searching_node_sockets.extend(from_node.inputs)

    return None
