# -*- coding: utf-8 -*-

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

bl_info = {
    "name": "Pen Tool",
    "author": "zmj100",
    "version": (0, 3, 1),
    "blender": (2, 78, 0),
    "location": "View3D > Tool Shelf",
    "description": "",
    "warning": "",
    "wiki_url": "",
    "category": "Mesh",
    }

import bpy
import bpy_extras
import blf
import bgl
import bmesh
from bpy.types import (
        Operator,
        PropertyGroup,
        Panel
        )
from bpy.props import (
        FloatProperty,
        IntProperty,
        PointerProperty,
        BoolProperty
        )
from bpy_extras.view3d_utils import (
        region_2d_to_location_3d,
        location_3d_to_region_2d,
        )
from mathutils import (
        Vector,
        Matrix,
        )
from math import degrees


def edit_mode_out():
    bpy.ops.object.mode_set(mode='OBJECT')


def edit_mode_in():
    bpy.ops.object.mode_set(mode='EDIT')


def get_direction_(bme, list_, ob_act):
    n = len(list_)
    for i in range(n):
        p = ob_act.matrix_world * (bme.verts[list_[i]].co).copy()
        p1 = ob_act.matrix_world * (bme.verts[list_[(i - 1) % n]].co).copy()
        p2 = ob_act.matrix_world * (bme.verts[list_[(i + 1) % n]].co).copy()

        if p == p1 or p == p2:
            continue
        ang = round(degrees((p - p1).angle((p - p2), any)))
        if ang == 0 or ang == 180:
            continue
        elif ang != 0 or ang != 180:
            return(((p - p1).cross((p - p2))).normalized())
            break


def store_restore_view(context, store=True):
    if not context.scene.pen_tool_props.restore_view:
        return

    if store is True:
        # copy the original view_matrix and rotation for restoring
        pt_buf.store_view_matrix = context.space_data.region_3d.view_matrix.copy()
        pt_buf.view_location = context.space_data.region_3d.view_location.copy()
    else:
        context.space_data.region_3d.view_matrix = pt_buf.store_view_matrix
        context.space_data.region_3d.view_location = pt_buf.view_location


def align_view_to_face_(context, bme, f):
    store_restore_view(context, True)
    ob_act = context.active_object
    list_e = [[v.index for v in e.verts] for e in f.edges][0]
    vec0 = -get_direction_(bme, [v.index for v in f.verts], ob_act)
    vec1 = ((ob_act.matrix_world * bme.verts[list_e[0]].co.copy()) -
            (ob_act.matrix_world * bme.verts[list_e[1]].co.copy())).normalized()
    vec2 = (vec0.cross(vec1)).normalized()
    context.space_data.region_3d.view_matrix = ((Matrix((vec1, vec2, vec0))).to_4x4()).inverted()
    context.space_data.region_3d.view_location = f.calc_center_median()


def draw_callback_px(self, context):
    font_id = 0
    alpha = context.scene.pen_tool_props.a
    font_size = context.scene.pen_tool_props.fs

    bgl.glColor4f(0.0, 0.6, 1.0, alpha)
    bgl.glPointSize(4.0)
    bgl.glBegin(bgl.GL_POINTS)
    bgl.glVertex2f(pt_buf.x, pt_buf.y)
    bgl.glEnd()
    bgl.glDisable(bgl.GL_BLEND)

    # location 3d
    if context.scene.pen_tool_props.b2 is True:
        mloc3d = region_2d_to_location_3d(
                        context.region,
                        context.space_data.region_3d, Vector((pt_buf.x, pt_buf.y)),
                        pt_buf.depth_location
                        )
        blf.position(font_id, pt_buf.x + 15, pt_buf.y - 15, 0)
        blf.size(font_id, font_size, context.preferences.system.dpi)
        blf.draw(font_id,
                '(' + str(round(mloc3d[0], 4)) + ', ' + str(round(mloc3d[1], 4)) +
                ', ' + str(round(mloc3d[2], 4)) + ')')

    n = len(pt_buf.list_m_loc_3d)

    if n != 0:
        # add points
        bgl.glEnable(bgl.GL_BLEND)
        bgl.glPointSize(4.0)
        bgl.glBegin(bgl.GL_POINTS)
        for i in pt_buf.list_m_loc_3d:
            loc_0 = location_3d_to_region_2d(
                            context.region, context.space_data.region_3d, i
                            )
            bgl.glVertex2f(loc_0[0], loc_0[1])
        bgl.glEnd()
        bgl.glDisable(bgl.GL_BLEND)

        # text next to the mouse
        m_loc_3d = region_2d_to_location_3d(
                            context.region,
                            context.space_data.region_3d, Vector((pt_buf.x, pt_buf.y)),
                            pt_buf.depth_location
                            )
        vec0 = pt_buf.list_m_loc_3d[-1] - m_loc_3d
        blf.position(font_id, pt_buf.x + 15, pt_buf.y + 15, 0)
        blf.size(font_id, font_size, context.preferences.system.dpi)
        blf.draw(font_id, str(round(vec0.length, 4)))

        #  angle first after mouse
        if n >= 2:
            vec1 = pt_buf.list_m_loc_3d[-2] - pt_buf.list_m_loc_3d[-1]
            if vec0.length == 0.0 or vec1.length == 0.0:
                pass
            else:
                ang = vec0.angle(vec1)

                if round(degrees(ang), 2) == 180.0:
                    text_0 = '0.0'
                elif round(degrees(ang), 2) == 0.0:
                    text_0 = '180.0'
                else:
                    text_0 = str(round(degrees(ang), 2))

                loc_4 = location_3d_to_region_2d(
                                        context.region,
                                        context.space_data.region_3d,
                                        pt_buf.list_m_loc_3d[-1]
                                        )
                bgl.glColor4f(0.0, 1.0, 0.525, alpha)
                blf.position(font_id, loc_4[0] + 10, loc_4[1] + 10, 0)
                blf.size(font_id, font_size, context.preferences.system.dpi)
                blf.draw(font_id, text_0 + '')

        bgl.glLineStipple(4, 0x5555)
        bgl.glEnable(bgl.GL_LINE_STIPPLE)      # enable line stipple

        bgl.glColor4f(0.0, 0.6, 1.0, alpha)
        #  draw line between last point and mouse
        bgl.glEnable(bgl.GL_BLEND)
        bgl.glBegin(bgl.GL_LINES)
        loc_1 = location_3d_to_region_2d(
                                context.region,
                                context.space_data.region_3d,
                                pt_buf.list_m_loc_3d[-1]
                                )
        bgl.glVertex2f(loc_1[0], loc_1[1])
        bgl.glVertex2f(pt_buf.x, pt_buf.y)
        bgl.glEnd()
        bgl.glDisable(bgl.GL_BLEND)

        # draw lines between points
        bgl.glEnable(bgl.GL_BLEND)
        bgl.glBegin(bgl.GL_LINE_STRIP)
        for j in pt_buf.list_m_loc_3d:
            loc_2 = location_3d_to_region_2d(context.region, context.space_data.region_3d, j)
            bgl.glVertex2f(loc_2[0], loc_2[1])
        bgl.glEnd()
        bgl.glDisable(bgl.GL_BLEND)

        bgl.glDisable(bgl.GL_LINE_STIPPLE)      # disable line stipple

        # draw line length between points
        if context.scene.pen_tool_props.b1 is True:
            for k in range(n - 1):
                loc_3 = location_3d_to_region_2d(
                                context.region, context.space_data.region_3d,
                                (pt_buf.list_m_loc_3d[k] + pt_buf.list_m_loc_3d[(k + 1) % n]) * 0.5
                                )
                blf.position(font_id, loc_3[0] + 10, loc_3[1] + 10, 0)
                blf.size(font_id, font_size, context.preferences.system.dpi)
                blf.draw(font_id,
                         str(round((pt_buf.list_m_loc_3d[k] - pt_buf.list_m_loc_3d[(k + 1) % n]).length, 4)))

        # draw all angles
        if context.scene.pen_tool_props.b0 is True:
            for h in range(n - 1):
                if n >= 2:
                    if h == 0:
                        pass
                    else:
                        vec_ = pt_buf.list_m_loc_3d[h] - pt_buf.list_m_loc_3d[(h - 1) % n]
                        vec_1_ = pt_buf.list_m_loc_3d[h]
                        vec_2_ = pt_buf.list_m_loc_3d[(h - 1) % n]
                        if vec_.length == 0.0 or vec_1_.length == 0.0 or vec_2_.length == 0.0:
                            pass
                        else:
                            ang = vec_.angle(vec_1_ - vec_2_)
                            if round(degrees(ang)) == 0.0:
                                pass
                            else:
                                loc_4 = location_3d_to_region_2d(
                                                context.region, context.space_data.region_3d,
                                                pt_buf.list_m_loc_3d[h]
                                                )
                                bgl.glColor4f(0.0, 1.0, 0.525, alpha)
                                blf.position(font_id, loc_4[0] + 10, loc_4[1] + 10, 0)
                                blf.size(font_id, font_size, context.preferences.system.dpi)
                                blf.draw(font_id, str(round(degrees(ang), 2)) + '')
    # tools on / off
    bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
    blf.position(font_id, self.text_location, 20, 0)
    blf.size(font_id, 15, context.preferences.system.dpi)
    blf.draw(font_id, "Draw On")
    blf.position(font_id, self.text_location, 40, 0)
    blf.draw(font_id, "Extrude On" if pt_buf.ctrl else "Extrude Off")


class pen_tool_properties(PropertyGroup):
    a: FloatProperty(
            name="Alpha",
            description="Set Font Alpha",
            default=1.0,
            min=0.1, max=1.0,
            step=10,
            precision=1
            )
    fs: IntProperty(
            name="Size",
            description="Set Font Size",
            default=14,
            min=12, max=40,
            step=1
            )
    b0: BoolProperty(
            name="Angles",
            description="Display All Angles on Drawn Edges",
            default=False
            )
    b1: BoolProperty(
            name="Edge Length",
            description="Display All Lengths of Drawn Edges",
            default=False
            )
    b2: BoolProperty(
            name="Mouse Location 3D",
            description="Display the location coordinates of the mouse cursor",
            default=False
            )
    restore_view: BoolProperty(
            name="Restore View",
            description="After the tool has finished, is the Viewport restored\n"
                        "to it's previous state",
            default=True
            )


class pt_buf():
    list_m_loc_2d = []
    list_m_loc_3d = []
    x = 0
    y = 0
    sws = 'off'
    depth_location = Vector((0.0, 0.0, 0.0))
    alt = False
    shift = False
    ctrl = False
    store_view_matrix = Matrix()
    view_location = (0.0, 0.0, 0.0)


# ------ Panel  ------
class pen_tool_panel(Panel):
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "Tools"
    bl_label = "Pen Tool"
    bl_context = "mesh_edit"
    bl_options = {"DEFAULT_CLOSED"}

    def draw(self, context):
        layout = self.layout
        pen_tool_props = context.scene.pen_tool_props

        if pt_buf.sws == "on":
            layout.active = False
            layout.label(text="Pen Tool Active", icon="INFO")
        else:
            col = layout.column(align=True)
            col.label(text="Font:")
            col.prop(pen_tool_props, "fs", text="Size", slider=True)
            col.prop(pen_tool_props, "a", text="Alpha", slider=True)

            col = layout.column(align=True)
            col.label(text="Settings:")
            col.prop(pen_tool_props, "b0", text="Angles", toggle=True)
            col.prop(pen_tool_props, "b1", text="Edge Length", toggle=True)
            col.prop(pen_tool_props, "b2", text="Mouse Location 3D", toggle=True)
            col.prop(pen_tool_props, "restore_view", text="Restore View", toggle=True)

            split = layout.split(0.80, align=True)
            split.operator("pen_tool.operator", text="Draw")
            split.operator("mesh.extra_tools_help",
                           icon="LAYER_USED").help_ids = "mesh_pen_tool"


# Operator
class pen_tool_operator(Operator):
    bl_idname = "pen_tool.operator"
    bl_label = "Pen Tool"
    bl_options = {"REGISTER", "UNDO", "INTERNAL"}

    text_location: IntProperty(
            name="",
            default=0,
            options={'HIDDEN'}
            )

    @classmethod
    def poll(cls, context):
        # do not run in object mode
        return (context.active_object and context.active_object.type == 'MESH' and
                context.mode == 'EDIT_MESH')

    def execute(self, context):
        edit_mode_out()
        ob_act = context.active_object
        bme = bmesh.new()
        bme.from_mesh(ob_act.data)

        mtrx = ob_act.matrix_world.inverted()   # ob_act matrix world inverted

        # add vertices
        list_ = []
        for i in pt_buf.list_m_loc_3d:
            bme.verts.new(mtrx * i)
            bme.verts.index_update()
            bme.verts.ensure_lookup_table()
            list_.append(bme.verts[-1])

        # add edges
        n = len(list_)
        for j in range(n - 1):
            bme.edges.new((list_[j], list_[(j + 1) % n]))
            bme.edges.index_update()

        bme.to_mesh(ob_act.data)
        store_restore_view(context, False)
        edit_mode_in()

        pt_buf.list_m_loc_2d[:] = []
        pt_buf.list_m_loc_3d[:] = []
        pt_buf.depth_location = Vector((0.0, 0.0, 0.0))
        pt_buf.store_view_matrix = Matrix()
        pt_buf.view_location = (0.0, 0.0, 0.0)
        pt_buf.ctrl = False

        context.area.tag_redraw()
        return {'FINISHED'}

    def modal(self, context, event):
        context.area.tag_redraw()

        # allow moving in the 3D View
        if event.type in {
                'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
                'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_6',
                'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_5'}:
            return {'PASS_THROUGH'}

        if event.type in {'LEFT_ALT', 'RIGHT_ALT'}:
            if event.value == 'PRESS':
                pt_buf.alt = True
            if event.value == 'RELEASE':
                pt_buf.alt = False
            return {'RUNNING_MODAL'}

        elif event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}:
            if event.value == 'PRESS':
                pt_buf.ctrl = not pt_buf.ctrl
            return {'RUNNING_MODAL'}

        elif event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}:
            if event.value == 'PRESS':
                pt_buf.shift = True
            if event.value == 'RELEASE':
                pt_buf.shift = False
            return {'RUNNING_MODAL'}

        elif event.type == 'MOUSEMOVE':
            if pt_buf.list_m_loc_2d != []:
                pt_buf_list_m_loc_3d_last_2d = location_3d_to_region_2d(
                                                context.region,
                                                context.space_data.region_3d,
                                                pt_buf.list_m_loc_3d[-1]
                                                )
                if pt_buf.alt is True:
                    pt_buf.x = pt_buf_list_m_loc_3d_last_2d[0]
                    pt_buf.y = event.mouse_region_y
                elif pt_buf.shift is True:
                    pt_buf.x = event.mouse_region_x
                    pt_buf.y = pt_buf_list_m_loc_3d_last_2d[1]
                else:
                    pt_buf.x = event.mouse_region_x
                    pt_buf.y = event.mouse_region_y
            else:
                pt_buf.x = event.mouse_region_x
                pt_buf.y = event.mouse_region_y

        elif event.type == 'LEFTMOUSE':
            if event.value == 'PRESS':
                mouse_loc_2d = Vector((pt_buf.x, pt_buf.y))
                pt_buf.list_m_loc_2d.append(mouse_loc_2d)

                mouse_loc_3d = region_2d_to_location_3d(
                                                context.region, context.space_data.region_3d,
                                                mouse_loc_2d, pt_buf.depth_location
                                                )
                pt_buf.list_m_loc_3d.append(mouse_loc_3d)

                pt_buf.depth_location = pt_buf.list_m_loc_3d[-1]   # <-- depth location
                # run Extrude at cursor
                if pt_buf.ctrl:
                    try:
                        bpy.ops.mesh.dupli_extrude_cursor('INVOKE_DEFAULT', rotate_source=False)
                    except:
                        pass
            elif event.value == 'RELEASE':
                pass
        elif event.type == 'RIGHTMOUSE':
            context.space_data.draw_handler_remove(self._handle_px, 'WINDOW')
            self.execute(context)
            pt_buf.sws = 'off'
            return {'FINISHED'}
        elif event.type == 'ESC':
            context.space_data.draw_handler_remove(self._handle_px, 'WINDOW')
            store_restore_view(context, False)
            pt_buf.list_m_loc_2d[:] = []
            pt_buf.list_m_loc_3d[:] = []
            pt_buf.depth_location = Vector((0.0, 0.0, 0.0))
            pt_buf.sws = 'off'
            pt_buf.store_view_matrix = Matrix()
            pt_buf.view_location = (0.0, 0.0, 0.0)
            pt_buf.ctrl = False
            return {'CANCELLED'}

        # Return has to be modal or the tool can crash
        # It's better to define PASS_THROUGH as the exception and not the default
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        bme = bmesh.from_edit_mesh(context.active_object.data)
        list_f = [f for f in bme.faces if f.select]

        if len(list_f) != 0:
            f = list_f[0]
            pt_buf.depth_location = f.calc_center_median()
            align_view_to_face_(context, bme, f)

        if context.area.type == 'VIEW_3D':
            # pre-compute the text location (thanks to the Carver add-on)
            self.text_location = 100
            overlap = context.preferences.system.use_region_overlap
            for region in context.area.regions:
                if region.type == "WINDOW":
                    self.text_location = region.width - 100
            if overlap:
                for region in context.area.regions:
                    # The Properties Region on the right is of UI type
                    if region.type == "UI":
                        self.text_location = self.text_location - region.width

            if pt_buf.sws == 'on':
                return {'RUNNING_MODAL'}
            elif pt_buf.sws != 'on':
                context.window_manager.modal_handler_add(self)
                self._handle_px = context.space_data.draw_handler_add(
                                            draw_callback_px,
                                            (self, context),
                                            'WINDOW', 'POST_PIXEL'
                                            )
                pt_buf.sws = 'on'
                return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "Pen Tool: Operation Cancelled. View3D not found")
            return {'CANCELLED'}


class_list = (
        pen_tool_panel,
        pen_tool_operator,
        pen_tool_properties
        )


KEYMAPS = (
    # First, keymap identifiers (last bool is True for modal km).
    (("3D View", "VIEW_3D", "WINDOW", False), (
    # Then a tuple of keymap items, defined by a dict of kwargs
    # for the km new func, and a tuple of tuples (name, val)
    # for ops properties, if needing non-default values.
        ({"idname": pen_tool_operator.bl_idname, "type": 'D', "value": 'PRESS', "ctrl": True},
         ()),
    )),
)


def register():
    for c in class_list:
        bpy.utils.register_class(c)

    bpy.types.Scene.pen_tool_props = PointerProperty(type=pen_tool_properties)

    bpy_extras.keyconfig_utils.addon_keymap_register(bpy.context.window_manager, KEYMAPS)


def unregister():
    bpy_extras.keyconfig_utils.addon_keymap_unregister(bpy.context.window_manager, KEYMAPS)

    del bpy.types.Scene.pen_tool_props

    for c in class_list:
        bpy.utils.unregister_class(c)


if __name__ == "__main__":
    register()
