# SPDX-License-Identifier: GPL-2.0-or-later

bl_info = {
    "name": "Edit Linked Library",
    "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
    "version": (0, 9, 2),
    "blender": (2, 80, 0),
    "location": "File > External Data / View3D > Sidebar > Item Tab / Node Editor > Sidebar > Node Tab",
    "description": "Allows editing of objects, collections, and node groups linked from a .blend library.",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/object/edit_linked_library.html",
    "category": "Object",
}

import bpy
import logging
import os

from bpy.app.handlers import persistent

logger = logging.getLogger('object_edit_linked')

settings = {
    "original_file": "",
    "linked_file": "",
    "linked_objects": [],
    "linked_nodes": []
    }


@persistent
def linked_file_check(context: bpy.context):
    if settings["linked_file"] != "":
        if os.path.samefile(settings["linked_file"], bpy.data.filepath):
            logger.info("Editing a linked library.")
            bpy.ops.object.select_all(action='DESELECT')
            for ob_name in settings["linked_objects"]:
                bpy.data.objects[ob_name].select_set(True) # XXX Assumes selected object is in the active scene
            if len(settings["linked_objects"]) == 1:
                context.view_layer.objects.active = bpy.data.objects[settings["linked_objects"][0]]
        else:
            # For some reason, the linked editing session ended
            # (failed to find a file or opened a different file
            # before returning to the originating .blend)
            settings["original_file"] = ""
            settings["linked_file"] = ""


class OBJECT_OT_EditLinked(bpy.types.Operator):
    """Edit Linked Library"""
    bl_idname = "object.edit_linked"
    bl_label = "Edit Linked Library"

    use_autosave: bpy.props.BoolProperty(
            name="Autosave",
            description="Save the current file before opening the linked library",
            default=True)
    use_instance: bpy.props.BoolProperty(
            name="New Blender Instance",
            description="Open in a new Blender instance",
            default=False)

    @classmethod
    def poll(cls, context: bpy.context):
        return settings["original_file"] == "" and context.active_object is not None and (
                (context.active_object.instance_collection and
                context.active_object.instance_collection.library is not None) or
                context.active_object.library is not None or
                (context.active_object.override_library and
                context.active_object.override_library.reference.library is not None))

    def execute(self, context: bpy.context):
        target = context.active_object

        if target.instance_collection and target.instance_collection.library:
            targetpath = target.instance_collection.library.filepath
            settings["linked_objects"].extend({ob.name for ob in target.instance_collection.objects})
        elif target.library:
            targetpath = target.library.filepath
            settings["linked_objects"].append(target.name)
        elif target.override_library:
            target = target.override_library.reference
            targetpath = target.library.filepath
            settings["linked_objects"].append(target.name)

        if targetpath:
            logger.debug(target.name + " is linked to " + targetpath)

            if self.use_autosave:
                if not bpy.data.filepath:
                    # File is not saved on disk, better to abort!
                    self.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
                    return {'CANCELLED'}
                bpy.ops.wm.save_mainfile()

            settings["original_file"] = bpy.data.filepath
            # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
            settings["linked_file"] = os.path.abspath(bpy.path.abspath(targetpath))

            if self.use_instance:
                import subprocess
                try:
                    subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
                except:
                    logger.error("Error on the new Blender instance")
                    import traceback
                    logger.error(traceback.print_exc())
            else:
                bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])

            logger.info("Opened linked file!")
        else:
            self.report({'WARNING'}, target.name + " is not linked")
            logger.warning(target.name + " is not linked")

        return {'FINISHED'}


class NODE_OT_EditLinked(bpy.types.Operator):
    """Edit Linked Library"""
    bl_idname = "node.edit_linked"
    bl_label = "Edit Linked Library"

    use_autosave: bpy.props.BoolProperty(
            name="Autosave",
            description="Save the current file before opening the linked library",
            default=True)
    use_instance: bpy.props.BoolProperty(
            name="New Blender Instance",
            description="Open in a new Blender instance",
            default=False)

    @classmethod
    def poll(cls, context: bpy.context):
        return settings["original_file"] == "" and context.active_node is not None and (
                (context.active_node.type == 'GROUP' and
                hasattr(context.active_node.node_tree, "library") and
                context.active_node.node_tree.library is not None) or
                (hasattr(context.active_node, "monad") and
                context.active_node.monad.library is not None))

    def execute(self, context: bpy.context):
        target = context.active_node
        if (target.type == "GROUP"):
            target = target.node_tree
        else:
            target = target.monad

        targetpath = target.library.filepath
        settings["linked_nodes"].append(target.name)

        if targetpath:
            logger.debug(target.name + " is linked to " + targetpath)

            if self.use_autosave:
                if not bpy.data.filepath:
                    # File is not saved on disk, better to abort!
                    self.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
                    return {'CANCELLED'}
                bpy.ops.wm.save_mainfile()

            settings["original_file"] = bpy.data.filepath
            # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
            settings["linked_file"] = os.path.abspath(bpy.path.abspath(targetpath))

            if self.use_instance:
                import subprocess
                try:
                    subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
                except:
                    logger.error("Error on the new Blender instance")
                    import traceback
                    logger.error(traceback.print_exc())
            else:
                bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])

            logger.info("Opened linked file!")
        else:
            self.report({'WARNING'}, target.name + " is not linked")
            logger.warning(target.name + " is not linked")

        return {'FINISHED'}


class WM_OT_ReturnToOriginal(bpy.types.Operator):
    """Load the original file"""
    bl_idname = "wm.return_to_original"
    bl_label = "Return to Original File"

    use_autosave: bpy.props.BoolProperty(
            name="Autosave",
            description="Save the current file before opening original file",
            default=True)

    @classmethod
    def poll(cls, context: bpy.context):
        return (settings["original_file"] != "")

    def execute(self, context: bpy.context):
        if self.use_autosave:
            bpy.ops.wm.save_mainfile()

        bpy.ops.wm.open_mainfile(filepath=settings["original_file"])

        settings["original_file"] = ""
        settings["linked_objects"] = []
        logger.info("Back to the original!")
        return {'FINISHED'}


class VIEW3D_PT_PanelLinkedEdit(bpy.types.Panel):
    bl_label = "Edit Linked Library"
    bl_space_type = "VIEW_3D"
    bl_region_type = 'UI'
    bl_category = "Item"
    bl_context = 'objectmode'
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context: bpy.context):
        return (context.active_object is not None) or (settings["original_file"] != "")

    def draw_common(self, scene, layout, props):
        if props is not None:
            props.use_autosave = scene.use_autosave
            props.use_instance = scene.use_instance

            layout.prop(scene, "use_autosave")
            layout.prop(scene, "use_instance")

    def draw(self, context: bpy.context):
        scene = context.scene
        layout = self.layout
        layout.use_property_split = False
        layout.use_property_decorate = False
        icon = "OUTLINER_DATA_" + context.active_object.type.replace("LIGHT_PROBE", "LIGHTPROBE")

        target = None

        target = context.active_object.instance_collection

        if settings["original_file"] == "" and (
                (target and
                target.library is not None) or
                context.active_object.library is not None or
                (context.active_object.override_library is not None and
                context.active_object.override_library.reference is not None)):

            if (target is not None):
                props = layout.operator("object.edit_linked", icon="LINK_BLEND",
                                        text="Edit Library: %s" % target.name)
            elif (context.active_object.library):
                props = layout.operator("object.edit_linked", icon="LINK_BLEND",
                                        text="Edit Library: %s" % context.active_object.name)
            else:
                props = layout.operator("object.edit_linked", icon="LINK_BLEND",
                                        text="Edit Override Library: %s" % context.active_object.override_library.reference.name)

            self.draw_common(scene, layout, props)

            if (target is not None):
                layout.label(text="Path: %s" %
                            target.library.filepath)
            elif (context.active_object.library):
                layout.label(text="Path: %s" %
                            context.active_object.library.filepath)
            else:
                layout.label(text="Path: %s" %
                            context.active_object.override_library.reference.library.filepath)

        elif settings["original_file"] != "":

            if scene.use_instance:
                layout.operator("wm.return_to_original",
                                text="Reload Current File",
                                icon="FILE_REFRESH").use_autosave = False

                layout.separator()

                # XXX - This is for nested linked assets... but it only works
                # when launching a new Blender instance. Nested links don't
                # currently work when using a single instance of Blender.
                if context.active_object.instance_collection is not None:
                    props = layout.operator("object.edit_linked",
                            text="Edit Library: %s" % context.active_object.instance_collection.name,
                            icon="LINK_BLEND")
                else:
                    props = None

                self.draw_common(scene, layout, props)

                if context.active_object.instance_collection is not None:
                    layout.label(text="Path: %s" %
                            context.active_object.instance_collection.library.filepath)

            else:
                props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
                props.use_autosave = scene.use_autosave

                layout.prop(scene, "use_autosave")

        else:
            layout.label(text="%s is not linked" % context.active_object.name,
                        icon=icon)


class NODE_PT_PanelLinkedEdit(bpy.types.Panel):
    bl_label = "Edit Linked Library"
    bl_space_type = 'NODE_EDITOR'
    bl_region_type = 'UI'
    if bpy.app.version >= (2, 93, 0):
        bl_category = "Node"
    else:
        bl_category = "Item"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        return context.active_node is not None

    def draw_common(self, scene, layout, props):
        if props is not None:
            props.use_autosave = scene.use_autosave
            props.use_instance = scene.use_instance

            layout.prop(scene, "use_autosave")
            layout.prop(scene, "use_instance")

    def draw(self, context):
        scene = context.scene
        layout = self.layout
        layout.use_property_split = False
        layout.use_property_decorate = False
        icon = 'NODETREE'

        target = context.active_node

        if settings["original_file"] == "" and (
                (target.type == 'GROUP' and hasattr(target.node_tree, "library") and
                target.node_tree.library is not None) or
                (hasattr(target, "monad") and target.monad.library is not None)):

            if (target.type == "GROUP"):
                props = layout.operator("node.edit_linked", icon="LINK_BLEND",
                                        text="Edit Library: %s" % target.name)
            else:
                props = layout.operator("node.edit_linked", icon="LINK_BLEND",
                                        text="Edit Library: %s" % target.monad.name)

            self.draw_common(scene, layout, props)

            if (target.type == "GROUP"):
                layout.label(text="Path: %s" % target.node_tree.library.filepath)
            else:
                layout.label(text="Path: %s" % target.monad.library.filepath)

        elif settings["original_file"] != "":

            if scene.use_instance:
                layout.operator("wm.return_to_original",
                                text="Reload Current File",
                                icon="FILE_REFRESH").use_autosave = False

                layout.separator()

                props = None

                self.draw_common(scene, layout, props)

                #layout.label(text="Path: %s" %
                #            context.active_object.instance_collection.library.filepath)

            else:
                props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
                props.use_autosave = scene.use_autosave

                layout.prop(scene, "use_autosave")

        else:
            layout.label(text="%s is not linked" % target.name, icon=icon)


class TOPBAR_MT_edit_linked_submenu(bpy.types.Menu):
    bl_label = 'Edit Linked Library'

    def draw(self, context):
        self.layout.separator()
        self.layout.operator(OBJECT_OT_EditLinked.bl_idname)
        self.layout.operator(WM_OT_ReturnToOriginal.bl_idname)


addon_keymaps = []
classes = (
    OBJECT_OT_EditLinked,
    NODE_OT_EditLinked,
    WM_OT_ReturnToOriginal,
    VIEW3D_PT_PanelLinkedEdit,
    NODE_PT_PanelLinkedEdit,
    TOPBAR_MT_edit_linked_submenu
    )


def register():
    bpy.app.handlers.load_post.append(linked_file_check)

    for c in classes:
        bpy.utils.register_class(c)

    bpy.types.Scene.use_autosave = bpy.props.BoolProperty(
            name="Autosave",
            description="Save the current file before opening a linked file",
            default=True)

    bpy.types.Scene.use_instance = bpy.props.BoolProperty(
            name="New Blender Instance",
            description="Open in a new Blender instance",
            default=False)

    # add the function to the file menu
    bpy.types.TOPBAR_MT_file_external_data.append(TOPBAR_MT_edit_linked_submenu.draw)




def unregister():

    bpy.app.handlers.load_post.remove(linked_file_check)
    bpy.types.TOPBAR_MT_file_external_data.remove(TOPBAR_MT_edit_linked_submenu)

    del bpy.types.Scene.use_autosave
    del bpy.types.Scene.use_instance


    for c in reversed(classes):
        bpy.utils.unregister_class(c)


if __name__ == "__main__":
    register()
