import os
import shutil
from typing import Any, Type

import bpy

import arm.assets
from arm.exporter import ArmoryExporter
import arm.log as log
from arm.logicnode.arm_nodes import ArmLogicTreeNode
import arm.make as make
import arm.make_state as state
import arm.node_utils
import arm.utils

if arm.is_reload(__name__):
    arm.assets = arm.reload_module(arm.assets)
    arm.exporter = arm.reload_module(arm.exporter)
    from arm.exporter import ArmoryExporter
    log = arm.reload_module(log)
    arm.logicnode.arm_nodes = arm.reload_module(arm.logicnode.arm_nodes)
    from arm.logicnode.arm_nodes import ArmLogicTreeNode
    make = arm.reload_module(make)
    state = arm.reload_module(state)
    arm.node_utils = arm.reload_module(arm.node_utils)
    arm.utils = arm.reload_module(arm.utils)
else:
    arm.enable_reload(__name__)

    patch_id = 0
    """Current patch id"""

    __running = False
    """Whether live patch is currently active"""

# Any object can act as a message bus owner
msgbus_owner = object()


def start():
    """Start the live patch session."""
    log.debug("Live patch session started")

    listen(bpy.types.Object, "location", "obj_location")
    listen(bpy.types.Object, "rotation_euler", "obj_rotation")
    listen(bpy.types.Object, "scale", "obj_scale")

    # 'energy' is defined in sub classes only, also workaround for
    # https://developer.blender.org/T88408
    for light_type in (bpy.types.AreaLight, bpy.types.PointLight, bpy.types.SpotLight, bpy.types.SunLight):
        listen(light_type, "color", "light_color")
        listen(light_type, "energy", "light_energy")

    global __running
    __running = True


def stop():
    """Stop the live patch session."""
    global __running, patch_id
    if __running:
        __running = False
        patch_id = 0

        log.debug("Live patch session stopped")
        bpy.msgbus.clear_by_owner(msgbus_owner)


def patch_export():
    """Re-export the current scene and update the game accordingly."""
    if not __running or state.proc_build is not None:
        return

    arm.assets.invalidate_enabled = False

    with arm.utils.WorkingDir(arm.utils.get_fp()):
        asset_path = arm.utils.get_fp_build() + '/compiled/Assets/' + arm.utils.safestr(bpy.context.scene.name) + '.arm'
        ArmoryExporter.export_scene(bpy.context, asset_path, scene=bpy.context.scene)

        dir_std_shaders_dst = os.path.join(arm.utils.build_dir(), 'compiled', 'Shaders', 'std')
        if not os.path.isdir(dir_std_shaders_dst):
            dir_std_shaders_src = os.path.join(arm.utils.get_sdk_path(), 'armory', 'Shaders', 'std')
            shutil.copytree(dir_std_shaders_src, dir_std_shaders_dst)

        node_path = arm.utils.get_node_path()
        khamake_path = arm.utils.get_khamake_path()
        cmd = [
            node_path, khamake_path, 'krom',
            '--shaderversion', '330',
            '--parallelAssetConversion', '4',
            '--to', arm.utils.build_dir() + '/debug',
            '--nohaxe',
            '--noproject'
        ]

        arm.assets.invalidate_enabled = True
        state.proc_build = make.run_proc(cmd, patch_done)


def patch_done():
    """Signal Iron to reload the running scene after a re-export."""
    js = 'iron.Scene.patch();'
    write_patch(js)
    state.proc_build = None


def write_patch(js: str):
    """Write the given javascript code to 'krom.patch'."""
    global patch_id
    with open(arm.utils.get_fp_build() + '/debug/krom/krom.patch', 'w') as f:
        patch_id += 1
        f.write(str(patch_id) + '\n')
        f.write(js)


def listen(rna_type: Type[bpy.types.bpy_struct], prop: str, event_id: str):
    """Subscribe to '<rna_type>.<prop>'. The event_id can be choosen
    freely but must match with the id used in send_event().
    """
    bpy.msgbus.subscribe_rna(
        key=(rna_type, prop),
        owner=msgbus_owner,
        args=(event_id, ),
        notify=send_event
        # options={"PERSISTENT"}
    )


def send_event(event_id: str, opt_data: Any = None):
    """Send the result of the given event to Krom."""
    if not __running:
        return

    if hasattr(bpy.context, 'object') and bpy.context.object is not None:
        obj = bpy.context.object.name

        if bpy.context.object.mode == "OBJECT":
            if event_id == "obj_location":
                vec = bpy.context.object.location
                js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.loc.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;'
                write_patch(js)

            elif event_id == 'obj_scale':
                vec = bpy.context.object.scale
                js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.scale.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;'
                write_patch(js)

            elif event_id == 'obj_rotation':
                vec = bpy.context.object.rotation_euler.to_quaternion()
                js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.rot.set({vec[1]}, {vec[2]}, {vec[3]}, {vec[0]}); o.transform.dirty = true;'
                write_patch(js)

            elif event_id == 'light_color':
                light: bpy.types.Light = bpy.context.object.data
                vec = light.color
                js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.color[0]={vec[0]}; lRaw.color[1]={vec[1]}; lRaw.color[2]={vec[2]};'
                write_patch(js)

            elif event_id == 'light_energy':
                light: bpy.types.Light = bpy.context.object.data

                # Align strength to Armory, see exporter.export_light()
                # TODO: Use exporter.export_light() and simply reload all raw light data in Iron?
                strength_fac = 1.0
                if light.type == 'SUN':
                    strength_fac = 0.325
                elif light.type in ('POINT', 'SPOT', 'AREA'):
                    strength_fac = 0.01

                js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.strength={light.energy * strength_fac};'
                write_patch(js)

        else:
            patch_export()

    if event_id == 'ln_insert_link':
        node: ArmLogicTreeNode
        link: bpy.types.NodeLink
        node, link = opt_data

        # This event is called twice for a connection but we only need
        # send it once
        if node == link.from_node:
            tree_name = arm.node_utils.get_export_tree_name(node.get_tree())

            # [1:] is used here because make_logic already uses that for
            # node names if arm_debug is used
            from_node_name = arm.node_utils.get_export_node_name(node)[1:]
            to_node_name = arm.node_utils.get_export_node_name(link.to_node)[1:]

            from_index = arm.node_utils.get_socket_index(node.outputs, link.from_socket)
            to_index = arm.node_utils.get_socket_index(link.to_node.inputs, link.to_socket)

            js = f'LivePatch.patchCreateNodeLink("{tree_name}", "{from_node_name}", "{to_node_name}", "{from_index}", "{to_index}");'
            write_patch(js)

    elif event_id == 'ln_update_prop':
        node: ArmLogicTreeNode
        prop_name: str
        node, prop_name = opt_data

        tree_name = arm.node_utils.get_export_tree_name(node.get_tree())
        node_name = arm.node_utils.get_export_node_name(node)[1:]

        value = arm.node_utils.haxe_format_prop_value(node, prop_name)

        if prop_name.endswith('_get'):
            # Hack because some nodes use a different Python property
            # name than they use in Haxe
            prop_name = prop_name[:-4]

        js = f'LivePatch.patchUpdateNodeProp("{tree_name}", "{node_name}", "{prop_name}", {value});'
        write_patch(js)

    elif event_id == 'ln_socket_val':
        node: ArmLogicTreeNode
        socket: bpy.types.NodeSocket
        node, socket = opt_data

        socket_index = arm.node_utils.get_socket_index(node.inputs, socket)

        if socket_index != -1:
            tree_name = arm.node_utils.get_export_tree_name(node.get_tree())
            node_name = arm.node_utils.get_export_node_name(node)[1:]

            value = socket.get_default_value()
            inp_type = socket.arm_socket_type

            if inp_type in ('VECTOR', 'RGB'):
                value = f'new iron.Vec4({arm.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)}, 1.0)'
            elif inp_type == 'RGBA':
                value = f'new iron.Vec4({arm.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)})'
            elif inp_type == 'OBJECT':
                value = f'iron.Scene.active.getChild("{value}")' if value != '' else 'null'
            else:
                value = arm.node_utils.haxe_format_socket_val(value)

            js = f'LivePatch.patchUpdateNodeInputVal("{tree_name}", "{node_name}", {socket_index}, {value});'
            write_patch(js)

    elif event_id == 'ln_create':
        node: ArmLogicTreeNode = opt_data

        tree_name = arm.node_utils.get_export_tree_name(node.get_tree())
        node_name = arm.node_utils.get_export_node_name(node)[1:]
        node_type = 'armory.logicnode.' + node.bl_idname[2:]

        prop_names = list(arm.node_utils.get_haxe_property_names(node))
        prop_py_names, prop_hx_names = zip(*prop_names) if len(prop_names) > 0 else ([], [])
        prop_values = (getattr(node, prop_name) for prop_name in prop_py_names)
        prop_datas = arm.node_utils.haxe_format_socket_val(list(zip(prop_hx_names, prop_values)))

        inp_data = [(inp.arm_socket_type, inp.get_default_value()) for inp in node.inputs]
        inp_data = arm.node_utils.haxe_format_socket_val(inp_data)
        out_data = [(out.arm_socket_type, out.get_default_value()) for out in node.outputs]
        out_data = arm.node_utils.haxe_format_socket_val(out_data)

        js = f'LivePatch.patchNodeCreate("{tree_name}", "{node_name}", "{node_type}", {prop_datas}, {inp_data}, {out_data});'
        write_patch(js)

    elif event_id == 'ln_delete':
        node: ArmLogicTreeNode = opt_data

        tree_name = arm.node_utils.get_export_tree_name(node.get_tree())
        node_name = arm.node_utils.get_export_node_name(node)[1:]

        js = f'LivePatch.patchNodeDelete("{tree_name}", "{node_name}");'
        write_patch(js)

    elif event_id == 'ln_copy':
        newnode: ArmLogicTreeNode
        node: ArmLogicTreeNode
        newnode, node = opt_data

        # Use newnode to get the tree, node has no id_data at this moment
        tree_name = arm.node_utils.get_export_tree_name(newnode.get_tree())

        newnode_name = arm.node_utils.get_export_node_name(newnode)[1:]
        node_name = arm.node_utils.get_export_node_name(node)[1:]

        props_list = '[' + ','.join(f'"{p}"' for _, p in arm.node_utils.get_haxe_property_names(node)) + ']'

        inp_data = [(inp.arm_socket_type, inp.get_default_value()) for inp in newnode.inputs]
        inp_data = arm.node_utils.haxe_format_socket_val(inp_data)
        out_data = [(out.arm_socket_type, out.get_default_value()) for out in newnode.outputs]
        out_data = arm.node_utils.haxe_format_socket_val(out_data)

        js = f'LivePatch.patchNodeCopy("{tree_name}", "{node_name}", "{newnode_name}", {props_list}, {inp_data}, {out_data});'
        write_patch(js)

    elif event_id == 'ln_update_sockets':
        node: ArmLogicTreeNode = opt_data

        tree_name = arm.node_utils.get_export_tree_name(node.get_tree())
        node_name = arm.node_utils.get_export_node_name(node)[1:]

        inp_data = '['
        for idx, inp in enumerate(node.inputs):
            inp_data += '{'
            # is_linked can be true even if there are no links if the
            # user starts dragging a connection away before releasing
            # the mouse
            if inp.is_linked and len(inp.links) > 0:
                inp_data += 'isLinked: true,'
                inp_data += f'fromNode: "{arm.node_utils.get_export_node_name(inp.links[0].from_node)[1:]}",'
                inp_data += f'fromIndex: {arm.node_utils.get_socket_index(inp.links[0].from_node.outputs, inp.links[0].from_socket)},'
            else:
                inp_data += 'isLinked: false,'
                inp_data += f'socketType: "{inp.arm_socket_type}",'
                inp_data += f'socketValue: {arm.node_utils.haxe_format_socket_val(inp.get_default_value())},'

            inp_data += f'toIndex: {idx}'
            inp_data += '},'
        inp_data += ']'

        out_data = '['
        for idx, out in enumerate(node.outputs):
            out_data += '['
            for link in out.links:
                out_data += '{'
                if out.is_linked:
                    out_data += 'isLinked: true,'
                    out_data += f'toNode: "{arm.node_utils.get_export_node_name(link.to_node)[1:]}",'
                    out_data += f'toIndex: {arm.node_utils.get_socket_index(link.to_node.inputs, link.to_socket)},'
                else:
                    out_data += 'isLinked: false,'
                    out_data += f'socketType: "{out.arm_socket_type}",'
                    out_data += f'socketValue: {arm.node_utils.haxe_format_socket_val(out.get_default_value())},'

                out_data += f'fromIndex: {idx}'
                out_data += '},'
            out_data += '],'
        out_data += ']'

        js = f'LivePatch.patchSetNodeLinks("{tree_name}", "{node_name}", {inp_data}, {out_data});'
        write_patch(js)


def on_operator(operator_id: str):
    """As long as bpy.msgbus doesn't listen to changes made by
    operators (*), additionally notify the callback manually.

    (*) https://developer.blender.org/T72109
    """
    if not __running:
        return

    if operator_id in IGNORE_OPERATORS:
        return

    if operator_id == 'TRANSFORM_OT_translate':
        send_event('obj_location')
    elif operator_id in ('TRANSFORM_OT_rotate', 'TRANSFORM_OT_trackball'):
        send_event('obj_rotation')
    elif operator_id == 'TRANSFORM_OT_resize':
        send_event('obj_scale')

    # Rebuild
    else:
        patch_export()


# Don't re-export the scene for the following operators
IGNORE_OPERATORS = (
    'ARM_OT_node_add_input',
    'ARM_OT_node_add_input_output',
    'ARM_OT_node_add_input_value',
    'ARM_OT_node_add_output',
    'ARM_OT_node_call_func',
    'ARM_OT_node_remove_input',
    'ARM_OT_node_remove_input_output',
    'ARM_OT_node_remove_input_value',
    'ARM_OT_node_remove_output',
    'ARM_OT_node_search',

    'NODE_OT_delete',
    'NODE_OT_duplicate_move',
    'NODE_OT_hide_toggle',
    'NODE_OT_link',
    'NODE_OT_move_detach_links',
    'NODE_OT_select',
    'NODE_OT_translate_attach',
    'NODE_OT_translate_attach_remove_on_cancel',

    'OBJECT_OT_editmode_toggle',
    'OUTLINER_OT_item_activate',
    'UI_OT_button_string_clear',
    'UI_OT_eyedropper_id',
    'VIEW3D_OT_select',
    'VIEW3D_OT_select_box',
)
