# coding: utf-8
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@yandex.com>
#
# 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 3 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.
'''
DESCRIPTION.
Reorganization and restructuring of assembly structure.
'''
__version__ = '0.3'
import logging

import bpy
import mathutils

from blender.utils.object_relations import (parenting,
                                            unparenting)
from blender.utils.object_transforms import round_transforms
from blender.utils.collection_tools import unlink_from_collections

logger = logging.getLogger(__name__)


def hierarchy_assembly(lcs_names, parts_sequence):
    ''' Hierarchy by LCS and Parts Assembling Sequence. '''
    # collect scene hierarchy start info
    main_locators = [obj for obj in bpy.data.objects if not obj.parent]
    lcs_inlet_objects = []
    lcs_outlet_objects = []
    for lcs_name in lcs_names:
        lcs_obj = bpy.data.objects[lcs_name]
        if (lcs_obj.get('Robossembler_SocketFlow') == 'inlet'
            and lcs_obj.get('Robossembler_DefaultOrigin')
            ):
            lcs_inlet_objects.append(lcs_obj)
        else:
            lcs_outlet_objects.append(lcs_obj)

    if not lcs_inlet_objects:
        raise Exception('No LCS Inlet objects found!')

    # get main_locator
    main_locator = None
    for locator in main_locators:
        if set(lcs_inlet_objects + lcs_outlet_objects).issubset(
            locator.children_recursive
        ):
            main_locator = locator
    if not main_locator:
        # TODO need checking
        return logger.error('CAD root locator should be parent of all LCS!')

    # check parts_sequence objects in scene
    for part in parts_sequence:
        if not bpy.data.objects.get(part):
            return logger.error('%s part object not found!', part)

    # create root lcs by parts sequence
    first_part_obj = bpy.data.objects[parts_sequence[0]]
    root_lcs = None
    for lcs_inlet in first_part_obj.children:
        # drop non lcs objs
        if lcs_inlet.name not in lcs_names:
            continue
        # drop non DefaultOrigins
        lcs_obj = bpy.data.objects[lcs_name]
        if not (lcs_obj.get('Robossembler_SocketFlow') == 'inlet'
            and lcs_obj.get('Robossembler_DefaultOrigin')
            ):
            continue
        root_lcs_name = 'root'
        root_lcs = bpy.data.objects.new(root_lcs_name, None)
        root_lcs.empty_display_type = 'ARROWS'
        root_lcs.empty_display_size = 0.15
        root_lcs.show_in_front = True
        root_lcs.location = lcs_inlet.location
        root_lcs.rotation_euler = lcs_inlet.rotation_euler
        root_lcs.parent = lcs_inlet.parent
        bpy.context.scene.collection.objects.link(root_lcs)
        logger.info('Root Inlet LCS object created!')
    unparenting(root_lcs)
    round_transforms(root_lcs)
    parenting(root_lcs, main_locator)

    # retree_by lcs
    for lcs_inlet_obj in lcs_inlet_objects:
        # lcs inlet as main parent
        parent_locator = lcs_inlet_obj.parent
        if not parent_locator:
            raise Exception('LCS %s should have a parent!', lcs_inlet_obj.name)
        unparenting(lcs_inlet_obj)
        round_transforms(lcs_inlet_obj)
        if parent_locator:
            if parent_locator.parent:
                unparenting(parent_locator)
            parenting(lcs_inlet_obj, parent_locator)
        parenting(root_lcs, lcs_inlet_obj)
        # lcs outlet parent to lcs inlet
        for lcs_outlet_obj in lcs_inlet_obj.children_recursive:
            if lcs_outlet_obj.name not in lcs_names:
                continue
            unparenting(lcs_outlet_obj)
            round_transforms(lcs_outlet_obj)
            parenting(lcs_inlet_obj, lcs_outlet_obj)

    # reset transforms for root_lcs
    root_lcs.matrix_world = mathutils.Matrix()

    # lcs collections
    part_names = []
    for lcs_inlet_obj in root_lcs.children:
        # remove unmarked parts
        if lcs_inlet_obj not in lcs_inlet_objects:
            for obj in lcs_inlet_obj.children_recursive:
                bpy.data.objects.remove(obj, do_unlink=True)
            bpy.data.objects.remove(lcs_inlet_obj, do_unlink=True)
            continue
        # collect part names
        part_name = None
        for locator in lcs_inlet_obj.children:
            if locator in lcs_outlet_objects:
                continue
            part_name = locator.name
            part_names.append(part_name)
        # pack parts to collections
        part_col = bpy.data.collections.new(f'{part_name}')
        bpy.data.collections['Render'].children.link(part_col)
        for obj in lcs_inlet_obj.children_recursive:
            unlink_from_collections(obj)
            part_col.objects.link(obj)
        unlink_from_collections(lcs_inlet_obj)
        part_col.objects.link(lcs_inlet_obj)

    # TODO DEPRECATED
    """
    # parts assembling
    for idx, part_name in enumerate(parts_sequence):
        #  TODO clones for clones
        if part_name not in part_names:
            continue
        lcs_inlet_obj = bpy.data.objects[part_name].parent
        constraint = lcs_inlet_obj.constraints.new(type='COPY_TRANSFORMS')
        # drop first_part_obj
        if idx == 0:
            constraint.target = root_lcs
            continue
        # if asm pair exists
        part_before = bpy.data.objects.get(parts_sequence[idx - 1])
        if part_before:
            lcs_outlet_objs = [
                lcs_out
                for lcs_out in part_before.parent.children
                if lcs_out in lcs_outlet_objects]
            if lcs_outlet_objs:
                constraint.target = lcs_outlet_objs[0]
        else:
            constraint.target = root_lcs
            constraint.enabled = False
    # for reset transforms when exporting
    for lcs in lcs_outlet_objects:
        constraint = lcs.constraints.new(type='COPY_TRANSFORMS')
        constraint.target = root_lcs
        constraint.enabled = False
    """

    logger.info('Restructuring assembly pipeline finished!')

    return part_names


def hierarchy_separated_parts(lcs_names):
    ''' Restructuring pipeline as separated parts. '''
    # collect scene hierarchy start info
    lcs_inlet_objects = []
    lcs_outlet_objects = []

    for lcs_name in lcs_names:
        lcs_obj = bpy.data.objects[lcs_name]
        if (lcs_obj.get('Robossembler_SocketFlow') == 'inlet'
            and lcs_obj.get('Robossembler_DefaultOrigin')
            ):
            lcs_inlet_objects.append(lcs_obj)
        else:
            lcs_outlet_objects.append(lcs_obj)

    if not lcs_inlet_objects:
        raise Exception('No LCS Inlet objects found!')

    # retree_by lcs
    part_names = []
    for lcs_inlet_obj in lcs_inlet_objects:
        # lcs inlet as main parent
        parent_locator = lcs_inlet_obj.parent
        if not parent_locator:
            raise Exception('LCS %s should have a parent!', lcs_inlet_obj.name)
        unparenting(lcs_inlet_obj)
        round_transforms(lcs_inlet_obj)
        if parent_locator:
            if parent_locator.parent:
                unparenting(parent_locator)
            parenting(lcs_inlet_obj, parent_locator)
        # lcs outlet parent to lcs inlet
        for lcs_outlet_obj in lcs_inlet_obj.children_recursive:
            if lcs_outlet_obj.name not in lcs_names:
                continue
            unparenting(lcs_outlet_obj)
            round_transforms(lcs_outlet_obj)
            parenting(lcs_inlet_obj, lcs_outlet_obj)

        # reset transforms for inlet_lcs
        lcs_inlet_obj.matrix_world = mathutils.Matrix()

        # pack parts to collections
        part_name = None
        for locator in lcs_inlet_obj.children:
            if locator not in lcs_outlet_objects:
                part_name = locator.name
                part_names.append(part_name)
        part_col = bpy.data.collections.new(f'{part_name}')
        bpy.data.collections['Render'].children.link(part_col)
        for obj in lcs_inlet_obj.children_recursive:
            unlink_from_collections(obj)
            part_col.objects.link(obj)
        unlink_from_collections(lcs_inlet_obj)
        part_col.objects.link(lcs_inlet_obj)
    # remove unmarked objects
    marked_objs = sum(
        [lcs_inlet_obj.children_recursive for lcs_inlet_obj in lcs_inlet_objects],
        [])
    parts_col_objs = bpy.data.collections['Render'].objects
    unmarked_objs = list(set(parts_col_objs) - set(marked_objs))
    if unmarked_objs:
        removed_objs = list(map(bpy.data.objects.remove, unmarked_objs))
        logger.info('%s unmarked objects removed!', len(removed_objs))

    logger.info('Restructuring pipeline as separated parts finished!')

    return part_names


def hierarchy_mono_part():
    ''' Restructuring pipeline as single part. '''
    # collect scene hierarchy start info
    main_locators = [obj for obj in bpy.data.objects if not obj.parent]

    # pack parts to collections
    part_names = []
    for main_locator in main_locators:
        part_name = main_locator.name
        part_names.append(part_name)
        part_col = bpy.data.collections.new(f'{part_name}')
        bpy.data.collections['Render'].children.link(part_col)
        for obj in main_locator.children_recursive:
            unlink_from_collections(obj)
            part_col.objects.link(obj)
        unlink_from_collections(main_locator)
        part_col.objects.link(main_locator)

    logger.info('Restructuring pipeline as single part finished!')

    return part_names
