# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

# -----------------------------------------------------------------------------
# Export Functions

__all__ = (
    "actionconfig_export_as_data",
    "actionconfig_import_from_data",
    "actionconfig_init_from_data",
    "actionmap_init_from_data",
    "actionmap_item_init_from_data",
)


def indent(levels):
    return levels * " "


def round_float_32(f):
    from struct import pack, unpack
    return unpack("f", pack("f", f))[0]


def repr_f32(f):
    f_round = round_float_32(f)
    f_str = repr(f)
    f_str_frac = f_str.partition(".")[2]
    if not f_str_frac:
        return f_str
    for i in range(1, len(f_str_frac)):
        f_test = round(f, i)
        f_test_round = round_float_32(f_test)
        if f_test_round == f_round:
            return "%.*f" % (i, f_test)
    return f_str

def ami_args_as_data(ami):
    s = [
        f"\"type\": '{ami.type}'",
        f"\"user_path0\": '{ami.user_path0}'",
        f"\"user_path1\": '{ami.user_path1}'",
    ]

    if ami.type == 'FLOAT' or ami.type == 'VECTOR2D':
        s.append(f"\"op\": '{ami.op}'")
        s.append(f"\"op_mode\": '{ami.op_mode}'")
        s.append(f"\"bimanual\": '{ami.bimanual}'")
        s.append(f"\"haptic_name\": '{ami.haptic_name}'")
        s.append(f"\"haptic_match_user_paths\": '{ami.haptic_match_user_paths}'")
        s.append(f"\"haptic_duration\": '{ami.haptic_duration}'")
        s.append(f"\"haptic_frequency\": '{ami.haptic_frequency}'")
        s.append(f"\"haptic_amplitude\": '{ami.haptic_amplitude}'")
        s.append(f"\"haptic_mode\": '{ami.haptic_mode}'")
    elif ami.type == 'POSE':
        s.append(f"\"pose_is_controller_grip\": '{ami.pose_is_controller_grip}'")
        s.append(f"\"pose_is_controller_aim\": '{ami.pose_is_controller_aim}'")


    return "{" + ", ".join(s) + "}"


def ami_data_from_args(ami, args):    
    ami.type = args["type"]
    ami.user_path0 = args["user_path0"]
    ami.user_path1 = args["user_path1"]
    
    if ami.type == 'FLOAT' or ami.type == 'VECTOR2D':
        ami.op = args["op"]
        ami.op_mode = args["op_mode"]
        ami.bimanual = True if (args["bimanual"] == 'True') else False
        ami.haptic_name = args["haptic_name"]
        ami.haptic_match_user_paths = True if (args["haptic_match_user_paths"] == 'True') else False
        ami.haptic_duration = float(args["haptic_duration"])
        ami.haptic_frequency = float(args["haptic_frequency"])
        ami.haptic_amplitude = float(args["haptic_amplitude"])
        ami.haptic_mode = args["haptic_mode"]
    elif ami.type == 'POSE':
        ami.pose_is_controller_grip = True if (args["pose_is_controller_grip"] == 'True') else False
        ami.pose_is_controller_aim = True if (args["pose_is_controller_aim"] == 'True') else False


def _ami_properties_to_lines_recursive(level, properties, lines):
    from bpy.types import OperatorProperties

    def string_value(value):
        if isinstance(value, (str, bool, int, set)):
            return repr(value)
        elif isinstance(value, float):
            return repr_f32(value)
        elif getattr(value, '__len__', False):
            return repr(tuple(value))
        raise Exception(f"Export action configuration: can't write {value!r}")

    for pname in properties.bl_rna.properties.keys():
        if pname != "rna_type":
            value = getattr(properties, pname)
            if isinstance(value, OperatorProperties):
                lines_test = []
                _ami_properties_to_lines_recursive(level + 2, value, lines_test)
                if lines_test:
                    lines.append(f"(")
                    lines.append(f"\"{pname}\",\n")
                    lines.append(f"{indent(level + 3)}" "[")
                    lines.extend(lines_test)
                    lines.append("],\n")
                    lines.append(f"{indent(level + 3)}" "),\n" f"{indent(level + 2)}")
                del lines_test
            elif properties.is_property_set(pname):
                value = string_value(value)
                lines.append((f"(\"{pname}\", {value:s}),\n" f"{indent(level + 2)}"))


def _ami_properties_to_lines(level, ami_props, lines):
    if ami_props is None:
        return

    lines_test = [f"\"op_properties\":\n" f"{indent(level + 1)}" "["]
    _ami_properties_to_lines_recursive(level, ami_props, lines_test)
    if len(lines_test) > 1:
        lines_test.append("],\n")
        lines.extend(lines_test)


def _ami_attrs_or_none(level, ami):
    lines = []
    _ami_properties_to_lines(level + 1, ami.op_properties, lines)
    if not lines:
        return None
    return "".join(lines)


def amb_args_as_data(amb, type):
    s = [
        f"\"profile\": '{amb.profile}'",
        f"\"component_path0\": '{amb.component_path0}'",
        f"\"component_path1\": '{amb.component_path1}'",
    ]

    if type == 'FLOAT' or type == 'VECTOR2D':
        s.append(f"\"threshold\": '{amb.threshold}'")
        if type == 'FLOAT':
            s.append(f"\"axis_region\": '{amb.axis0_region}'")
        else: # type == 'VECTOR2D':
            s.append(f"\"axis0_region\": '{amb.axis0_region}'")
            s.append(f"\"axis1_region\": '{amb.axis1_region}'")
    elif type == 'POSE':
        s.append(f"\"pose_location\": '{amb.pose_location.x, amb.pose_location.y, amb.pose_location.z}'")
        s.append(f"\"pose_rotation\": '{amb.pose_rotation.x, amb.pose_rotation.y, amb.pose_rotation.z}'")

    return "{" + ", ".join(s) + "}"


def amb_data_from_args(amb, args, type):    
    amb.profile = args["profile"]
    amb.component_path0 = args["component_path0"]
    amb.component_path1 = args["component_path1"]

    if type == 'FLOAT' or type == 'VECTOR2D':
        amb.threshold = float(args["threshold"])
        if type == 'FLOAT':
            amb.axis0_region = args["axis_region"]
        else: # type == 'VECTOR2D':
            amb.axis0_region = args["axis0_region"]
            amb.axis1_region = args["axis1_region"]
    elif type == 'POSE':
        l = args["pose_location"].strip(')(').split(', ')
        amb.pose_location.x = float(l[0])
        amb.pose_location.y = float(l[1])
        amb.pose_location.z = float(l[2])
        l = args["pose_rotation"].strip(')(').split(', ')
        amb.pose_rotation.x = float(l[0])
        amb.pose_rotation.y = float(l[1])
        amb.pose_rotation.z = float(l[2])


def actionconfig_export_as_data(session_state, filepath, *, sort=False):
    export_actionmaps = []

    for am in session_state.actionmaps:
        export_actionmaps.append(am)

    if sort:
        export_actionmaps.sort(key=lambda k: k.name)

    with open(filepath, "w", encoding="utf-8") as fh:
        fw = fh.write

        # Use the file version since it includes the sub-version
        # which we can bump multiple times between releases.
        from bpy.app import version_file
        fw(f"actionconfig_version = {version_file!r}\n")
        del version_file

        fw("actionconfig_data = \\\n[")

        for am in export_actionmaps:
            fw("(")
            fw(f"\"{am.name:s}\",\n")

            fw(f"{indent(2)}" "{")
            fw(f"\"items\":\n")
            fw(f"{indent(3)}[")
            for ami in am.actionmap_items:
                fw(f"(")
                fw(f"\"{ami.name:s}\"")
                ami_args = ami_args_as_data(ami)
                ami_data = _ami_attrs_or_none(4, ami)
                if ami_data is None:
                    fw(f", ")
                else:
                    fw(",\n" f"{indent(5)}")

                fw(ami_args)
                if ami_data is None:
                    fw(", None,\n")
                else:
                    fw(",\n")
                    fw(f"{indent(5)}" "{")
                    fw(ami_data)
                    fw(f"{indent(6)}")
                    fw("}," f"{indent(5)}")
                    fw("\n")

                fw(f"{indent(5)}" "{")
                fw(f"\"bindings\":\n")
                fw(f"{indent(6)}[")
                for amb in ami.bindings:
                    fw(f"(")
                    fw(f"\"{amb.name:s}\"")
                    fw(f", ")
                    amb_args = amb_args_as_data(amb, ami.type)
                    fw(amb_args)
                    fw("),\n" f"{indent(7)}")
                fw("],\n" f"{indent(6)}")
                fw("},\n" f"{indent(5)}")
                fw("),\n" f"{indent(4)}")

            fw("],\n" f"{indent(3)}")
            fw("},\n" f"{indent(2)}")
            fw("),\n" f"{indent(1)}")

        fw("]\n")
        fw("\n\n")
        fw("if __name__ == \"__main__\":\n")

        # We could remove this in the future, as loading new action-maps in older Blender versions
        # makes less and less sense as Blender changes.
        fw("    # Only add keywords that are supported.\n")
        fw("    from bpy.app import version as blender_version\n")
        fw("    keywords = {}\n")
        fw("    if blender_version >= (3, 0, 0):\n")
        fw("        keywords[\"actionconfig_version\"] = actionconfig_version\n")

        fw("    import os\n")
        fw("    from viewport_vr_preview.io import actionconfig_import_from_data\n")
        fw("    actionconfig_import_from_data(\n")
        fw("        os.path.splitext(os.path.basename(__file__))[0],\n")
        fw("        actionconfig_data,\n")
        fw("        **keywords,\n")
        fw("    )\n")


# -----------------------------------------------------------------------------
# Import Functions

def _ami_props_setattr(ami_name, ami_props, attr, value):
    if type(value) is list:
        ami_subprop = getattr(ami_props, attr)
        for subattr, subvalue in value:
            _ami_props_setattr(ami_subprop, subattr, subvalue)
        return

    try:
        setattr(ami_props, attr, value)
    except AttributeError:
        print(f"Warning: property '{attr}' not found in action map item '{ami_name}'")
    except Exception as ex:
        print(f"Warning: {ex!r}")


def actionmap_item_init_from_data(ami, ami_bindings):
    new_fn = getattr(ami.bindings, "new")
    for (amb_name, amb_args) in ami_bindings:
        amb = new_fn(amb_name, True)
        amb_data_from_args(amb, amb_args, ami.type)


def actionmap_init_from_data(am, am_items):
    new_fn = getattr(am.actionmap_items, "new")
    for (ami_name, ami_args, ami_data, ami_content) in am_items:
        ami = new_fn(ami_name, True)
        ami_data_from_args(ami, ami_args)
        if ami_data is not None:
            ami_props_data = ami_data.get("op_properties", None)
            if ami_props_data is not None:
                ami_props = ami.op_properties
                assert type(ami_props_data) is list
                for attr, value in ami_props_data:
                    _ami_props_setattr(ami_name, ami_props, attr, value)
        ami_bindings = ami_content["bindings"]
        assert type(ami_bindings) is list
        actionmap_item_init_from_data(ami, ami_bindings)


def actionconfig_init_from_data(session_state, actionconfig_data, actionconfig_version):
    # Load data in the format defined above.
    #
    # Runs at load time, keep this fast!
    if actionconfig_version is not None:
        from .versioning import actionconfig_update
        actionconfig_data = actionconfig_update(actionconfig_data, actionconfig_version)
    
    for (am_name, am_content) in actionconfig_data:
        am = session_state.actionmaps.new(session_state, am_name, True)
        am_items = am_content["items"]
        # Check here instead of inside 'actionmap_init_from_data'
        # because we want to allow both tuple & list types in that case.
        #
        # For full action maps, ensure these are always lists to allow for extending them
        # in a generic way that doesn't have to check for the type each time.
        assert type(am_items) is list
        actionmap_init_from_data(am, am_items)


def actionconfig_import_from_data(session_state, actionconfig_data, *, actionconfig_version=(0, 0, 0)):
    # Load data in the format defined above.
    #
    # Runs at load time, keep this fast!
    import bpy
    actionconfig_init_from_data(session_state, actionconfig_data, actionconfig_version)
