### 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 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.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####

#python tip: from-imports don't save memory.
#They execute and cache the entire module just like a regular import.

import bpy
import bmesh

from mathutils import Vector
from mathutils.geometry import (
        intersect_point_line,
        intersect_line_line,
        intersect_ray_tri,
        )

from .snap_context_l import SnapContext


def get_units_info(scale, unit_system, separate_units):
    if unit_system == 'METRIC':
        scale_steps = ((1000, 'km'), (1, 'm'), (1 / 100, 'cm'),
            (1 / 1000, 'mm'), (1 / 1000000, '\u00b5m'))
    elif unit_system == 'IMPERIAL':
        scale_steps = ((5280, 'mi'), (1, '\''),
            (1 / 12, '"'), (1 / 12000, 'thou'))
        scale /= 0.3048  # BU to feet
    else:
        scale_steps = ((1, ' BU'),)
        separate_units = False

    return (scale, scale_steps, separate_units)


def convert_distance(val, units_info, precision=5):
    scale, scale_steps, separate_units = units_info
    sval = val * scale
    idx = 0
    while idx < len(scale_steps) - 1:
        if sval >= scale_steps[idx][0]:
            break
        idx += 1
    factor, suffix = scale_steps[idx]
    sval /= factor
    if not separate_units or idx == len(scale_steps) - 1:
        dval = str(round(sval, precision)) + suffix
    else:
        ival = int(sval)
        dval = str(round(ival, precision)) + suffix
        fval = sval - ival
        idx += 1
        while idx < len(scale_steps):
            fval *= scale_steps[idx - 1][0] / scale_steps[idx][0]
            if fval >= 1:
                dval += ' ' \
                    + ("%.1f" % fval) \
                    + scale_steps[idx][1]
                break
            idx += 1

    return dval


def location_3d_to_region_2d(region, rv3d, coord):
    prj = rv3d.perspective_matrix @ Vector((coord[0], coord[1], coord[2], 1.0))
    width_half = region.width / 2.0
    height_half = region.height / 2.0
    return Vector((width_half + width_half * (prj.x / prj.w),
                   height_half + height_half * (prj.y / prj.w)
                   ))


def out_Location(rv3d, orig, vector):
    view_matrix = rv3d.view_matrix
    v1 = (int(view_matrix[0][0]*1.5), int(view_matrix[0][1]*1.5), int(view_matrix[0][2]*1.5))
    v2 = (int(view_matrix[1][0]*1.5), int(view_matrix[1][1]*1.5), int(view_matrix[1][2]*1.5))

    hit = intersect_ray_tri((1,0,0), (0,1,0), (0,0,0), (vector), (orig), False)
    if hit is None:
        hit = intersect_ray_tri(v1, v2, (0,0,0), (vector), (orig), False)
    if hit is None:
        hit = intersect_ray_tri(v1, v2, (0,0,0), (-vector), (orig), False)
    if hit is None:
        hit = Vector()
    return hit


def get_snap_bm_geom(sctx, main_snap_obj, mcursor):

    r_snp_obj, r_loc, r_elem, r_elem_co = sctx.snap_get(mcursor, main_snap_obj)
    r_view_vector, r_orig = sctx.last_ray
    r_bm = None
    r_bm_geom = None

    if r_snp_obj is not None:
        obj = r_snp_obj.data[0]

        if obj.type == 'MESH' and obj.data.is_editmode:
            r_bm = bmesh.from_edit_mesh(obj.data)
            if len(r_elem) == 1:
                r_bm_geom = r_bm.verts[r_elem[0]]

            elif len(r_elem) == 2:
                try:
                    v1 = r_bm.verts[r_elem[0]]
                    v2 = r_bm.verts[r_elem[1]]
                    r_bm_geom = r_bm.edges.get([v1, v2])
                except IndexError:
                    r_bm.verts.ensure_lookup_table()

            elif len(r_elem) == 3:
                tri = [
                    r_bm.verts[r_elem[0]],
                    r_bm.verts[r_elem[1]],
                    r_bm.verts[r_elem[2]],
                ]

                faces = set(tri[0].link_faces).intersection(tri[1].link_faces, tri[2].link_faces)
                if len(faces) == 1:
                    r_bm_geom = faces.pop()
                else:
                    i = -2
                    edge = None
                    while not edge and i != 1:
                        edge = r_bm.edges.get([tri[i], tri[i + 1]])
                        i += 1
                    if edge:
                        for l in edge.link_loops:
                            if l.link_loop_next.vert == tri[i] or l.link_loop_prev.vert == tri[i - 2]:
                                r_bm_geom = l.face
                                break
        if r_loc is None:
            r_loc = r_elem_co[0]

    return r_snp_obj, r_loc, r_elem, r_elem_co, r_view_vector, r_orig, r_bm, r_bm_geom


class SnapCache(object):
    __slots__ = 'edge', 'face'

    class Edge:
        __slots__ = 'snp_obj', 'elem', 'vmid', 'vperp', 'v2dmid', 'v2dperp', 'is_increment'

        def __init__(self):
            self.snp_obj = None
            self.elem = None
            self.vmid = None
            self.vperp = None
            self.v2dmid = None
            self.v2dperp = None
            self.is_increment = False

    class Face:
        __slots__ = 'bm_face', 'vmid', 'v2dmid'

        def __init__(self):
            self.bm_face = None

    def __init__(self):
        self.edge = self.Edge()
        self.face = self.Face()

    def clear(self):
        self.edge.snp_obj = self.face.bm_face = None

_snap_cache = SnapCache()


def snap_utilities(
        sctx, main_snap_obj,
        mcursor,
        constrain = None,
        previous_vert = None,
        increment = 0.0):

    snp_obj, loc, elem, elem_co, view_vector, orig, bm, bm_geom = get_snap_bm_geom(sctx, main_snap_obj, mcursor)

    is_increment = False
    r_loc = None
    r_type = 'OUT'
    r_len = 0.0

    if not snp_obj:
        is_increment = True
        if constrain:
            end = orig + view_vector
            t_loc = intersect_line_line(constrain[0], constrain[1], orig, end)
            if t_loc is None:
                t_loc = constrain
            r_loc = t_loc[0]
        else:
            r_loc = out_Location(sctx.rv3d, orig, view_vector)

    elif len(elem) == 1:
        r_type = 'VERT'

        if constrain:
            r_loc = intersect_point_line(loc, constrain[0], constrain[1])[0]
        else:
            r_loc = loc

    elif len(elem) == 2:
        r_type = 'EDGE'

        if _snap_cache.edge.snp_obj is not snp_obj or not (elem == _snap_cache.edge.elem).all():
            _snap_cache.edge.snp_obj = snp_obj
            _snap_cache.edge.elem = elem

            v0 = elem_co[0]
            v1 = elem_co[1]
            _snap_cache.edge.vmid = 0.5 * (v0 + v1)
            _snap_cache.edge.v2dmid = location_3d_to_region_2d(
                    sctx.region, sctx.rv3d, _snap_cache.edge.vmid)

            if previous_vert and (not bm_geom or previous_vert not in bm_geom.verts):
                pvert_co = main_snap_obj.mat @ previous_vert.co
                perp_point = intersect_point_line(pvert_co, v0, v1)
                _snap_cache.edge.vperp = perp_point[0]
                #factor = point_perpendicular[1]
                _snap_cache.edge.v2dperp = location_3d_to_region_2d(sctx.region, sctx.rv3d, perp_point[0])
                _snap_cache.edge.is_increment = False
            else:
                _snap_cache.edge.is_increment = True

            #else: _snap_cache.edge.v2dperp = None

        if constrain:
            t_loc = intersect_line_line(constrain[0], constrain[1], elem_co[0], elem_co[1])
            if t_loc is None:
                is_increment = True
                end = orig + view_vector
                t_loc = intersect_line_line(constrain[0], constrain[1], orig, end)
            r_loc = t_loc[0]

        elif _snap_cache.edge.v2dperp and\
            abs(_snap_cache.edge.v2dperp[0] - mcursor[0]) < 10 and abs(_snap_cache.edge.v2dperp[1] - mcursor[1]) < 10:
                r_type = 'PERPENDICULAR'
                r_loc = _snap_cache.edge.vperp

        elif abs(_snap_cache.edge.v2dmid[0] - mcursor[0]) < 10 and abs(_snap_cache.edge.v2dmid[1] - mcursor[1]) < 10:
                r_type = 'CENTER'
                r_loc = _snap_cache.edge.vmid

        else:
                r_loc = loc
                is_increment = _snap_cache.edge.is_increment

    elif len(elem) == 3:
        r_type = 'FACE'

#        vmid = v2dmid = None
#        if bm_geom and _snap_cache.face is not bm_geom:
#            _snap_cache.face.bm_face = bm_geom
#            vmid = _snap_cache.face.vmid = bm_geom.calc_center_median()
#            v2dmid = _snap_cache.face.v2dmid = location_3d_to_region_2d(
#                    sctx.region, sctx.rv3d, _snap_cache.face.vmid)

        if constrain:
            is_increment = False
#            elem_world_co = [snp_obj.mat @ co for co in elem_co]
#            ray_dir = constrain[1] - constrain[0]
#            r_loc = intersect_ray_tri(*elem_world_co, ray_dir, constrain[0], False)
#            if r_loc is None:
#                r_loc = intersect_ray_tri(*elem_world_co, -ray_dir, constrain[0], False)
#            if r_loc is None:
            r_loc = intersect_point_line(loc, constrain[0], constrain[1])[0]

#        elif v2dmid and abs(v2dmid[0] - mcursor[0]) < 10 and abs(v2dmid[1] - mcursor[1]) < 10:
#            r_type = 'CENTER'
#            r_loc = vmid

        else:
            r_loc = loc
            is_increment = True

    if previous_vert:
        pv_co = main_snap_obj.mat @ previous_vert.co
        vec = r_loc - pv_co
        if is_increment and increment:
            r_len = round((1 / increment) * vec.length) * increment
            r_loc = r_len * vec.normalized() + pv_co
        else:
            r_len = vec.length

    return snp_obj, loc, r_loc, r_type, bm, bm_geom, r_len

snap_utilities.cache = _snap_cache
