# ##### 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 #####


import logging
import uuid

import bpy

from . import utils


bk_logger = logging.getLogger(__name__)


def append_brush(file_name, brushname=None, link=False, fake_user=True):
    """append a brush"""
    with bpy.data.libraries.load(file_name, link=link, relative=True) as (
        data_from,
        data_to,
    ):
        for m in data_from.brushes:
            if m == brushname or brushname is None:
                data_to.brushes = [m]
                brushname = m
    brush = bpy.data.brushes[brushname]
    brush.use_fake_user = fake_user
    return brush


def append_nodegroup(file_name, nodegroupname=None, link=False, fake_user=True):
    """Append selected node group. If nodegroupname is None, first node group is appended.
    If node group with the same name is already in the scene, it is not appended again.
    Try to look for a suitable node editor and insert the node group there, in the middle of the area.
    """
    with bpy.data.libraries.load(file_name, link=link, relative=True) as (
        data_from,
        data_to,
    ):
        for g in data_from.node_groups:
            print(g)
            if g == nodegroupname or nodegroupname is None:
                data_to.node_groups = [g]
                nodegroupname = g
    nodegroup = bpy.data.node_groups[nodegroupname]
    nodegroup.use_fake_user = fake_user
    # if there's an open node editor, let's find if it matches the type of the asset group and insert it
    # in middle of the area.
    # mapping dict for editor type to node group node types
    sdict = {
        "GeometryNodeTree": "GeometryNodeGroup",
        "ShaderNodeTree": "ShaderNodeGroup",
        "CompositorNodeTree": "CompositorNodeGroup",
    }
    # Look for a suitable node editor and insert the node group there, in the middle of the area.
    for area in bpy.context.screen.areas:
        if area.type != "NODE_EDITOR":
            continue

        if area.spaces.active.tree_type != nodegroup.bl_rna.identifier:
            continue

        nt = area.spaces.active.edit_tree

        if nt is None:
            continue

        # deselect all nodes
        for n in nt.nodes:
            n.select = False
        node = nt.nodes.new(sdict[area.spaces.active.tree_type])
        node.node_tree = nodegroup
        area.spaces.active.node_tree = nodegroup
        break
    return nodegroup


def append_material(file_name, matname=None, link=False, fake_user=True):
    """append a material type asset

    first, we have to check if there is a material with same name
    in previous step there's check if the imported material
    is already in the scene, so we know same name != same material
    """

    mats_before = bpy.data.materials[:]
    try:
        with bpy.data.libraries.load(file_name, link=link, relative=True) as (
            data_from,
            data_to,
        ):
            found = False
            for m in data_from.materials:
                if m == matname or matname is None:
                    data_to.materials = [m]
                    matname = m
                    found = True
                    break

            # not found yet? probably some name inconsistency then.
            if not found and len(data_from.materials) > 0:
                data_to.materials = [data_from.materials[0]]
                matname = data_from.materials[0]
                bk_logger.warning(
                    f"the material wasn't found under the exact name, appended another one: {matname}"
                )

    except Exception as e:
        bk_logger.error(f"{e} - failed to open the asset file")
    # we have to find the new material , due to possible name changes
    mat = None
    for m in bpy.data.materials:
        if m not in mats_before:
            mat = m
            break
    # still not found?
    if mat is None:
        mat = bpy.data.materials.get(matname)

    if fake_user:
        mat.use_fake_user = True
    return mat


def append_scene(file_name, scenename=None, link=False, fake_user=False):
    """append a scene type asset"""
    with bpy.data.libraries.load(file_name, link=link, relative=True) as (
        data_from,
        data_to,
    ):
        for s in data_from.scenes:
            if s == scenename or scenename is None:
                data_to.scenes = [s]
                scenename = s
    scene = bpy.data.scenes[scenename]
    if fake_user:
        scene.use_fake_user = True
    # scene has to have a new uuid, so user reports aren't screwed.
    scene["uuid"] = str(uuid.uuid4())

    # reset ui_props of the scene to defaults:
    ui_props = bpy.context.window_manager.blenderkitUI
    ui_props.down_up = "SEARCH"

    return scene


def get_node_sure(node_tree, ntype=""):
    """
    Gets a node of certain type, but creates a new one if not pre
    """
    node = None
    for n in node_tree.nodes:
        if ntype == n.bl_rna.identifier:
            node = n
            return node
    if not node:
        node = node_tree.nodes.new(type=ntype)

    return node


def hdr_swap(name, hdr):
    """
    Try to replace the hdr in current world setup. If this fails, create a new world.
    :param name: Name of the resulting world (renamse the current one if swap is successfull)
    :param hdr: Image type
    :return: None
    """
    w = bpy.context.scene.world
    if w:
        w.use_nodes = True
        w.name = name
        nt = w.node_tree
        for n in nt.nodes:
            if "ShaderNodeTexEnvironment" == n.bl_rna.identifier:
                env_node = n
                env_node.image = hdr
                return
    new_hdr_world(name, hdr)


def new_hdr_world(name, hdr):
    """
    creates a new world, links in the hdr with mapping node, and links the world to scene
    :param name: Name of the world datablock
    :param hdr: Image type
    :return: None
    """
    w = bpy.data.worlds.new(name=name)
    w.use_nodes = True
    bpy.context.scene.world = w

    nt = w.node_tree
    env_node = nt.nodes.new(type="ShaderNodeTexEnvironment")
    env_node.image = hdr
    background = get_node_sure(nt, "ShaderNodeBackground")
    tex_coord = get_node_sure(nt, "ShaderNodeTexCoord")
    mapping = get_node_sure(nt, "ShaderNodeMapping")

    nt.links.new(env_node.outputs["Color"], background.inputs["Color"])
    nt.links.new(tex_coord.outputs["Generated"], mapping.inputs["Vector"])
    nt.links.new(mapping.outputs["Vector"], env_node.inputs["Vector"])
    env_node.location.x = -400
    mapping.location.x = -600
    tex_coord.location.x = -800


def load_HDR(file_name, name):
    """Load a HDR into file and link it to scene world."""
    already_linked = False
    for i in bpy.data.images:
        if i.filepath == file_name:
            hdr = i
            already_linked = True
            break

    if not already_linked:
        hdr = bpy.data.images.load(file_name)

    hdr_swap(name, hdr)
    return hdr


def link_collection(
    file_name, obnames=[], location=(0, 0, 0), link=False, parent=None, **kwargs
):
    """link an instanced group - model type asset"""
    sel = utils.selection_get()

    with bpy.data.libraries.load(file_name, link=link, relative=True) as (
        data_from,
        data_to,
    ):
        scols = []
        for col in data_from.collections:
            if col == kwargs["name"]:
                data_to.collections = [col]

    rotation = (0, 0, 0)
    if kwargs.get("rotation") is not None:
        rotation = kwargs["rotation"]

    bpy.ops.object.empty_add(type="PLAIN_AXES", location=location, rotation=rotation)
    main_object = bpy.context.view_layer.objects.active
    main_object.instance_type = "COLLECTION"

    if parent is not None:
        main_object.parent = bpy.data.objects.get(parent)

    main_object.matrix_world.translation = location

    for col in bpy.data.collections:
        if col.library is not None:
            fp = bpy.path.abspath(col.library.filepath)
            fp1 = bpy.path.abspath(file_name)
            if fp == fp1:
                main_object.instance_collection = col
                break

    # sometimes, the lib might already  be without the actual link.
    if not main_object.instance_collection and kwargs["name"]:
        col = bpy.data.collections.get(kwargs["name"])
        if col:
            main_object.instance_collection = col

    main_object.name = main_object.instance_collection.name

    # bpy.ops.wm.link(directory=file_name + "/Collection/", filename=kwargs['name'], link=link, instance_collections=True,
    #                 autoselect=True)
    # main_object = bpy.context.view_layer.objects.active
    # if kwargs.get('rotation') is not None:
    #     main_object.rotation_euler = kwargs['rotation']
    # main_object.location = location

    utils.selection_set(sel)
    return main_object, []


def append_particle_system(
    file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs
):
    """link an instanced group - model type asset"""

    pss = []
    with bpy.data.libraries.load(file_name, link=link, relative=True) as (
        data_from,
        data_to,
    ):
        for ps in data_from.particles:
            pss.append(ps)
        data_to.particles = pss

    s = bpy.context.scene
    sel = utils.selection_get()

    target_object = bpy.context.scene.objects.get(kwargs["target_object"])
    if target_object is not None and target_object.type == "MESH":
        target_object.select_set(True)
        bpy.context.view_layer.objects.active = target_object

        for ps in pss:
            # now let's tune this ps to the particular objects area:
            totarea = 0
            for p in target_object.data.polygons:
                totarea += p.area
            count = int(ps.count * totarea)

            if ps.child_type in ("INTERPOLATED", "SIMPLE"):
                total_count = count * ps.rendered_child_count
                disp_count = count * ps.child_nbr
            else:
                total_count = count

            bbox_threshold = 25000
            display_threshold = 200000
            total_max_threshold = 2000000
            # emitting too many parent particles just kills blender now.

            # this part tuned child count, we'll leave children to artists only.
            # if count > total_max_threshold:
            #     ratio = round(count / total_max_threshold)
            #
            #     if ps.child_type in ('INTERPOLATED', 'SIMPLE'):
            #         ps.rendered_child_count *= ratio
            #     else:
            #         ps.child_type = 'INTERPOLATED'
            #         ps.rendered_child_count = ratio
            #     count = max(2, int(count / ratio))

            # 1st level of optimizaton - switch t bounding boxes.
            if total_count > bbox_threshold:
                target_object.display_type = "BOUNDS"
            # 2nd level of optimization - reduce percentage of displayed particles.
            ps.display_percentage = min(
                ps.display_percentage,
                max(1, int(100 * display_threshold / total_count)),
            )
            # here we can also tune down number of children displayed.
            # set the count
            ps.count = count
            # add the modifier
            bpy.ops.object.particle_system_add()
            # 3rd level - hide particle system from viewport - is done on the modifier..
            if total_count > total_max_threshold:
                target_object.modifiers[-1].show_viewport = False

            target_object.particle_systems[-1].settings = ps

        target_object.select_set(False)
    utils.selection_set(sel)
    return target_object, []


def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs):
    """Append object into scene individually. 2 approaches based in definition of name argument.
    TODO: really split this function into 2 functions: kwargs.get('name')==None and else.
    """
    # simplified version of append
    if kwargs.get("name"):
        scene = bpy.context.scene
        sel = utils.selection_get()
        bpy.ops.object.select_all(action="DESELECT")

        path = file_name + "/Collection"
        collection_name = kwargs.get("name")
        bpy.ops.wm.append(filename=collection_name, directory=path)

        # fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D')
        # bpy.ops.wm.append(fc, filename=collection_name, directory=path)

        return_obs = []
        to_hidden_collection = []
        collection = None
        main_object = None
        # get first at least one parent for sure
        for ob in bpy.context.scene.objects:
            if ob.select_get():
                if not ob.parent:
                    main_object = ob
                    ob.location = location
        # do once again to ensure hidden objects are hidden
        for ob in bpy.context.scene.objects:
            if ob.select_get():
                return_obs.append(ob)
                # check for object that should be hidden
                if ob.users_collection[0].name == collection_name:
                    collection = ob.users_collection[0]
                    collection["is_blenderkit_asset"] = True
                    if not ob.parent:
                        main_object = ob
                        ob.location = location
                else:
                    to_hidden_collection.append(ob)

        assert (
            main_object != None
        ), f"asset {kwargs['name']} not found in scene after appending"
        if kwargs.get("rotation"):
            main_object.rotation_euler = kwargs["rotation"]

        if kwargs.get("parent") is not None:
            main_object.parent = bpy.data.objects[kwargs["parent"]]
            main_object.matrix_world.translation = location

        # move objects that should be hidden to a sub collection
        if len(to_hidden_collection) > 0 and collection is not None:
            hidden_collections = []
            scene_collection = bpy.context.scene.collection
            for ob in to_hidden_collection:
                hide_collection = ob.users_collection[0]

                # objects from scene collection (like rigify widgets go to a new collection
                if (
                    hide_collection == scene_collection
                    or hide_collection.name in scene_collection.children
                ):
                    hidden_collection_name = collection_name + "_hidden"
                    h_col = bpy.data.collections.get(hidden_collection_name)
                    if h_col is None:
                        h_col = bpy.data.collections.new(name=hidden_collection_name)
                        collection.children.link(h_col)
                        utils.exclude_collection(hidden_collection_name)

                    ob.users_collection[0].objects.unlink(ob)
                    h_col.objects.link(ob)
                    continue
                if hide_collection in hidden_collections:
                    continue
                # All other collections are moved to be children of the model collection
                bk_logger.info(f"{hide_collection}, {collection}")
                utils.move_collection(hide_collection, collection)
                utils.exclude_collection(hide_collection.name)
                hidden_collections.append(hide_collection)

        bpy.ops.object.select_all(action="DESELECT")
        utils.selection_set(sel)
        # let collection also store info that it was created by BlenderKit, for purging reasons

        return main_object, return_obs

    # this is used for uploads:
    with bpy.data.libraries.load(file_name, link=link, relative=True) as (
        data_from,
        data_to,
    ):
        sobs = []
        # for col in data_from.collections:
        #     if col == kwargs.get('name'):
        for ob in data_from.objects:
            if ob in obnames or obnames == []:
                sobs.append(ob)
        data_to.objects = sobs
        # data_to.objects = data_from.objects#[name for name in data_from.objects if name.startswith("house")]

    # link them to scene
    scene = bpy.context.scene
    sel = utils.selection_get()
    bpy.ops.object.select_all(action="DESELECT")

    return_obs = []  # this might not be needed, but better be sure to rewrite the list.
    main_object = None
    hidden_objects = []

    for obj in data_to.objects:
        if obj is not None:
            # if obj.name not in scene.objects:
            scene.collection.objects.link(obj)
            if obj.parent is None:
                obj.location = location
                main_object = obj
            obj.select_set(True)
            # we need to unhide object so make_local op can use those too.
            if link == True:
                if obj.hide_viewport:
                    hidden_objects.append(obj)
                    obj.hide_viewport = False
            return_obs.append(obj)

    # Only after all objects are in scene! Otherwise gets broken relationships
    if link == True:
        bpy.ops.object.make_local(type="SELECT_OBJECT")
        for ob in hidden_objects:
            ob.hide_viewport = True

    if kwargs.get("rotation") is not None:
        main_object.rotation_euler = kwargs["rotation"]

    if kwargs.get("parent") is not None:
        main_object.parent = bpy.data.objects[kwargs["parent"]]
        main_object.matrix_world.translation = location

    bpy.ops.object.select_all(action="DESELECT")
    utils.selection_set(sel)

    return main_object, return_obs
