# <pep8-80 compliant>

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

__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "6.5"
__date__ = "6 Mar 2021"

import bpy
from bpy.props import (
    BoolProperty,
)
import bmesh

from .. import common
from ..utils.bl_class_registry import BlClassRegistry
from ..utils.property_class_registry import PropertyClassRegistry


def _is_valid_context(context):
    # Multiple objects editing mode is not supported in this feature.
    objs = common.get_uv_editable_objects(context)
    if len(objs) != 1:
        return False

    # only edit mode is allowed to execute
    if context.object.mode != 'EDIT':
        return False

    # only 'VIEW_3D' space is allowed to execute
    if not common.is_valid_space(context, ['VIEW_3D']):
        return False

    return True


@PropertyClassRegistry()
class _Properties:
    idname = "texture_wrap"

    @classmethod
    def init_props(cls, scene):
        class Props():
            ref_face_index = -1
            ref_obj = None

        scene.muv_props.texture_wrap = Props()

        scene.muv_texture_wrap_enabled = BoolProperty(
            name="Texture Wrap",
            description="Texture Wrap is enabled",
            default=False
        )
        scene.muv_texture_wrap_set_and_refer = BoolProperty(
            name="Set and Refer",
            description="Refer and set UV",
            default=True
        )
        scene.muv_texture_wrap_selseq = BoolProperty(
            name="Selection Sequence",
            description="Set UV sequentially",
            default=False
        )

    @classmethod
    def del_props(cls, scene):
        del scene.muv_props.texture_wrap
        del scene.muv_texture_wrap_enabled
        del scene.muv_texture_wrap_set_and_refer
        del scene.muv_texture_wrap_selseq


@BlClassRegistry()
class MUV_OT_TextureWrap_Refer(bpy.types.Operator):
    """
    Operation class: Refer UV
    """

    bl_idname = "uv.muv_texture_wrap_refer"
    bl_label = "Refer"
    bl_description = "Refer UV"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        # we can not get area/space/region from console
        if common.is_console_mode():
            return True
        return _is_valid_context(context)

    def execute(self, context):
        props = context.scene.muv_props.texture_wrap

        objs = common.get_uv_editable_objects(context)
        # poll() method ensures that only one object is selected.
        obj = objs[0]
        bm = bmesh.from_edit_mesh(obj.data)
        if common.check_version(2, 73, 0) >= 0:
            bm.faces.ensure_lookup_table()

        if not bm.loops.layers.uv:
            self.report({'WARNING'}, "Object must have more than one UV map")
            return {'CANCELLED'}

        sel_faces = [f for f in bm.faces if f.select]
        if len(sel_faces) != 1:
            self.report({'WARNING'}, "Must select only one face")
            return {'CANCELLED'}

        props.ref_face_index = sel_faces[0].index
        props.ref_obj = obj

        return {'FINISHED'}


@BlClassRegistry()
class MUV_OT_TextureWrap_Set(bpy.types.Operator):
    """
    Operation class: Set UV
    """

    bl_idname = "uv.muv_texture_wrap_set"
    bl_label = "Set"
    bl_description = "Set UV"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        # we can not get area/space/region from console
        if common.is_console_mode():
            return True
        sc = context.scene
        props = sc.muv_props.texture_wrap
        if not props.ref_obj:
            return False
        return _is_valid_context(context)

    def execute(self, context):
        sc = context.scene
        props = sc.muv_props.texture_wrap

        objs = common.get_uv_editable_objects(context)
        # poll() method ensures that only one object is selected.
        obj = objs[0]
        bm = bmesh.from_edit_mesh(obj.data)
        if common.check_version(2, 73, 0) >= 0:
            bm.faces.ensure_lookup_table()

        if not bm.loops.layers.uv:
            self.report({'WARNING'}, "Object must have more than one UV map")
            return {'CANCELLED'}
        uv_layer = bm.loops.layers.uv.verify()

        if sc.muv_texture_wrap_selseq:
            sel_faces = []
            for hist in bm.select_history:
                if isinstance(hist, bmesh.types.BMFace) and hist.select:
                    sel_faces.append(hist)
            if not sel_faces:
                self.report({'WARNING'}, "Must select more than one face")
                return {'CANCELLED'}
        else:
            sel_faces = [f for f in bm.faces if f.select]
            if len(sel_faces) != 1:
                self.report({'WARNING'}, "Must select only one face")
                return {'CANCELLED'}

        ref_face_index = props.ref_face_index
        for face in sel_faces:
            tgt_face_index = face.index
            if ref_face_index == tgt_face_index:
                self.report({'WARNING'}, "Must select different face")
                return {'CANCELLED'}

            if props.ref_obj != obj:
                self.report({'WARNING'}, "Object must be same")
                return {'CANCELLED'}

            ref_face = bm.faces[ref_face_index]
            tgt_face = bm.faces[tgt_face_index]

            # get common vertices info
            common_verts = []
            for sl in ref_face.loops:
                for dl in tgt_face.loops:
                    if sl.vert == dl.vert:
                        info = {"vert": sl.vert, "ref_loop": sl,
                                "tgt_loop": dl}
                        common_verts.append(info)
                        break

            if len(common_verts) != 2:
                self.report({'WARNING'},
                            "2 vertices must be shared among faces")
                return {'CANCELLED'}

            # get reference other vertices info
            ref_other_verts = []
            for sl in ref_face.loops:
                for ci in common_verts:
                    if sl.vert == ci["vert"]:
                        break
                else:
                    info = {"vert": sl.vert, "loop": sl}
                    ref_other_verts.append(info)

            if not ref_other_verts:
                self.report({'WARNING'}, "More than 1 vertex must be unshared")
                return {'CANCELLED'}

            # get reference info
            ref_info = {}
            cv0 = common_verts[0]["vert"].co
            cv1 = common_verts[1]["vert"].co
            cuv0 = common_verts[0]["ref_loop"][uv_layer].uv
            cuv1 = common_verts[1]["ref_loop"][uv_layer].uv
            ov0 = ref_other_verts[0]["vert"].co
            ouv0 = ref_other_verts[0]["loop"][uv_layer].uv
            ref_info["vert_vdiff"] = cv1 - cv0
            ref_info["uv_vdiff"] = cuv1 - cuv0
            ref_info["vert_hdiff"], _ = common.diff_point_to_segment(
                cv0, cv1, ov0)
            ref_info["uv_hdiff"], _ = common.diff_point_to_segment(
                cuv0, cuv1, ouv0)

            # get target other vertices info
            tgt_other_verts = []
            for dl in tgt_face.loops:
                for ci in common_verts:
                    if dl.vert == ci["vert"]:
                        break
                else:
                    info = {"vert": dl.vert, "loop": dl}
                    tgt_other_verts.append(info)

            if not tgt_other_verts:
                self.report({'WARNING'}, "More than 1 vertex must be unshared")
                return {'CANCELLED'}

            # get target info
            for info in tgt_other_verts:
                cv0 = common_verts[0]["vert"].co
                cv1 = common_verts[1]["vert"].co
                cuv0 = common_verts[0]["ref_loop"][uv_layer].uv
                ov = info["vert"].co
                info["vert_hdiff"], x = common.diff_point_to_segment(
                    cv0, cv1, ov)
                info["vert_vdiff"] = x - common_verts[0]["vert"].co

                # calclulate factor
                fact_h = -info["vert_hdiff"].length / \
                    ref_info["vert_hdiff"].length
                fact_v = info["vert_vdiff"].length / \
                    ref_info["vert_vdiff"].length
                duv_h = ref_info["uv_hdiff"] * fact_h
                duv_v = ref_info["uv_vdiff"] * fact_v

                # get target UV
                info["target_uv"] = cuv0 + duv_h + duv_v

            # apply to common UVs
            for info in common_verts:
                info["tgt_loop"][uv_layer].uv = \
                    info["ref_loop"][uv_layer].uv.copy()
            # apply to other UVs
            for info in tgt_other_verts:
                info["loop"][uv_layer].uv = info["target_uv"]

            common.debug_print("===== Target Other Vertices =====")
            common.debug_print(tgt_other_verts)

            bmesh.update_edit_mesh(obj.data)

            ref_face_index = tgt_face_index

        if sc.muv_texture_wrap_set_and_refer:
            props.ref_face_index = tgt_face_index

        return {'FINISHED'}
