# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "5.2"
__date__ = "17 Nov 2018"

import math
from math import atan2, tan, sin, cos

import bpy
import bmesh
from mathutils import Vector
from bpy.props import EnumProperty, BoolProperty, FloatProperty

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


__all__ = [
    'Properties',
    'MUV_OT_AlignUV_Circle',
    'MUV_OT_AlignUV_Straighten',
    'MUV_OT_AlignUV_Axis',
]


def is_valid_context(context):
    obj = context.object

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

    # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
    # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
    # after the execution
    for space in context.area.spaces:
        if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
            break
    else:
        return False

    return True


@PropertyClassRegistry(legacy=True)
class Properties:
    idname = "align_uv"

    @classmethod
    def init_props(cls, scene):
        scene.muv_align_uv_enabled = BoolProperty(
            name="Align UV Enabled",
            description="Align UV is enabled",
            default=False
        )
        scene.muv_align_uv_transmission = BoolProperty(
            name="Transmission",
            description="Align linked UVs",
            default=False
        )
        scene.muv_align_uv_select = BoolProperty(
            name="Select",
            description="Select UVs which are aligned",
            default=False
        )
        scene.muv_align_uv_vertical = BoolProperty(
            name="Vert-Infl (Vertical)",
            description="Align vertical direction influenced "
                        "by mesh vertex proportion",
            default=False
        )
        scene.muv_align_uv_horizontal = BoolProperty(
            name="Vert-Infl (Horizontal)",
            description="Align horizontal direction influenced "
                        "by mesh vertex proportion",
            default=False
        )
        scene.muv_align_uv_mesh_infl = FloatProperty(
            name="Mesh Influence",
            description="Influence rate of mesh vertex",
            min=0.0,
            max=1.0,
            default=0.0
        )
        scene.muv_align_uv_location = EnumProperty(
            name="Location",
            description="Align location",
            items=[
                ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
                ('MIDDLE', "Middle", "Align to middle"),
                ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
            ],
            default='MIDDLE'
        )

    @classmethod
    def del_props(cls, scene):
        del scene.muv_align_uv_enabled
        del scene.muv_align_uv_transmission
        del scene.muv_align_uv_select
        del scene.muv_align_uv_vertical
        del scene.muv_align_uv_horizontal
        del scene.muv_align_uv_mesh_infl
        del scene.muv_align_uv_location


# get sum vertex length of loop sequences
def get_loop_vert_len(loops):
    length = 0
    for l1, l2 in zip(loops[:-1], loops[1:]):
        diff = l2.vert.co - l1.vert.co
        length = length + abs(diff.length)

    return length


# get sum uv length of loop sequences
def get_loop_uv_len(loops, uv_layer):
    length = 0
    for l1, l2 in zip(loops[:-1], loops[1:]):
        diff = l2[uv_layer].uv - l1[uv_layer].uv
        length = length + abs(diff.length)

    return length


# get center/radius of circle by 3 vertices
def get_circle(v):
    alpha = atan2((v[0].y - v[1].y), (v[0].x - v[1].x)) + math.pi / 2
    beta = atan2((v[1].y - v[2].y), (v[1].x - v[2].x)) + math.pi / 2
    ex = (v[0].x + v[1].x) / 2.0
    ey = (v[0].y + v[1].y) / 2.0
    fx = (v[1].x + v[2].x) / 2.0
    fy = (v[1].y + v[2].y) / 2.0
    cx = (ey - fy - ex * tan(alpha) + fx * tan(beta)) / \
         (tan(beta) - tan(alpha))
    cy = ey - (ex - cx) * tan(alpha)
    center = Vector((cx, cy))

    r = v[0] - center
    radian = r.length

    return center, radian


# get position on circle with same arc length
def calc_v_on_circle(v, center, radius):
    base = v[0]
    theta = atan2(base.y - center.y, base.x - center.x)
    new_v = []
    for i in range(len(v)):
        angle = theta + i * 2 * math.pi / len(v)
        new_v.append(Vector((center.x + radius * sin(angle),
                             center.y + radius * cos(angle))))

    return new_v


@BlClassRegistry(legacy=True)
class MUV_OT_AlignUV_Circle(bpy.types.Operator):

    bl_idname = "uv.muv_align_uv_operator_circle"
    bl_label = "Align UV (Circle)"
    bl_description = "Align UV coordinates to Circle"
    bl_options = {'REGISTER', 'UNDO'}

    transmission = BoolProperty(
        name="Transmission",
        description="Align linked UVs",
        default=False
    )
    select = BoolProperty(
        name="Select",
        description="Select UVs which are aligned",
        default=False
    )

    @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):
        obj = context.active_object
        bm = bmesh.from_edit_mesh(obj.data)
        if common.check_version(2, 73, 0) >= 0:
            bm.faces.ensure_lookup_table()
        uv_layer = bm.loops.layers.uv.verify()

        # loop_seqs[horizontal][vertical][loop]
        loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True)
        if not loop_seqs:
            self.report({'WARNING'}, error)
            return {'CANCELLED'}

        # get circle and new UVs
        uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs]
        c, r = get_circle(uvs[0:3])
        new_uvs = calc_v_on_circle(uvs, c, r)

        # check center UV of circle
        center = loop_seqs[0][-1][0].vert
        for hseq in loop_seqs[1:]:
            if len(hseq[-1]) != 1:
                self.report({'WARNING'}, "Last face must be triangle")
                return {'CANCELLED'}
            if hseq[-1][0].vert != center:
                self.report({'WARNING'}, "Center must be identical")
                return {'CANCELLED'}

        # align to circle
        if self.transmission:
            for hidx, hseq in enumerate(loop_seqs):
                for vidx, pair in enumerate(hseq):
                    all_ = int((len(hseq) + 1) / 2)
                    r = (all_ - int((vidx + 1) / 2)) / all_
                    pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r
                    if self.select:
                        pair[0][uv_layer].select = True

                    if len(pair) < 2:
                        continue
                    # for quad polygon
                    next_hidx = (hidx + 1) % len(loop_seqs)
                    pair[1][uv_layer].uv = c + ((new_uvs[next_hidx]) - c) * r
                    if self.select:
                        pair[1][uv_layer].select = True
        else:
            for hidx, hseq in enumerate(loop_seqs):
                pair = hseq[0]
                pair[0][uv_layer].uv = new_uvs[hidx]
                pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)]
                if self.select:
                    pair[0][uv_layer].select = True
                    pair[1][uv_layer].select = True

        bmesh.update_edit_mesh(obj.data)

        return {'FINISHED'}


# get accumulate vertex lengths of loop sequences
def get_loop_vert_accum_len(loops):
    accum_lengths = [0.0]
    length = 0
    for l1, l2 in zip(loops[:-1], loops[1:]):
        diff = l2.vert.co - l1.vert.co
        length = length + abs(diff.length)
        accum_lengths.extend([length])

    return accum_lengths


# get sum uv length of loop sequences
def get_loop_uv_accum_len(loops, uv_layer):
    accum_lengths = [0.0]
    length = 0
    for l1, l2 in zip(loops[:-1], loops[1:]):
        diff = l2[uv_layer].uv - l1[uv_layer].uv
        length = length + abs(diff.length)
        accum_lengths.extend([length])

    return accum_lengths


# get horizontal differential of UV influenced by mesh vertex
def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
    common.debug_print(
        "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))

    base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy()

    # calculate original length
    hloops = []
    for s in loop_seqs:
        hloops.extend([s[vidx][0], s[vidx][1]])
    total_vlen = get_loop_vert_len(hloops)
    accum_vlens = get_loop_vert_accum_len(hloops)
    total_uvlen = get_loop_uv_len(hloops, uv_layer)
    accum_uvlens = get_loop_uv_accum_len(hloops, uv_layer)
    orig_uvs = [l[uv_layer].uv.copy() for l in hloops]

    # calculate target length
    tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs)
    tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen
    target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
    common.debug_print(target_length)
    common.debug_print(accum_uvlens)

    # calculate target UV
    for i in range(len(accum_uvlens[:-1])):
        # get line segment which UV will be placed
        if ((accum_uvlens[i] <= target_length) and
                (accum_uvlens[i + 1] > target_length)):
            tgt_seg_len = target_length - accum_uvlens[i]
            seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
            uv1 = orig_uvs[i]
            uv2 = orig_uvs[i + 1]
            target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
            break
        elif i == (len(accum_uvlens[:-1]) - 1):
            if accum_uvlens[i + 1] != target_length:
                raise Exception(
                    "Internal Error: horizontal_target_length={}"
                    " is not equal to {}"
                    .format(target_length, accum_uvlens[-1]))
            tgt_seg_len = target_length - accum_uvlens[i]
            seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
            uv1 = orig_uvs[i]
            uv2 = orig_uvs[i + 1]
            target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
            break
    else:
        raise Exception("Internal Error: horizontal_target_length={}"
                        " is not in range {} to {}"
                        .format(target_length, accum_uvlens[0],
                                accum_uvlens[-1]))

    return target_uv


# --------------------- LOOP STRUCTURE ----------------------
#
#  loops[hidx][vidx][pidx]
#     hidx: horizontal index
#     vidx: vertical index
#     pidx: pair index
#
#              <----- horizontal ----->
#
#              (hidx, vidx, pidx) = (0, 3, 0)
#              |      (hidx, vidx, pidx) = (1, 3, 0)
#              v      v
#          ^   o --- oo --- o
#          |   |     ||     |
# vertical |   o --- oo --- o  <- (hidx, vidx, pidx)
#          |   o --- oo --- o          = (1, 2, 1)
#          |   |     ||     |
#          v   o --- oo --- o
#              ^            ^
#              |            (hidx, vidx, pidx) = (1, 0, 1)
#              (hidx, vidx, pidx) = (0, 0, 0)
#
# -----------------------------------------------------------


# get vertical differential of UV influenced by mesh vertex
def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
    common.debug_print(
        "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))

    base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy()

    # calculate original length
    vloops = []
    for s in loop_seqs[hidx]:
        vloops.append(s[pidx])
    total_vlen = get_loop_vert_len(vloops)
    accum_vlens = get_loop_vert_accum_len(vloops)
    total_uvlen = get_loop_uv_len(vloops, uv_layer)
    accum_uvlens = get_loop_uv_accum_len(vloops, uv_layer)
    orig_uvs = [l[uv_layer].uv.copy() for l in vloops]

    # calculate target length
    tgt_noinfl = total_uvlen * int((vidx + 1) / 2) / len(loop_seqs)
    tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen
    target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
    common.debug_print(target_length)
    common.debug_print(accum_uvlens)

    # calculate target UV
    for i in range(len(accum_uvlens[:-1])):
        # get line segment which UV will be placed
        if ((accum_uvlens[i] <= target_length) and
                (accum_uvlens[i + 1] > target_length)):
            tgt_seg_len = target_length - accum_uvlens[i]
            seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
            uv1 = orig_uvs[i]
            uv2 = orig_uvs[i + 1]
            target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
            break
        elif i == (len(accum_uvlens[:-1]) - 1):
            if accum_uvlens[i + 1] != target_length:
                raise Exception("Internal Error: horizontal_target_length={}"
                                " is not equal to {}"
                                .format(target_length, accum_uvlens[-1]))
            tgt_seg_len = target_length - accum_uvlens[i]
            seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
            uv1 = orig_uvs[i]
            uv2 = orig_uvs[i + 1]
            target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
            break
    else:
        raise Exception("Internal Error: horizontal_target_length={}"
                        " is not in range {} to {}"
                        .format(target_length, accum_uvlens[0],
                                accum_uvlens[-1]))

    return target_uv


# get horizontal differential of UV no influenced
def get_hdiff_uv(uv_layer, loop_seqs, hidx):
    base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
    h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv

    return hidx * h_uv / len(loop_seqs)


# get vertical differential of UV no influenced
def get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx):
    base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
    v_uv = loop_seqs[0][-1][0][uv_layer].uv.copy() - base_uv

    hseq = loop_seqs[hidx]
    return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2)


@BlClassRegistry(legacy=True)
class MUV_OT_AlignUV_Straighten(bpy.types.Operator):

    bl_idname = "uv.muv_align_uv_operator_straighten"
    bl_label = "Align UV (Straighten)"
    bl_description = "Straighten UV coordinates"
    bl_options = {'REGISTER', 'UNDO'}

    transmission = BoolProperty(
        name="Transmission",
        description="Align linked UVs",
        default=False
    )
    select = BoolProperty(
        name="Select",
        description="Select UVs which are aligned",
        default=False
    )
    vertical = BoolProperty(
        name="Vert-Infl (Vertical)",
        description="Align vertical direction influenced "
                    "by mesh vertex proportion",
        default=False
    )
    horizontal = BoolProperty(
        name="Vert-Infl (Horizontal)",
        description="Align horizontal direction influenced "
                    "by mesh vertex proportion",
        default=False
    )
    mesh_infl = FloatProperty(
        name="Mesh Influence",
        description="Influence rate of mesh vertex",
        min=0.0,
        max=1.0,
        default=0.0
    )

    @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)

    # selected and paralleled UV loop sequence will be aligned
    def __align_w_transmission(self, loop_seqs, uv_layer):
        base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()

        # calculate diff UVs
        diff_uvs = []
        # hseq[vertical][loop]
        for hidx, hseq in enumerate(loop_seqs):
            # pair[loop]
            diffs = []
            for vidx in range(0, len(hseq), 2):
                if self.horizontal:
                    hdiff_uvs = [
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
                                           self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
                                           self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 0, self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 1, self.mesh_infl),
                    ]
                else:
                    hdiff_uvs = [
                        get_hdiff_uv(uv_layer, loop_seqs, hidx),
                        get_hdiff_uv(uv_layer, loop_seqs, hidx + 1),
                        get_hdiff_uv(uv_layer, loop_seqs, hidx),
                        get_hdiff_uv(uv_layer, loop_seqs, hidx + 1)
                    ]
                if self.vertical:
                    vdiff_uvs = [
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
                                           self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
                                           self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 0, self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 1, self.mesh_infl),
                    ]
                else:
                    vdiff_uvs = [
                        get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
                    ]
                diffs.append([hdiff_uvs, vdiff_uvs])
            diff_uvs.append(diffs)

        # update UV
        for hseq, diffs in zip(loop_seqs, diff_uvs):
            for vidx in range(0, len(hseq), 2):
                loops = [
                    hseq[vidx][0], hseq[vidx][1],
                    hseq[vidx + 1][0], hseq[vidx + 1][1]
                ]
                for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
                                           diffs[int(vidx / 2)][1]):
                    l[uv_layer].uv = base_uv + hdiff + vdiff
                    if self.select:
                        l[uv_layer].select = True

    # only selected UV loop sequence will be aligned
    def __align_wo_transmission(self, loop_seqs, uv_layer):
        base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()

        h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv
        for hidx, hseq in enumerate(loop_seqs):
            # only selected loop pair is targeted
            pair = hseq[0]
            hdiff_uv_0 = hidx * h_uv / len(loop_seqs)
            hdiff_uv_1 = (hidx + 1) * h_uv / len(loop_seqs)
            pair[0][uv_layer].uv = base_uv + hdiff_uv_0
            pair[1][uv_layer].uv = base_uv + hdiff_uv_1
            if self.select:
                pair[0][uv_layer].select = True
                pair[1][uv_layer].select = True

    def __align(self, loop_seqs, uv_layer):
        if self.transmission:
            self.__align_w_transmission(loop_seqs, uv_layer)
        else:
            self.__align_wo_transmission(loop_seqs, uv_layer)

    def execute(self, context):
        obj = context.active_object
        bm = bmesh.from_edit_mesh(obj.data)
        if common.check_version(2, 73, 0) >= 0:
            bm.faces.ensure_lookup_table()
        uv_layer = bm.loops.layers.uv.verify()

        # loop_seqs[horizontal][vertical][loop]
        loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
        if not loop_seqs:
            self.report({'WARNING'}, error)
            return {'CANCELLED'}

        # align
        self.__align(loop_seqs, uv_layer)

        bmesh.update_edit_mesh(obj.data)

        return {'FINISHED'}


@BlClassRegistry(legacy=True)
class MUV_OT_AlignUV_Axis(bpy.types.Operator):

    bl_idname = "uv.muv_align_uv_operator_axis"
    bl_label = "Align UV (XY-Axis)"
    bl_description = "Align UV to XY-axis"
    bl_options = {'REGISTER', 'UNDO'}

    transmission = BoolProperty(
        name="Transmission",
        description="Align linked UVs",
        default=False
    )
    select = BoolProperty(
        name="Select",
        description="Select UVs which are aligned",
        default=False
    )
    vertical = BoolProperty(
        name="Vert-Infl (Vertical)",
        description="Align vertical direction influenced "
                    "by mesh vertex proportion",
        default=False
    )
    horizontal = BoolProperty(
        name="Vert-Infl (Horizontal)",
        description="Align horizontal direction influenced "
                    "by mesh vertex proportion",
        default=False
    )
    location = EnumProperty(
        name="Location",
        description="Align location",
        items=[
            ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
            ('MIDDLE', "Middle", "Align to middle"),
            ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
        ],
        default='MIDDLE'
    )
    mesh_infl = FloatProperty(
        name="Mesh Influence",
        description="Influence rate of mesh vertex",
        min=0.0,
        max=1.0,
        default=0.0
    )

    @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)

    # get min/max of UV
    def __get_uv_max_min(self, loop_seqs, uv_layer):
        uv_max = Vector((-1000000.0, -1000000.0))
        uv_min = Vector((1000000.0, 1000000.0))
        for hseq in loop_seqs:
            for l in hseq[0]:
                uv = l[uv_layer].uv
                uv_max.x = max(uv.x, uv_max.x)
                uv_max.y = max(uv.y, uv_max.y)
                uv_min.x = min(uv.x, uv_min.x)
                uv_min.y = min(uv.y, uv_min.y)

        return uv_max, uv_min

    # get UV differentiation when UVs are aligned to X-axis
    def __get_x_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
                                    width, height):
        diff_uvs = []
        for hidx, hseq in enumerate(loop_seqs):
            pair = hseq[0]
            luv0 = pair[0][uv_layer]
            luv1 = pair[1][uv_layer]
            target_uv0 = Vector((0.0, 0.0))
            target_uv1 = Vector((0.0, 0.0))
            if self.location == 'RIGHT_BOTTOM':
                target_uv0.y = target_uv1.y = uv_min.y
            elif self.location == 'MIDDLE':
                target_uv0.y = target_uv1.y = uv_min.y + height * 0.5
            elif self.location == 'LEFT_TOP':
                target_uv0.y = target_uv1.y = uv_min.y + height
            if luv0.uv.x < luv1.uv.x:
                target_uv0.x = uv_min.x + hidx * width / len(loop_seqs)
                target_uv1.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
            else:
                target_uv0.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
                target_uv1.x = uv_min.x + hidx * width / len(loop_seqs)
            diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])

        return diff_uvs

    # get UV differentiation when UVs are aligned to Y-axis
    def __get_y_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
                                    width, height):
        diff_uvs = []
        for hidx, hseq in enumerate(loop_seqs):
            pair = hseq[0]
            luv0 = pair[0][uv_layer]
            luv1 = pair[1][uv_layer]
            target_uv0 = Vector((0.0, 0.0))
            target_uv1 = Vector((0.0, 0.0))
            if self.location == 'RIGHT_BOTTOM':
                target_uv0.x = target_uv1.x = uv_min.x + width
            elif self.location == 'MIDDLE':
                target_uv0.x = target_uv1.x = uv_min.x + width * 0.5
            elif self.location == 'LEFT_TOP':
                target_uv0.x = target_uv1.x = uv_min.x
            if luv0.uv.y < luv1.uv.y:
                target_uv0.y = uv_min.y + hidx * height / len(loop_seqs)
                target_uv1.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
            else:
                target_uv0.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
                target_uv1.y = uv_min.y + hidx * height / len(loop_seqs)
            diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])

        return diff_uvs

    # only selected UV loop sequence will be aligned along to X-axis
    def __align_to_x_axis_wo_transmission(self, loop_seqs, uv_layer,
                                          uv_min, width, height):
        # reverse if the UV coordinate is not sorted by position
        need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
            loop_seqs[-1][0][0][uv_layer].uv.x
        if need_revese:
            loop_seqs.reverse()
            for hidx, hseq in enumerate(loop_seqs):
                for vidx, pair in enumerate(hseq):
                    tmp = loop_seqs[hidx][vidx][0]
                    loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
                    loop_seqs[hidx][vidx][1] = tmp

        # get UV differential
        diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer,
                                                    uv_min, width, height)

        # update UV
        for hseq, duv in zip(loop_seqs, diff_uvs):
            pair = hseq[0]
            luv0 = pair[0][uv_layer]
            luv1 = pair[1][uv_layer]
            luv0.uv = luv0.uv + duv[0]
            luv1.uv = luv1.uv + duv[1]

    # only selected UV loop sequence will be aligned along to Y-axis
    def __align_to_y_axis_wo_transmission(self, loop_seqs, uv_layer,
                                          uv_min, width, height):
        # reverse if the UV coordinate is not sorted by position
        need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
            loop_seqs[-1][0][0][uv_layer].uv.y
        if need_revese:
            loop_seqs.reverse()
            for hidx, hseq in enumerate(loop_seqs):
                for vidx, pair in enumerate(hseq):
                    tmp = loop_seqs[hidx][vidx][0]
                    loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
                    loop_seqs[hidx][vidx][1] = tmp

        # get UV differential
        diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer,
                                                    uv_min, width, height)

        # update UV
        for hseq, duv in zip(loop_seqs, diff_uvs):
            pair = hseq[0]
            luv0 = pair[0][uv_layer]
            luv1 = pair[1][uv_layer]
            luv0.uv = luv0.uv + duv[0]
            luv1.uv = luv1.uv + duv[1]

    # selected and paralleled UV loop sequence will be aligned along to X-axis
    def __align_to_x_axis_w_transmission(self, loop_seqs, uv_layer,
                                         uv_min, width, height):
        # reverse if the UV coordinate is not sorted by position
        need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
            loop_seqs[-1][0][0][uv_layer].uv.x
        if need_revese:
            loop_seqs.reverse()
            for hidx, hseq in enumerate(loop_seqs):
                for vidx in range(len(hseq)):
                    tmp = loop_seqs[hidx][vidx][0]
                    loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
                    loop_seqs[hidx][vidx][1] = tmp

        # get offset UVs when the UVs are aligned to X-axis
        align_diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer,
                                                          uv_min, width,
                                                          height)
        base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
        offset_uvs = []
        for hseq, aduv in zip(loop_seqs, align_diff_uvs):
            luv0 = hseq[0][0][uv_layer]
            luv1 = hseq[0][1][uv_layer]
            offset_uvs.append([luv0.uv + aduv[0] - base_uv,
                               luv1.uv + aduv[1] - base_uv])

        # get UV differential
        diff_uvs = []
        # hseq[vertical][loop]
        for hidx, hseq in enumerate(loop_seqs):
            # pair[loop]
            diffs = []
            for vidx in range(0, len(hseq), 2):
                if self.horizontal:
                    hdiff_uvs = [
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
                                           self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
                                           self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 0, self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 1, self.mesh_infl),
                    ]
                    hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y
                    hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y
                    hdiff_uvs[2].y = hdiff_uvs[2].y + offset_uvs[hidx][0].y
                    hdiff_uvs[3].y = hdiff_uvs[3].y + offset_uvs[hidx][1].y
                else:
                    hdiff_uvs = [
                        offset_uvs[hidx][0],
                        offset_uvs[hidx][1],
                        offset_uvs[hidx][0],
                        offset_uvs[hidx][1],
                    ]
                if self.vertical:
                    vdiff_uvs = [
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
                                           self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
                                           self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 0, self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 1, self.mesh_infl),
                    ]
                else:
                    vdiff_uvs = [
                        get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
                    ]
                diffs.append([hdiff_uvs, vdiff_uvs])
            diff_uvs.append(diffs)

        # update UV
        for hseq, diffs in zip(loop_seqs, diff_uvs):
            for vidx in range(0, len(hseq), 2):
                loops = [
                    hseq[vidx][0], hseq[vidx][1],
                    hseq[vidx + 1][0], hseq[vidx + 1][1]
                ]
                for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
                                           diffs[int(vidx / 2)][1]):
                    l[uv_layer].uv = base_uv + hdiff + vdiff
                    if self.select:
                        l[uv_layer].select = True

    # selected and paralleled UV loop sequence will be aligned along to Y-axis
    def __align_to_y_axis_w_transmission(self, loop_seqs, uv_layer,
                                         uv_min, width, height):
        # reverse if the UV coordinate is not sorted by position
        need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
            loop_seqs[-1][0][-1][uv_layer].uv.y
        if need_revese:
            loop_seqs.reverse()
            for hidx, hseq in enumerate(loop_seqs):
                for vidx in range(len(hseq)):
                    tmp = loop_seqs[hidx][vidx][0]
                    loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
                    loop_seqs[hidx][vidx][1] = tmp

        # get offset UVs when the UVs are aligned to Y-axis
        align_diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer,
                                                          uv_min, width,
                                                          height)
        base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
        offset_uvs = []
        for hseq, aduv in zip(loop_seqs, align_diff_uvs):
            luv0 = hseq[0][0][uv_layer]
            luv1 = hseq[0][1][uv_layer]
            offset_uvs.append([luv0.uv + aduv[0] - base_uv,
                               luv1.uv + aduv[1] - base_uv])

        # get UV differential
        diff_uvs = []
        # hseq[vertical][loop]
        for hidx, hseq in enumerate(loop_seqs):
            # pair[loop]
            diffs = []
            for vidx in range(0, len(hseq), 2):
                if self.horizontal:
                    hdiff_uvs = [
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
                                           self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
                                           self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 0, self.mesh_infl),
                        get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 1, self.mesh_infl),
                    ]
                    hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x
                    hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x
                    hdiff_uvs[2].x = hdiff_uvs[2].x + offset_uvs[hidx][0].x
                    hdiff_uvs[3].x = hdiff_uvs[3].x + offset_uvs[hidx][1].x
                else:
                    hdiff_uvs = [
                        offset_uvs[hidx][0],
                        offset_uvs[hidx][1],
                        offset_uvs[hidx][0],
                        offset_uvs[hidx][1],
                    ]
                if self.vertical:
                    vdiff_uvs = [
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
                                           self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
                                           self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 0, self.mesh_infl),
                        get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
                                           hidx, 1, self.mesh_infl),
                    ]
                else:
                    vdiff_uvs = [
                        get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
                        get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
                    ]
                diffs.append([hdiff_uvs, vdiff_uvs])
            diff_uvs.append(diffs)

        # update UV
        for hseq, diffs in zip(loop_seqs, diff_uvs):
            for vidx in range(0, len(hseq), 2):
                loops = [
                    hseq[vidx][0], hseq[vidx][1],
                    hseq[vidx + 1][0], hseq[vidx + 1][1]
                ]
                for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
                                           diffs[int(vidx / 2)][1]):
                    l[uv_layer].uv = base_uv + hdiff + vdiff
                    if self.select:
                        l[uv_layer].select = True

    def __align(self, loop_seqs, uv_layer, uv_min, width, height):
        # align along to x-axis
        if width > height:
            if self.transmission:
                self.__align_to_x_axis_w_transmission(loop_seqs, uv_layer,
                                                      uv_min, width, height)
            else:
                self.__align_to_x_axis_wo_transmission(loop_seqs, uv_layer,
                                                       uv_min, width, height)
        # align along to y-axis
        else:
            if self.transmission:
                self.__align_to_y_axis_w_transmission(loop_seqs, uv_layer,
                                                      uv_min, width, height)
            else:
                self.__align_to_y_axis_wo_transmission(loop_seqs, uv_layer,
                                                       uv_min, width, height)

    def execute(self, context):
        obj = context.active_object
        bm = bmesh.from_edit_mesh(obj.data)
        if common.check_version(2, 73, 0) >= 0:
            bm.faces.ensure_lookup_table()
        uv_layer = bm.loops.layers.uv.verify()

        # loop_seqs[horizontal][vertical][loop]
        loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
        if not loop_seqs:
            self.report({'WARNING'}, error)
            return {'CANCELLED'}

        # get height and width
        uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer)
        width = uv_max.x - uv_min.x
        height = uv_max.y - uv_min.y

        self.__align(loop_seqs, uv_layer, uv_min, width, height)

        bmesh.update_edit_mesh(obj.data)

        return {'FINISHED'}
