# SPDX-FileCopyrightText: 2020-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

import bpy
from bpy.types import Operator
from bpy.props import IntProperty

from bpy.app.translations import pgettext_data as data_

from bpy.props import (
    EnumProperty,
)


def add_empty_geometry_node_group(name):
    group = bpy.data.node_groups.new(name, 'GeometryNodeTree')

    group.interface.new_socket(data_("Geometry"), in_out='INPUT', socket_type='NodeSocketGeometry')
    input_node = group.nodes.new('NodeGroupInput')
    input_node.select = False
    input_node.location.x = -200 - input_node.width

    group.interface.new_socket(data_("Geometry"), in_out='OUTPUT', socket_type='NodeSocketGeometry')
    output_node = group.nodes.new('NodeGroupOutput')
    output_node.is_active_output = True
    output_node.select = False
    output_node.location.x = 200

    return group


def geometry_node_group_empty_new(name):
    group = add_empty_geometry_node_group(name)
    group.links.new(group.nodes[data_("Group Input")].outputs[0], group.nodes[data_("Group Output")].inputs[0])
    return group


def geometry_node_group_empty_modifier_new(name):
    group = geometry_node_group_empty_new(data_("Geometry Nodes"))
    group.is_modifier = True
    return group


def geometry_node_group_empty_tool_new(context):
    group = geometry_node_group_empty_new(data_("Tool"))
    # Node tools have fake users by default, otherwise Blender will delete them since they have no users.
    group.use_fake_user = True
    group.is_tool = True

    ob_type = context.object.type if context.object else 'MESH'
    if ob_type == 'CURVES':
        group.is_type_curve = True
    elif ob_type == 'POINTCLOUD':
        group.is_type_point_cloud = True
    else:
        group.is_type_mesh = True

    mode = context.object.mode if context.object else 'OBJECT'
    if mode in {'SCULPT', 'SCULPT_CURVES'}:
        group.is_mode_sculpt = True
    elif mode == 'EDIT':
        group.is_mode_edit = True
    else:
        group.is_mode_object = True

    return group


def geometry_modifier_poll(context):
    ob = context.object

    # Test object support for geometry node modifier
    if not ob or ob.type not in {'MESH', 'POINTCLOUD', 'VOLUME', 'CURVE', 'FONT', 'CURVES', 'GREASEPENCIL'}:
        return False

    return True


def get_context_modifier(context):
    # Context only has a "modifier" attribute in the modifier extra operators drop-down.
    modifier = getattr(context, "modifier", ...)
    if modifier is ...:
        ob = context.object
        if ob is None:
            return False
        modifier = ob.modifiers.active
    if modifier is None or modifier.type != 'NODES':
        return None
    return modifier


def edit_geometry_nodes_modifier_poll(context):
    return get_context_modifier(context) is not None


def socket_idname_to_attribute_type(idname):
    if idname.startswith("NodeSocketInt"):
        return 'INT'
    elif idname.startswith("NodeSocketColor"):
        return 'FLOAT_COLOR'
    elif idname.startswith("NodeSocketVector"):
        return 'FLOAT_VECTOR'
    elif idname.startswith("NodeSocketBool"):
        return 'BOOLEAN'
    elif idname.startswith("NodeSocketFloat"):
        return 'FLOAT'
    raise ValueError("Unsupported socket type")


def modifier_attribute_name_get(modifier, identifier):
    try:
        return modifier[identifier + "_attribute_name"]
    except KeyError:
        return None


def modifier_input_use_attribute(modifier, identifier):
    try:
        return modifier[identifier + "_use_attribute"] != 0
    except KeyError:
        return False


def get_socket_with_identifier(sockets, identifier):
    for socket in sockets:
        if socket.identifier == identifier:
            return socket
    return None


def get_enabled_socket_with_name(sockets, name):
    for socket in sockets:
        if socket.name == name and socket.enabled:
            return socket
    return None


class MoveModifierToNodes(Operator):
    """Move inputs and outputs from in the modifier to a new node group"""

    bl_idname = "object.geometry_nodes_move_to_nodes"
    bl_label = "Move to Nodes"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return edit_geometry_nodes_modifier_poll(context)

    def execute(self, context):
        modifier = get_context_modifier(context)
        if not modifier:
            return {'CANCELLED'}
        old_group = modifier.node_group
        if not old_group:
            return {'CANCELLED'}

        wrapper_name = old_group.name + ".wrapper"
        group = bpy.data.node_groups.new(wrapper_name, 'GeometryNodeTree')
        group.interface.new_socket(data_("Geometry"), in_out='OUTPUT', socket_type='NodeSocketGeometry')
        group.is_modifier = True

        first_geometry_input = next((item for item in old_group.interface.items_tree if item.item_type == 'SOCKET' and
                                    item.in_out == 'INPUT' and
                                    item.bl_socket_idname == 'NodeSocketGeometry'), None)
        if first_geometry_input:
            group.interface.new_socket(data_("Geometry"), in_out='INPUT', socket_type='NodeSocketGeometry')
            group_input_node = group.nodes.new('NodeGroupInput')
            group_input_node.location.x = -200 - group_input_node.width
            group_input_node.select = False

        group_output_node = group.nodes.new('NodeGroupOutput')
        group_output_node.is_active_output = True
        group_output_node.location.x = 200
        group_output_node.select = False

        group_node = group.nodes.new("GeometryNodeGroup")
        group_node.node_tree = old_group
        group_node.update()

        # Copy default values for inputs and create named attribute input nodes.
        input_nodes = []
        for input_socket in old_group.interface.items_tree:
            if input_socket.item_type != 'SOCKET' or (input_socket.in_out not in {'INPUT', 'BOTH'}):
                continue
            identifier = input_socket.identifier
            group_node_input = get_socket_with_identifier(group_node.inputs, identifier)
            if modifier_input_use_attribute(modifier, identifier):
                input_node = group.nodes.new("GeometryNodeInputNamedAttribute")
                input_nodes.append(input_node)
                input_node.data_type = socket_idname_to_attribute_type(input_socket.bl_socket_idname)
                attribute_name = modifier_attribute_name_get(modifier, identifier)
                input_node.inputs["Name"].default_value = attribute_name
                output_socket = get_enabled_socket_with_name(input_node.outputs, "Attribute")
                group.links.new(output_socket, group_node_input)
            elif hasattr(input_socket, "default_value"):
                group_node_input.default_value = modifier[identifier]

        if first_geometry_input:
            group.links.new(group_input_node.outputs[0],
                            get_socket_with_identifier(group_node.inputs, first_geometry_input.identifier))

            # Adjust locations of named attribute input nodes and group input node to make some space.
            if input_nodes:
                for i, node in enumerate(input_nodes):
                    node.location.x = -175
                    node.location.y = i * -50
                group_input_node.location.x = -350

        # Connect outputs to store named attribute nodes to replace modifier attribute outputs.
        store_nodes = []
        first_geometry_output = None
        for output_socket in old_group.interface.items_tree:
            if output_socket.item_type != 'SOCKET' or (output_socket.in_out not in {'OUTPUT', 'BOTH'}):
                continue
            identifier = output_socket.identifier
            group_node_output = get_socket_with_identifier(group_node.outputs, identifier)
            attribute_name = modifier_attribute_name_get(modifier, identifier)
            if attribute_name:
                store_node = group.nodes.new("GeometryNodeStoreNamedAttribute")
                store_nodes.append(store_node)
                store_node.data_type = socket_idname_to_attribute_type(output_socket.bl_socket_idname)
                store_node.domain = output_socket.attribute_domain
                store_node.inputs["Name"].default_value = attribute_name
                input_socket = get_enabled_socket_with_name(store_node.inputs, "Value")
                group.links.new(group_node_output, input_socket)
            elif output_socket.bl_socket_idname == 'NodeSocketGeometry':
                if not first_geometry_output:
                    first_geometry_output = group_node_output

        # Adjust locations of store named attribute nodes and move group output.
        # Note that the node group has its sockets names translated, while the built-in nodes don't.
        if store_nodes:
            for i, node in enumerate(store_nodes):
                node.location.x = (i + 1) * 175
                node.location.y = 0
            group_output_node.location.x = (len(store_nodes) + 1) * 175

            group.links.new(first_geometry_output, store_nodes[0].inputs["Geometry"])
            for i in range(len(store_nodes) - 1):
                group.links.new(store_nodes[i].outputs["Geometry"], store_nodes[i + 1].inputs["Geometry"])

            group.links.new(store_nodes[-1].outputs["Geometry"], group_output_node.inputs[data_("Geometry")])
        else:
            if not first_geometry_output:
                self.report({'WARNING'}, "Node group must have a geometry output")
                return {'CANCELLED'}
            group.links.new(first_geometry_output, group_output_node.inputs[data_("Geometry")])

        modifier.node_group = group

        return {'FINISHED'}


class NewGeometryNodesModifier(Operator):
    """Create a new modifier with a new geometry node group"""

    bl_idname = "node.new_geometry_nodes_modifier"
    bl_label = "New Geometry Node Modifier"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return geometry_modifier_poll(context)

    def execute(self, context):
        ob = context.object
        modifier = ob.modifiers.new(data_("GeometryNodes"), 'NODES')
        if not modifier:
            return {'CANCELLED'}

        group = geometry_node_group_empty_modifier_new(data_("Geometry Nodes"))
        modifier.node_group = group

        return {'FINISHED'}


class NewGeometryNodeTreeAssign(Operator):
    """Create a new geometry node group and assign it to the active modifier"""

    bl_idname = "node.new_geometry_node_group_assign"
    bl_label = "Assign New Geometry Node Group"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return geometry_modifier_poll(context)

    def execute(self, context):
        modifier = get_context_modifier(context)
        if not modifier:
            return {'CANCELLED'}
        group = geometry_node_group_empty_modifier_new(data_("Geometry Nodes"))
        modifier.node_group = group

        return {'FINISHED'}


class NewGeometryNodeGroupTool(Operator):
    """Create a new geometry node group for a tool"""
    bl_idname = "node.new_geometry_node_group_tool"
    bl_label = "New Geometry Node Tool Group"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        space = context.space_data
        return space and space.type == 'NODE_EDITOR' and space.geometry_nodes_type == 'TOOL'

    def execute(self, context):
        group = geometry_node_group_empty_tool_new(context)
        context.space_data.geometry_nodes_tool_tree = group
        return {'FINISHED'}


class ZoneOperator:
    @classmethod
    def get_node(cls, context):
        node = context.active_node
        if node is None:
            return None
        if node.bl_idname == cls.output_node_type:
            return node
        if node.bl_idname == cls.input_node_type:
            return node.paired_output

    @classmethod
    def poll(cls, context):
        space = context.space_data
        # Needs active node editor and a tree.
        if not space or space.type != 'NODE_EDITOR' or not space.edit_tree or space.edit_tree.library:
            return False
        if cls.get_node(context) is None:
            return False
        return True


class NodeOperator:
    @classmethod
    def get_node(cls, context):
        node = context.active_node
        if node is None:
            return None
        if node.bl_idname == cls.node_type:
            return node

    @classmethod
    def poll(cls, context):
        space = context.space_data
        # Needs active node editor and a tree.
        if not space or space.type != 'NODE_EDITOR' or not space.edit_tree or space.edit_tree.library:
            return False
        node = cls.get_node(context)
        if node is None:
            return False
        return True


class SocketItemAddOperator:
    items_name = None
    active_index_name = None
    default_socket_type = 'GEOMETRY'

    def execute(self, context):
        node = self.get_node(context)
        items = getattr(node, self.items_name)
        # Remember index to move the item.
        old_active_index = getattr(node, self.active_index_name)
        if 0 <= old_active_index < len(items):
            old_active_item = items[old_active_index]
            dst_index = old_active_index + 1
            dst_type = old_active_item.socket_type
            dst_name = old_active_item.name
        else:
            dst_index = len(items)
            dst_type = self.default_socket_type
            # Empty name so it is based on the type.
            dst_name = ""
        items.new(dst_type, dst_name)
        items.move(len(items) - 1, dst_index)
        setattr(node, self.active_index_name, dst_index)
        return {'FINISHED'}


class SocketItemRemoveOperator:
    items_name = None
    active_index_name = None

    def execute(self, context):
        node = self.get_node(context)
        items = getattr(node, self.items_name)
        old_active_index = getattr(node, self.active_index_name)

        if 0 <= old_active_index < len(items):
            items.remove(items[old_active_index])

        return {'FINISHED'}


class SocketMoveItemOperator:
    items_name = None
    active_index_name = None

    direction: EnumProperty(
        name="Direction",
        items=[('UP', "Up", ""), ('DOWN', "Down", "")],
        default='UP',
    )

    def execute(self, context):
        node = self.get_node(context)
        items = getattr(node, self.items_name)
        old_active_index = getattr(node, self.active_index_name)

        if self.direction == 'UP' and old_active_index > 0:
            items.move(old_active_index, old_active_index - 1)
            setattr(node, self.active_index_name, old_active_index - 1)
        elif self.direction == 'DOWN' and old_active_index < len(items) - 1:
            items.move(old_active_index, old_active_index + 1)
            setattr(node, self.active_index_name, old_active_index + 1)

        return {'FINISHED'}


class SimulationZoneOperator(ZoneOperator):
    input_node_type = 'GeometryNodeSimulationInput'
    output_node_type = 'GeometryNodeSimulationOutput'

    items_name = "state_items"
    active_index_name = "active_index"


class SimulationZoneItemAddOperator(SimulationZoneOperator, SocketItemAddOperator, Operator):
    """Add a state item to the simulation zone"""
    bl_idname = "node.simulation_zone_item_add"
    bl_label = "Add State Item"
    bl_options = {'REGISTER', 'UNDO'}


class SimulationZoneItemRemoveOperator(SimulationZoneOperator, SocketItemRemoveOperator, Operator):
    """Remove a state item from the simulation zone"""
    bl_idname = "node.simulation_zone_item_remove"
    bl_label = "Remove State Item"
    bl_options = {'REGISTER', 'UNDO'}


class SimulationZoneItemMoveOperator(SimulationZoneOperator, SocketMoveItemOperator, Operator):
    """Move a simulation state item up or down in the list"""
    bl_idname = "node.simulation_zone_item_move"
    bl_label = "Move State Item"
    bl_options = {'REGISTER', 'UNDO'}


class RepeatZoneOperator(ZoneOperator):
    input_node_type = 'GeometryNodeRepeatInput'
    output_node_type = 'GeometryNodeRepeatOutput'

    items_name = "repeat_items"
    active_index_name = "active_index"


class RepeatZoneItemAddOperator(RepeatZoneOperator, SocketItemAddOperator, Operator):
    """Add a repeat item to the repeat zone"""
    bl_idname = "node.repeat_zone_item_add"
    bl_label = "Add Repeat Item"
    bl_options = {'REGISTER', 'UNDO'}


class RepeatZoneItemRemoveOperator(RepeatZoneOperator, SocketItemRemoveOperator, Operator):
    """Remove a repeat item from the repeat zone"""
    bl_idname = "node.repeat_zone_item_remove"
    bl_label = "Remove Repeat Item"
    bl_options = {'REGISTER', 'UNDO'}


class RepeatZoneItemMoveOperator(RepeatZoneOperator, SocketMoveItemOperator, Operator):
    """Move a repeat item up or down in the list"""
    bl_idname = "node.repeat_zone_item_move"
    bl_label = "Move Repeat Item"
    bl_options = {'REGISTER', 'UNDO'}


class BakeNodeOperator(NodeOperator):
    node_type = 'GeometryNodeBake'

    items_name = "bake_items"
    active_index_name = "active_index"


class BakeNodeItemAddOperator(BakeNodeOperator, SocketItemAddOperator, Operator):
    """Add a bake item to the bake node"""
    bl_idname = "node.bake_node_item_add"
    bl_label = "Add Bake Item"
    bl_options = {'REGISTER', 'UNDO'}


class BakeNodeItemRemoveOperator(BakeNodeOperator, SocketItemRemoveOperator, Operator):
    """Remove a bake item from the bake node"""
    bl_idname = "node.bake_node_item_remove"
    bl_label = "Remove Bake Item"
    bl_options = {'REGISTER', 'UNDO'}


class BakeNodeItemMoveOperator(BakeNodeOperator, SocketMoveItemOperator, Operator):
    """Move a bake item up or down in the list"""
    bl_idname = "node.bake_node_item_move"
    bl_label = "Move Bake Item"
    bl_options = {'REGISTER', 'UNDO'}


def _editable_tree_with_active_node_type(context, node_type):
    space = context.space_data
    # Needs active node editor and a tree.
    if not space or space.type != 'NODE_EDITOR' or not space.edit_tree or space.edit_tree.library:
        return False
    node = context.active_node
    if node is None or node.bl_idname != node_type:
        return False
    return True


class IndexSwitchItemAddOperator(Operator):
    """Add an item to the index switch"""
    bl_idname = "node.index_switch_item_add"
    bl_label = "Add Item"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return _editable_tree_with_active_node_type(context, 'GeometryNodeIndexSwitch')

    def execute(self, context):
        node = context.active_node
        node.index_switch_items.new()
        return {'FINISHED'}


class IndexSwitchItemRemoveOperator(Operator):
    """Remove an item from the index switch"""
    bl_idname = "node.index_switch_item_remove"
    bl_label = "Remove Item"
    bl_options = {'REGISTER', 'UNDO'}

    index: IntProperty(
        name="Index",
        description="Index of item to remove",
    )

    @classmethod
    def poll(cls, context):
        return _editable_tree_with_active_node_type(context, 'GeometryNodeIndexSwitch')

    def execute(self, context):
        node = context.active_node
        items = node.index_switch_items
        items.remove(items[self.index])
        return {'FINISHED'}


classes = (
    NewGeometryNodesModifier,
    NewGeometryNodeTreeAssign,
    NewGeometryNodeGroupTool,
    MoveModifierToNodes,
    SimulationZoneItemAddOperator,
    SimulationZoneItemRemoveOperator,
    SimulationZoneItemMoveOperator,
    RepeatZoneItemAddOperator,
    RepeatZoneItemRemoveOperator,
    RepeatZoneItemMoveOperator,
    BakeNodeItemAddOperator,
    BakeNodeItemRemoveOperator,
    BakeNodeItemMoveOperator,
    IndexSwitchItemAddOperator,
    IndexSwitchItemRemoveOperator,
)
