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

import bpy
import os
import blf
import bgl
import gpu
import bmesh
import aud
import bpy_extras.view3d_utils
import time as time_
from bpy.app.handlers import persistent
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
from . import kex_utils
from . import turok_object_panel

# imageShader = gpu.shader.from_builtin('2D_IMAGE')
# drawImages = []

eventLinesShader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
eventLinesBatch = None
targetLinesBatch = None
targetLines2Batch = None
eventLinesText = [] #(pos, text)
sectorsEditBMesh = None #global bMesh cache
sectorsBMesh = None #global bMesh cache
ceilingBMesh = None #global bMesh cache
ceilingEditBMesh = None #global bMesh cache
freeSectorsBMesh = False
freeSectorsEditBMesh = False
freeCeilingBMesh = False
freeCeilingEditBMesh = False
sectorsFaceCount = -1

# -----------------------------------------------------------------------------
# 
@persistent
def OnLoadPostTimer():
    # global tempbool
    
    #Calculate time and delta time
    millis = time_.monotonic()
    kex_utils.deltaTime = millis - kex_utils.curTime
    kex_utils.curTime = millis

    # if not tempbool:
        # tempbool = True
        # global drawImages
        # from bpy_extras.image_utils import load_image
        # image = load_image(os.path.join(kex_utils.addon_texture_dir(), "icon_actor.png"), check_existing=True)
        # image.use_fake_user = True
        # if image.gl_load():
            # raise Exception()
        # drawImages.append({"name": image.name, "pos": (0, 100, 0), "batch": None, "drawPos": None})
    # #end if
    
    #Update Error Sector Mat Color (Too slow)
    # if kex_utils.time_reached(kex_utils.sectorErrorUpdateTime):
        # kex_utils.sectorErrorUpdateTime = kex_utils.time_from(0.1)
        # sectorErrorMat = bpy.data.materials.get(mat_sector_error)
        # if sectorErrorMat is not None:
            # bsdf = None
            # for node in sectorErrorMat.node_tree.nodes:
                # if node.bl_idname == 'ShaderNodeBsdfPrincipled':
                    # bsdf = node
                # #end if
            # #end for
            # if bsdf is not None:
                # newColor = (random.random(), random.random(), random.random(), 1.0)
                # bsdf.inputs['Emission'].default_value = newColor
            # #end if
        # #end if
    # #end if
    
    #Update Sounds
    playingSoundsDeleteKeys = []
    deviceLocked = False
    for key, soundList in kex_utils.playingSounds.items():
        needRemoval = False
        for sound in soundList:
            sound.update()
            #Check if sound is finished playing
            if sound.doRemove:
                needRemoval = True
            #end if
            
            #update sound delaying
            if sound.isDelaying and kex_utils.time_reached(sound.delayTime):
                if not deviceLocked:
                    aud.Device().lock()
                    deviceLocked = True
                #end if
                sound.delay_finished()
            #end if
        #end for
        
        #Remove sounds that are finished playing
        if needRemoval:
            soundList[:] = [sound for sound in soundList if not sound.doRemove]
            if len(kex_utils.playingSounds[key]) == 0:
                playingSoundsDeleteKeys.append(key)
            #end if
        #end if
    #end for
    
    #Delete empty keys
    for key in playingSoundsDeleteKeys:
        del kex_utils.playingSounds[key]
    #end for
    
    if deviceLocked:
        aud.Device().unlock()
    #end if
    
    return 0.0166666
#end func

# -----------------------------------------------------------------------------
# 
def get_sectors_bmesh(mesh):
    global sectorsBMesh
    global freeSectorsBMesh
    
    if sectorsBMesh is None or not sectorsBMesh.is_valid:
        sectorsBMesh = bmesh.new()
        sectorsBMesh.from_mesh(mesh)
    #end if
    kex_utils.create_BMLayers(sectorsBMesh)
    freeSectorsBMesh = False
    return sectorsBMesh
#end func

# -----------------------------------------------------------------------------
# 
def free_sectors_bmesh():
    global sectorsBMesh
    
    if sectorsBMesh is not None and sectorsBMesh.is_valid:
        sectorsBMesh.free()
        sectorsBMesh = None
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def get_sectors_bmesh_edit(mesh):
    global sectorsEditBMesh
    global freeSectorsEditBMesh
    
    if sectorsEditBMesh is None or not sectorsEditBMesh.is_valid:
        sectorsEditBMesh = bmesh.from_edit_mesh(mesh)
    #end if
    kex_utils.create_BMLayers(sectorsEditBMesh)
    freeSectorsEditBMesh = False
    return sectorsEditBMesh
#end func

# -----------------------------------------------------------------------------
# 
def free_sectors_bmesh_edit():
    global sectorsEditBMesh
    
    if sectorsEditBMesh is not None and sectorsEditBMesh.is_valid:
        sectorsEditBMesh.free()
        sectorsEditBMesh = None
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def get_sectors_ceiling_bmesh(mesh):
    global ceilingBMesh
    global freeCeilingBMesh
    
    if ceilingBMesh is None or not ceilingBMesh.is_valid:
        ceilingBMesh = bmesh.new()
        ceilingBMesh.from_mesh(mesh)
    #end if
    kex_utils.create_BMLayers(ceilingBMesh)
    freeCeilingBMesh = False
    return ceilingBMesh
#end func

# -----------------------------------------------------------------------------
# 
def free_sectors_ceiling_bmesh():
    global ceilingBMesh
    
    if ceilingBMesh is not None and ceilingBMesh.is_valid:
        ceilingBMesh.free()
        ceilingBMesh = None
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def get_sectors_ceiling_bmesh_edit(mesh):
    global ceilingEditBMesh
    global freeCeilingEditBMesh
    
    if ceilingEditBMesh is None or not ceilingEditBMesh.is_valid:
        ceilingEditBMesh = bmesh.from_edit_mesh(mesh)
    #end if
    kex_utils.create_BMLayers(ceilingEditBMesh)
    freeCeilingEditBMesh = False
    return ceilingEditBMesh
#end func

# -----------------------------------------------------------------------------
# 
def free_sectors_ceiling_bmesh_edit():
    global ceilingEditBMesh
    
    if ceilingEditBMesh is not None and ceilingEditBMesh.is_valid:
        ceilingEditBMesh.free()
        ceilingEditBMesh = None
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def handler_main_pre_update(isUndo=False, isRedo=False):
    global sectorsFaceCount
    
    # obj = bpy.context.object
    # if obj is not None:
        # if obj.t1.objType == "MAP SECTORS" and obj.type == "MESH" and obj.mode == 'EDIT':
            # bm = get_sectors_bmesh_edit(obj.data)
            # if bm.is_valid:
                # print("pre verts:%s lastVerts:%s" % (len(bm.verts), sectorsVertCount))
                # sectorsVertCount = len(bm.verts)
            # #end if
            
            # # bm = bmesh.new()
            # # depsgraph = bpy.context.evaluated_depsgraph_get()
            # # bm.from_object(obj, depsgraph)
            # # print("pre verts:%s lastVerts:%s" % (len(bm.verts), sectorsVertCount))
            # # sectorsVertCount = len(bm.verts)
            # # bm.free()
        # #end if
    # #end if
#end func

# -----------------------------------------------------------------------------
# 
def handler_main_post_update(isUndo=False, isRedo=False):
    global eventLinesBatch
    global targetLinesBatch
    global targetLines2Batch
    global eventLinesText
    global freeSectorsBMesh
    global freeSectorsEditBMesh
    global freeCeilingBMesh
    global freeCeilingEditBMesh
    global sectorsFaceCount
    
    eventLinesBatch = None
    targetLinesBatch = None
    targetLines2Batch = None
    eventLinesText.clear()
    freeSectorsBMesh = True
    freeSectorsEditBMesh = True
    freeCeilingBMesh = True
    freeCeilingEditBMesh = True
    
    obj = bpy.context.object
    if obj is not None:
        if obj.t1.objType == "MAP CEILING SECTORS" and obj.type == "MESH" and obj.mode == 'EDIT':
            #get all verts world z position and the linked vert index
            ceilingBM = None
            if obj.mode == 'EDIT':
                ceilingBM = get_sectors_ceiling_bmesh_edit(obj.data)
            else:
                ceilingBM = get_sectors_ceiling_bmesh(obj.data) #REMOVE THIS
            vertsCeilingZ = {}
            if ceilingBM.is_valid:
                ceilingBM.verts.ensure_lookup_table()
                for v in ceilingBM.verts:
                    #verts could have the same sector vert index value
                    vertsCeilingZ[v[kex_utils.VL_CEILINGLINK]] = ((obj.matrix_world @ v.co).z, v)
                #end for
            #end if
            
            if len(vertsCeilingZ) > 0:
                sectorsObj = kex_utils.get_root_turok_object(obj, "MAP SECTORS", True)
                if sectorsObj is not None and sectorsObj.type == "MESH":
                    sectorsBM = get_sectors_bmesh(sectorsObj.data)
                    if sectorsBM.is_valid:
                        sectorsBM.verts.ensure_lookup_table()
                        #set those values to the verts in the sector mesh
                        for vertIndex, value in vertsCeilingZ.items():
                            if vertIndex >= 0 and vertIndex < len(sectorsBM.verts):
                                bVert = sectorsBM.verts[vertIndex]
                                bVert[kex_utils.VL_CEILINGZ] = value[0]
                                worldSectorVertPos = sectorsObj.matrix_world @ bVert.co
                                worldSectorVertPos.z = value[0]
                                value[1].co = obj.matrix_world.inverted() @ worldSectorVertPos
                            #end if
                        #end for
                    #end if
                #end if
            #end if
        elif obj.t1.objType == "MAP SECTORS" and obj.type == "MESH" and obj.mode == 'EDIT':
            ceilingObj = next((x for x in obj.children if x.type == "MESH" and x.t1.objType == "MAP CEILING SECTORS"), None)
            
            #makes sure sector materials are created
            if kex_utils.mat_sector_none not in bpy.data.materials:
                kex_utils.try_create_color_mats()
            #end if
            
            #make sure sectors mesh has the correct materials
            if len(obj.data.materials) != len(kex_utils.T1_MATS_SECTORS):
                obj.data.materials.clear()
                for sectorMatInfo in kex_utils.T1_MATS_SECTORS:
                    obj.data.materials.append(bpy.data.materials[sectorMatInfo[0]])
            #end if
            
            sectorsEditBM = get_sectors_bmesh_edit(obj.data)
            if sectorsEditBM.is_valid:
                #if vert count changed then recreate the ceiling mesh
                needCreateCeiling = (len(sectorsEditBM.faces) != sectorsFaceCount and sectorsFaceCount >= 0)
                if needCreateCeiling:
                    recreate_sector_ceiling_edit_mesh(obj, ceilingObj, sectorsEditBM)
                    kex_utils.create_BMLayers(sectorsEditBM)
                else:
                    #if any verts change position set the sector ceiling xy position
                    if ceilingObj is not None:
                        ceilingBM = get_sectors_ceiling_bmesh(ceilingObj.data)
                        sectorsEditBM.verts.ensure_lookup_table()
                        for v in ceilingBM.verts:
                            vIndex = v[kex_utils.VL_CEILINGLINK]
                            if vIndex < 0 or vIndex >= len(sectorsEditBM.verts):
                                continue
                            pos = sectorsEditBM.verts[vIndex].co
                            v.co = (pos.x, pos.y, v.co.z)
                        #end for
                        ceilingBM.to_mesh(ceilingObj.data)
                        kex_utils.create_BMLayers(sectorsEditBM)
                    #end if
                #end if
                sectorsFaceCount = len(sectorsEditBM.faces)
                
                
                #(in object panel) if you apply any collide ceiling (add/remove) to sectors then recreate ceiling mesh
                
            
                #if any face in the bmesh has more than 3 verts then triangulate those faces
                nonTriFaces = [f for f in sectorsEditBM.faces if len(f.loops) > 3]
                if len(nonTriFaces) > 0:
                    bmesh.ops.triangulate(sectorsEditBM, faces=nonTriFaces)
                    bmesh.update_edit_mesh(obj.data)
                #end if
                
                #Check if any faces normal.z value is <= 0.0
                noneMatIndex = kex_utils.find_material_index(obj.data, bpy.data.materials[kex_utils.mat_sector_none])
                errorMatIndex = kex_utils.find_material_index(obj.data, bpy.data.materials[kex_utils.mat_sector_error])
                for f in sectorsEditBM.faces:
                    if f.material_index != errorMatIndex:
                        if f.normal.z <= 0.0:
                            f.material_index = errorMatIndex
                        elif f.material_index == noneMatIndex:
                            kex_utils.bmface_update_material_index(obj.data, f)
                        #end if
                    elif f.material_index == errorMatIndex and f.normal.z > 0.0:
                        kex_utils.bmface_update_material_index(obj.data, f)
                    #end if
                #end for
                
                #Make sure current face updates object sector prop when you select a different one
                face = sectorsEditBM.faces.active
                if face is not None:
                    selected_faces = [f for f in sectorsEditBM.faces if f.select]
                    if len(selected_faces) > 0 and face not in selected_faces:
                        #set active to first selected face
                        sectorsEditBM.faces.active = selected_faces[0]
                        face = selected_faces[0]
                    #end if
                    
                    if face.is_valid:
                        #Check if needs to update the objects sector props
                        if isUndo or isRedo or face.index != kex_utils.lastSelectedSectorIndex:
                            kex_utils.lastSelectedSectorIndex = face.index
                            kex_utils.obj_sector_to_bmface(obj, face)
                            kex_utils.sectorDirty = False
                        #end if
                        
                        update_bmface_eventlines_batch(obj, face)
                    #end if
                #end if
            #end if
        elif obj.t1.objType == "MAP ACTOR" and obj.select_get() and obj.mode == 'OBJECT':
            update_actor_eventlines_batch(obj)
        #end if
    #end if
    
    #Try freeing any cached bmeshs
    if freeSectorsBMesh:
        free_sectors_bmesh()
    if freeSectorsEditBMesh:
        free_sectors_bmesh_edit()
    if freeCeilingBMesh:
        free_sectors_ceiling_bmesh()
    if freeCeilingEditBMesh:
        free_sectors_ceiling_bmesh_edit()

#end func

# -----------------------------------------------------------------------------
# 
def recreate_sector_ceiling_edit_mesh(sectorObj, ceilingObj=None, sectorBM=None):
    ceiling_tris = []
    ceiling_indice_dict = {}
    ceiling_vertpos_dict = {}
    last_ceiling_index = -1
    sectorMesh = sectorObj.data
    createdNewBM = sectorBM is None
    if createdNewBM:
        if sectorObj.mode == 'EDIT':
            sectorBM = bmesh.from_edit_mesh(sectorMesh)
        else:
            sectorBM = bmesh.new()
            sectorBM.from_mesh(sectorMesh)
        #end if
    #end if

    if not sectorBM.is_valid:
        return
        
    kex_utils.create_BMLayers(sectorBM)
    for f in sectorBM.faces:
        #get the verts and tris used (faces that have ceiling flag)
        if kex_utils.bmface_has_ceiling(f):
            tri_indices = []
            for face_vert in f.verts:
                ceiling_index = ceiling_indice_dict.get(face_vert.index)
                if ceiling_index is None:
                    last_ceiling_index += 1
                    ceiling_index = last_ceiling_index
                    ceiling_indice_dict[face_vert.index] = last_ceiling_index
                    vertWorldPos = sectorObj.matrix_world @ Vector((face_vert.co.x, face_vert.co.y, 0.0))
                    vertWorldPos.z = face_vert[kex_utils.VL_CEILINGZ]
                    ceiling_vertpos_dict[last_ceiling_index] = vertWorldPos
                #end if
                tri_indices.append(ceiling_index)
            #end for
            ceiling_tris.append((tri_indices[1], tri_indices[0], tri_indices[2])) #flipped indices 0,1
        #end if
    #end if
    if createdNewBM:
        sectorBM.free()

    if ceilingObj is None:
        ceilingObj = next((x for x in sectorObj.children if x.type == "MESH" and x.t1.objType == "MAP CEILING SECTORS"), None)
    
    if ceilingObj is None:
        return
    
    ceilingMesh = ceilingObj.data
    if ceilingObj.mode == 'EDIT':
        ceilingBM = get_sectors_ceiling_bmesh_edit(ceilingObj.data)
    else:
        ceilingBM = get_sectors_ceiling_bmesh(ceilingObj.data)
    #end if
    ceilingBM.clear()
    kex_utils.create_BMLayers(ceilingBM)
    sortedKeys = sorted(ceiling_indice_dict, key=ceiling_indice_dict.__getitem__)
    bVerts = []
    for orgVertIndex in sortedKeys:
        newVertIndex = ceiling_indice_dict[orgVertIndex]
        local_pos = ceilingObj.matrix_world.inverted() @ ceiling_vertpos_dict[newVertIndex]
        bVert = ceilingBM.verts.new(local_pos)
        bVert[kex_utils.VL_CEILINGLINK] = orgVertIndex
        bVert.index = newVertIndex #should start at zero and increase by 1 every vert
        bVerts.append(bVert)
    #end for
    
    for tri in ceiling_tris:
        ceilingBM.faces.new((bVerts[tri[0]], bVerts[tri[1]], bVerts[tri[2]]))
    #end for
    if ceilingObj.mode == 'EDIT':
        bmesh.update_edit_mesh(ceilingMesh)
    else:
        ceilingBM.to_mesh(ceilingMesh)
    #end if
#end func

# -----------------------------------------------------------------------------
# obj: actorObj
def update_actor_eventlines_batch(obj):
    global eventLinesShader
    global eventLinesBatch
    global targetLinesBatch
    global targetLines2Batch
    global eventLinesText
    
    eventTID = obj.t1.actorTID
    actorPos = obj.matrix_world.translation
    eventLinesText.append((actorPos, str(eventTID)))

    mapObj = kex_utils.get_root_turok_object(obj, "MAP ROOT", True)
    if mapObj is None:
        return
    
    #Lines from sectors to this actor
    gotSectorBMesh = False
    sectorsObj = next((child for child in mapObj.children if child.name.startswith("Sectors")), None)
    if sectorsObj is not None and sectorsObj.type == "MESH" and sectorsObj.t1.objType == "MAP SECTORS":
        bm = get_sectors_bmesh(sectorsObj.data)
        if bm.is_valid:
            faces = [f for f in bm.faces if f.is_valid and f[kex_utils.FL_ARG4] == eventTID and (f[kex_utils.FL_FLAGS] & kex_utils.get_enum_type(turok_object_panel.T1SectorSetFlags, "EVENT")) != 0]
            if len(faces) > 0:
                lineCoords = [actorPos] * (len(faces) * 2)
                #draw lines from this actor to each face
                for i, f in enumerate(faces):
                    facePos = sectorsObj.matrix_world @ f.calc_center_median()
                    lineCoords[i*2+1] = facePos
                    eventLinesText.append((facePos, str(eventTID)))
                #end for
                eventLinesBatch = batch_for_shader(eventLinesShader, 'LINES', {"pos": lineCoords})
            #end if
        #end if
    #end if
    
    #Lines from this actor to other actors with the same TID as this actors TargetTID
    #and Lines from actors with a targetTID the same as this actors TID
    actorsObj = next((child for child in mapObj.children if child.name.startswith("Actors")), None)
    targetTID = obj.t1.actorTargetTID
    textDict = {}
    if actorsObj is not None:
        actorObjs = [actorObj for actorObj in actorsObj.children if actorObj.t1.actorTID == targetTID and actorObj != obj]
        if len(actorObjs) > 0:
            lineCoords = [actorPos] * (len(actorObjs) * 2)
            #draw lines from this actorObj to each other actorObj
            for i, targetActorObj in enumerate(actorObjs):
                targetActorPos = targetActorObj.matrix_world.translation
                lineCoords[i*2+1] = targetActorPos
                eventLinesText.append((targetActorPos, str(targetActorObj.t1.actorTID)))
                textDict[targetActorObj.name] = None
            #end for
            targetLinesBatch = batch_for_shader(eventLinesShader, 'LINES', {"pos": lineCoords})
        #end if
        actorObjs = [actorObj for actorObj in actorsObj.children if actorObj.t1.actorTargetTID == eventTID and actorObj != obj]
        if len(actorObjs) > 0:
            actorPos2 = actorPos.copy()
            lineCoords = [None] * (len(actorObjs) * 2)
            vOffset = Vector((0.0, 0.0, 5.0))
            #draw lines from this actorObj to each other actorObj
            for i, targetActorObj in enumerate(actorObjs):
                targetActorPos = targetActorObj.matrix_world.translation
                #if hasn't already drawn that TID on that actor
                if targetActorObj.name not in textDict:
                    lineCoords[i*2] = actorPos2
                    lineCoords[i*2+1] = targetActorPos
                    eventLinesText.append((targetActorPos, str(targetActorObj.t1.actorTID)))
                else:
                    lineCoords[i*2] = actorPos2 + vOffset
                    lineCoords[i*2+1] = targetActorPos + vOffset
                #end if
            #end for
            targetLines2Batch = batch_for_shader(eventLinesShader, 'LINES', {"pos": lineCoords})
        #end if
    #end if
#end func

# -----------------------------------------------------------------------------
# obj: SectorsObj
def update_bmface_eventlines_batch(obj, face):
    global eventLinesShader
    global eventLinesBatch
    global eventLinesText
    
    eventTID = kex_utils.bmface_eventTID(face)
    if eventTID is not None:
        facePos = obj.matrix_world @ face.calc_center_median()
        eventLinesText.append((facePos, str(eventTID)))
        
        mapObj = kex_utils.get_root_turok_object(obj, "MAP ROOT", True)
        if mapObj is None:
            return
            
        actorsObj = next((child for child in mapObj.children if child.name == "Actors"), None)
        if actorsObj is None:
            return
        
        #Lines from sector to actors
        actorObjs = [actorObj for actorObj in actorsObj.children if actorObj.t1.actorTID == eventTID]
        if len(actorObjs) > 0:
            lineCoords = [facePos] * (len(actorObjs) * 2)
            #draw lines from this face to each actorObj
            for i, actorObj in enumerate(actorObjs):
                actorPos = actorObj.matrix_world.translation
                lineCoords[i*2+1] = actorPos
                eventLinesText.append((actorPos, str(eventTID)))
            #end for
            eventLinesBatch = batch_for_shader(eventLinesShader, 'LINES', {"pos": lineCoords})
            return
        #end if
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def draw_3d(self, context):
    global eventLinesShader
    global eventLinesBatch
    global targetLinesBatch
    
    if eventLinesBatch is None and targetLinesBatch is None and targetLines2Batch is None:
        return
    
    eventLinesShader.bind()
    
    bgl.glLineWidth(3)
    eventLinesShader.uniform_float("color", (0.0, 0.0, 0.0, 1.0))
    if eventLinesBatch is not None:
        eventLinesBatch.draw(eventLinesShader)
    if targetLinesBatch is not None:
        targetLinesBatch.draw(eventLinesShader)
    if targetLines2Batch is not None:
        targetLines2Batch.draw(eventLinesShader)
    bgl.glLineWidth(1)
    if eventLinesBatch is not None:
        eventLinesShader.uniform_float("color", (0.0, 1.0, 0.0, 1.0))
        eventLinesBatch.draw(eventLinesShader)
    #end if
    if targetLinesBatch is not None:
        eventLinesShader.uniform_float("color", (1.0, 0.0, 0.0, 1.0))
        targetLinesBatch.draw(eventLinesShader)
    #end if
    if targetLines2Batch is not None:
        eventLinesShader.uniform_float("color", (0.0, 0.0, 1.0, 1.0))
        targetLines2Batch.draw(eventLinesShader)
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def draw_2d(self, context):
    global eventLinesText
    # global imageShader
    # global drawImages
    
    kex_utils.cachedRegion3D = bpy.context.region_data
    #rv3d = bpy.context.region_data
    region = bpy.context.region
    
    
    font_id = 0
    blf.enable(font_id, blf.SHADOW)
    blf.shadow(font_id, 0, 0.0, 0.0, 0.0, 1.0)
    blf.shadow_offset(font_id, 1, -1)
    blf.color(font_id, 1.0, 0.0, 1.0, 1.0)
    blf.size(font_id, 18, 72)
    for msg in eventLinesText:
        pos = bpy_extras.view3d_utils.location_3d_to_region_2d(region, kex_utils.cachedRegion3D, msg[0])
        if pos is None:
            continue
        text = msg[1]
        dimensions = blf.dimensions(font_id, text)
        pos.x -= dimensions[0] / 2.0
        pos.y -= dimensions[1] / 2.0
        blf.position(font_id, pos[0], pos[1], 0)
        blf.draw(font_id, text)
    #end if
    
    
    
    # pos = bpy_extras.view3d_utils.location_3d_to_region_2d(region, rv3d, msg[0])
    # drawImages[image.name] = batch_for_shader(imageShader, 'TRI_FAN',
        # {
            # "pos": ((100, 100), (200, 100), (200, 200), (100, 200)),
            # "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
        # },
    # )

    # cam_info = rv3d.view_matrix.inverted()
    # camPos = cam_info.translation
    # if len(drawImages) > 0:
        # imageShader.bind()
        # imageShader.uniform_int("image", 0)
        # bgl.glActiveTexture(bgl.GL_TEXTURE0)
        # for drawImage in drawImages:
            # imageName = drawImage["name"]
            # image = bpy.data.images.get(imageName)
            # if image is None:
                # drawImage["batch"] = None
                # continue
            # #end if
            # imagePos = drawImage["pos"]
            # imageDrawPos = drawImage["drawPos"]
            # imageBatch = drawImage["batch"]
            # pos = bpy_extras.view3d_utils.location_3d_to_region_2d(region, rv3d, imagePos)
            # if pos is None:
                # drawImage["batch"] = None
                # continue
            # #end if
            # if imageBatch is None or imageDrawPos is None or not kex_utils.is_vectors2_equal(pos, imageDrawPos):
                # drawImage["drawPos"] = pos
                # dist = kex_utils.vec3_distance(camPos, imagePos)
                # lerpAmount = (dist + 500) / 5000.0
                # #if image is too small don't draw it
                # if lerpAmount > 1.0:
                    # drawImage["batch"] = None
                    # continue
                # #end if
                # scale = kex_utils.lerp(40, 4, kex_utils.max(lerpAmount, 0.0))
                # #print("dist=%s" % (dist))
                # drawImage["batch"] = batch_for_shader(
                    # imageShader, 'TRI_FAN',
                    # {
                        # "pos": ((pos[0] - scale, pos[1] - scale),
                                # (pos[0] + scale, pos[1] - scale),
                                # (pos[0] + scale, pos[1] + scale),
                                # (pos[0] - scale, pos[1] + scale)),
                        # "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
                    # },
                # )
            # #end if
            
            # # batch = batch_for_shader(
                # # imageShader, 'TRI_FAN',
                # # {
                    # # "pos": ((pos[0] - 100, pos[1] - 100), (pos[0] + 100, pos[1] - 100), (pos[0] + 100, pos[1] + 100), (pos[0] - 100, pos[1] + 100)),
                    # # "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
                # # },
            # # )
            
            # if image.bindcode == 0:
                # print("image.bindcode broke: %s " % (image.bindcode))
                # image.gl_load()
            # bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode)
            # drawImage["batch"].draw(imageShader)
        # #end for
    # #end if

#end func

# -----------------------------------------------------------------------------
# Called after whenever a new file is loaded (basically OnStart)
@persistent
def LoadPostHandler(dummy):
    if not bpy.app.timers.is_registered(OnLoadPostTimer):
        kex_utils.curTime = time_.monotonic()
        bpy.app.timers.register(OnLoadPostTimer, first_interval=1.0, persistent=True)
    #end if
#end func

# -----------------------------------------------------------------------------
# Called before whenever something changes
@persistent
def DepsgraphPreHandler(scene):
    handler_main_pre_update()
#end func

# -----------------------------------------------------------------------------
# Called After whenever something changes
@persistent
def DepsgraphPostHandler(scene):
    handler_main_post_update(False, False)
#end func

# -----------------------------------------------------------------------------
# Called After Undo
@persistent
def UndoPostHandler(scene):
    handler_main_post_update(True, False)
#end func

# -----------------------------------------------------------------------------
# Called After Redo
@persistent
def RedoPostHandler(scene):
    handler_main_post_update(False, True)
#end func

# -----------------------------------------------------------------------------
#
def register():
    kex_utils.add_handler(bpy.app.handlers.load_post, LoadPostHandler)
    kex_utils.add_handler(bpy.app.handlers.depsgraph_update_pre, DepsgraphPreHandler)
    kex_utils.add_handler(bpy.app.handlers.depsgraph_update_post, DepsgraphPostHandler)
    kex_utils.add_handler(bpy.app.handlers.undo_post, UndoPostHandler)
    kex_utils.add_handler(bpy.app.handlers.redo_post, RedoPostHandler)
    bpy.types.SpaceView3D.draw_handler_add(draw_3d, (None, None), 'WINDOW', 'POST_VIEW')
    bpy.types.SpaceView3D.draw_handler_add(draw_2d, (None, None), 'WINDOW', 'POST_PIXEL')
#end func

# -----------------------------------------------------------------------------
#     
def unregister():
    kex_utils.remove_handler(bpy.app.handlers.load_post, LoadPostHandler)
    kex_utils.remove_handler(bpy.app.handlers.depsgraph_update_pre, DepsgraphPreHandler)
    kex_utils.remove_handler(bpy.app.handlers.depsgraph_update_post, DepsgraphPostHandler)
    kex_utils.remove_handler(bpy.app.handlers.undo_post, UndoPostHandler)
    kex_utils.remove_handler(bpy.app.handlers.redo_post, RedoPostHandler)
    try:
        bpy.types.SpaceView3D.draw_handler_remove(draw_3d, 'WINDOW')
        bpy.types.SpaceView3D.draw_handler_remove(draw_2d, 'WINDOW')
    except:
        pass
    #end try except
#end func
