
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
import blf
from mathutils import bvhtree, Vector, Color
from mathutils.geometry import (intersect_point_quad_2d, intersect_point_tri_2d, 
        intersect_tri_tri_2d)
from ..constants import PICKERS
from .addon_utils import get_operator_from_id
from .geometry_utils import bounding_rect

from pathlib import Path
import re
import json
import os

import threading 


class Shape:
    def __init__(self, picker, points, polygons=None, edges=None, tooltip='', color=None, source_name=''):

        self.type = 'display'
        self.picker = picker
        self.rig = picker.rig
        self.source_name = source_name

        self.hover = False
        self.press = False

        self.shader = gpu.shader.from_builtin('UNIFORM_COLOR')
        self.shader.bind()

        #self.hover_shader = gpu.shader.from_builtin('UNIFORM_COLOR')
        #self.hover_shader.uniform_float("color", [1, 1, 1, 0.1])

        self.color = color
        self.hover_color = self.brighten_color(self.color)

        self.tooltip = tooltip
        self.points = points
        self.polygons = polygons or []
        self.edges = edges or []

        self.p_batch = batch_for_shader(self.shader, 'TRIS', {"pos": self.points}, indices=self.polygons)
        self.e_batch = batch_for_shader(self.shader, 'LINES', {"pos": self.points}, indices=self.edges)

        self.rect = bounding_rect(points)

    @property
    def color(self):
        return self._color
    
    def brighten_color(self, color):
        brighten_color = Color(color[:3])
        brighten_color.v += 0.05
        brighten_color.v *= 1.1
        brighten_color.v = max(brighten_color.v, 0.15)

        return [*brighten_color, color[3]]

    @color.setter
    def color(self, color=None):
        if not color:
            color = [0.5, 0.5, 0.5, 1]
        elif isinstance(color, (tuple, list)) and len(color) in (3, 4):
            if len(color) == 3:
                color = [*color, 1]
            elif len(color) == 4:
                color = list(color)
            else:
                raise Exception('color must have a len of 3 or 4')
        else:
            raise Exception(f'color is {type(color)} must be None or (tuple, list)')
        
        #self.shader.uniform_float("color", color)
        self._color = color

    def draw(self):

        #self.shader.bind()
        self.shader.uniform_float("color", self.color)

        if self.polygons:
            self.p_batch.draw(self.shader)

        if self.edges:
            self.e_batch.draw(self.shader)

    def move_event(self, location):
        if not intersect_point_quad_2d(location, *self.rect):
            self.hover = False
            return False
        
        for p in self.polygons:
            if intersect_point_tri_2d(location, *[self.points[i] for i in p]):
                self.hover = True
                return True
        
        self.hover = False
        return False
            
    def press_event(self, mode='SET'):
        self.press = True

    def release_event(self, mode='SET'):
        self.press = False


class BoneShape(Shape):
    def __init__(self, picker, points, polygons, edges, bone, tooltip='', color=None, source_name=''):
        super().__init__(picker, points=points, polygons=polygons, edges=edges, 
        tooltip=tooltip, color=color, source_name=source_name)

        self.type = 'bone'
        self.bone = bone
        self.active_color = [1, 1, 1, 0.1]
        self.select_color = self.brighten_color(self.hover_color)

        self.bone_colors = self.get_bone_colors()

    @property
    def select(self):
        if not self.bone:
            return False

        return self.bone in (bpy.context.selected_pose_bones or []) #self.bone.bone.select

    @property
    def active(self):
        if not self.bone:
            return False

        return self.bone == bpy.context.active_pose_bone #self.rig.data.bones.active == self.bone.bone
    
    @property
    def hide(self):
        if not self.bone:
            return False

        #return self.bone not in (bpy.context.visible_pose_bones or [])
        
        return self.bone.bone.hide or not any(l.is_visible for l in self.bone.bone.collections)

    @property
    def bone_color(self):
        if not self.bone:
            return [0, 0, 0, 1]

        if self.select and self.active:
            return self.bone_colors['active']
        elif self.select:
            return self.bone_colors['select']
        elif self.hide:
            return self.bone_colors['hide']
        else:
            return self.bone_colors['normal']

    def get_bone_colors(self):

        theme = bpy.context.preferences.themes['Default']
        bone_colors = {
            'select': [*theme.view_3d.bone_pose, 1],
            'normal': [0.05, 0.05, 0.05, 1],
            'active': [*theme.view_3d.bone_pose_active, 1],
            'hide': [0.85, 0.85, 0.85, 0.2],
        }

        if not self.bone:
            return bone_colors

        if self.bone.color.palette == 'CUSTOM':
            bone_color = self.bone

        elif self.bone.color.palette == 'DEFAULT':
            bone_color = self.bone.bone

        normal_color = bone_color.color.custom.normal.copy()
        normal_color.s *= 0.75

        bone_colors['normal'] = [*normal_color, 1]
        bone_colors['select'] = [*bone_color.color.custom.select, 1]
        bone_colors['active'] = [*bone_color.color.custom.active, 1]
        bone_colors['hide'] = [*normal_color, 0.1]

        return bone_colors

    def draw_hided(self):
        gpu.state.blend_set('ALPHA')
        gpu.state.line_width_set(1.0)

        line_color = (1, 1, 1, 0.1)
        color = Color(self.color[:3])
        if self.hover:
            color.v += 0.05
            color.v *= 1.1

            line_color = (1, 1, 1, 0.3)

        self.shader.uniform_float("color", (*color[:3], 0.5))
        self.p_batch.draw(self.shader)
        
        self.shader.uniform_float("color", line_color)
        self.e_batch.draw(self.shader)

        gpu.state.blend_set('NONE')

    def draw_zero_scaled(self):
        gpu.state.blend_set('ALPHA')
        gpu.state.line_width_set(1.0)

        line_color = (*self.bone_colors['normal'][:3], 0.2)
        color = Color(self.color[:3])
        if self.hover or self.select:
            color.v += 0.05
            color.v *= 1.1
            line_color = (*self.bone_color[:3], 0.66)

        self.shader.uniform_float("color", (*color[:3], 0.5))
        self.p_batch.draw(self.shader)
        
        self.shader.uniform_float("color", line_color)
        self.e_batch.draw(self.shader)

        gpu.state.blend_set('NONE')

    def draw(self):
        gpu.state.blend_set('ALPHA')
        gpu.state.line_width_set(1.0)

        if self.hide:
            return self.draw_hided()

        elif self.bone and self.bone.custom_shape_scale_xyz.length < 0.00001:
            return self.draw_zero_scaled()

        # Draw Fill
        color = self.color
        if self.select:
            color = self.select_color
        elif self.hover:
            color = self.hover_color

        self.shader.uniform_float("color", color)
        self.p_batch.draw(self.shader)
            
        # Draw Outline
        gpu.state.blend_set('NONE')
        if self.select or self.active:
            gpu.state.line_width_set(2.0)

        self.shader.uniform_float("color", self.bone_color)
        self.e_batch.draw(self.shader)

        gpu.state.line_width_set(1.0)
    
    def assign_bone_event(self):

        #print('assign_bone_event', self)

        scn = bpy.context.scene
        rig = self.picker.rig
        source_object = scn.objects.get(self.source_name)
        if not source_object:
            print(f'Source object {self.source_name} not found')
            return
        
        active_bone = rig.data.bones.active
        if not active_bone:
            print('You need to have an active bone')
            return

        #print(active_bone, source_object)
        #print(rig)
        source_object.name = rig.data.bones.active.name
        source_object.rig_picker.name = rig.data.bones.active.name

    def release_event(self, mode='SET'):
        super().release_event(mode)

        if self.hide or not self.bone:
            return

        select = True
        if mode == 'SUBSTRACT':
            select = False

        self.bone.bone.select = select
        if self.hover:

            if mode != 'SUBSTRACT':
                self.rig.data.bones.active = self.bone.bone


    def border_select(self, border, mode='SET'):
        if not self.bone:
            return

        if self.hide:
            self.bone.bone.select = False
            return

        rect_tri1 = self.rect[0], self.rect[1], self.rect[2]
        rect_tri2 = self.rect[2], self.rect[3], self.rect[0]

        border_tri1 = border[0], border[1], border[2]
        border_tri2 = border[2], border[3], border[0]

        if (not intersect_tri_tri_2d(*border_tri1, *rect_tri1) and
            not intersect_tri_tri_2d(*border_tri1, *rect_tri2) and 
            not intersect_tri_tri_2d(*border_tri2, *rect_tri1) and 
            not intersect_tri_tri_2d(*border_tri2, *rect_tri2)):
            return

        select = True
        if mode == 'SUBSTRACT':
            select = False
        
        for polygon in self.polygons:
            points = [self.points[i] for i in polygon]

            if intersect_tri_tri_2d(*border_tri1, *points):
                self.bone.bone.select = select
                return
            
            if intersect_tri_tri_2d(*border_tri2, *points):
                self.bone.bone.select = select
                return

            '''
            for b in border:
                if intersect_point_tri_2d(b, *points):
                    self.bone.bone.select = select
                    return
                
            for p in points:
                if intersect_point_quad_2d(p, *border):
                    self.bone.bone.select = select
                    return
            '''

class OperatorShape(Shape):
    def __init__(self, picker, points, polygons, operator, tooltip='', color=None, source_name=''):
        super().__init__(picker, points=points, polygons=polygons, tooltip=tooltip, 
            color=color, source_name=source_name)

        self.type = 'operator'
        self.active_color = [1, 1, 1, 0.15]
        self.press_color = [0, 0, 0, 0.25]
        self.operator = operator
        #self.arguments = arguments#{k:eval(v)}
        #self.operator = get_operator_from_id(self.operator)

        if not tooltip:
            self.tooltip = self.operator.replace('bpy.ops.', '').replace("'INVOKE_DEFAULT', ", '')

        #self.reg_args = re.compile(r'(\w+)=')
    '''
    def parse_args(self):
        
        args = self.reg_args.split(self.arguments)[1:]
        #print(args, zip(args[::2], args[1::2]))
        return {k: eval(v) for k, v in zip(args[::2], args[1::2])}
        #return {k:eval(v) for k, v in self.reg_args.split(self.arguments)}
        '''

    def release_event(self, mode='SET'):
        super().release_event(mode)

        #args = self.parse_args()
        if not self.operator:
            return

        exec(self.operator)
        #f'bpy.ops;{idname}'

        #print(self.idname)
        #print(self.arguments)
        #else:
        #    self.bone.bone.select = False

    def draw(self):
        super().draw()

        if self.press:
            color = self.press_color
        elif self.hover:
            color = self.hover_color
        else:
            return

        gpu.state.blend_set('ALPHA')
        self.shader.uniform_float("color", color)
        self.p_batch.draw(self.shader)
        gpu.state.blend_set('NONE')


class Picker:
    def __init__(self, parent, rig, shapes):
        self.parent = parent
        self.region = bpy.context.region
        self.rig = rig

        self.translation = Vector((0, 0))
        self.shapes = []
        self.box_select = None
        self.hover_shape = None
        self.shader = gpu.shader.from_builtin('UNIFORM_COLOR')

        self.mouse = None

        for shape_data in shapes:
            if not shape_data['points']:
                continue
                
            if shape_data['type'] in ('CANVAS', 'DISPLAY'):
                shape = Shape(
                    self,
                    points=shape_data['points'], 
                    polygons=shape_data['polygons'], 
                    edges=shape_data['edges'], 
                    color=shape_data['color']
                )

            elif shape_data['type'] == 'BONE':
                bone = rig.pose.bones.get(shape_data['bone'])
                #if not bone:
                #    print(f'Bone {shape_data["bone"]} not exist')
                #    continue

                shape = BoneShape(
                    self,
                    source_name=shape_data['source_name'],
                    points=shape_data['points'], 
                    polygons=shape_data['polygons'], 
                    edges=shape_data['edges'], 
                    bone=bone, 
                    color=shape_data['color']
                )

            elif shape_data['type'] == 'OPERATOR':
                shape = OperatorShape(
                    self,
                    source_name=shape_data['source_name'],
                    points=shape_data['points'], 
                    polygons=shape_data['polygons'],
                    operator=shape_data['operator'],
                    color=shape_data['color'],
                    tooltip=shape_data['tooltip'], 
                )

            self.shapes.append(shape)

        self.rect = bounding_rect([p for s in self.shapes for p in s.points])

    def assign_bone_event(self):
        for shape in self.shapes:
            if shape.type == 'bone' and shape.hover:
                shape.assign_bone_event()
        
        bpy.ops.rigpicker.save_picker(index=self.index)

    def press_event(self, mode='SET'):
        for shape in self.shapes:
            #print(s)
            if shape.hover:
                shape.press_event(mode)
            else:
                shape.press = False

    def release_event(self, mode='SET'):


            #bpy.ops.pose.select_all(action='DESELECT')

        #print('PICKER release event', mode)
        #print(f'type={event.type}, value={event.value}, ctrl={event.ctrl}, shift={event.shift}, alt={event.alt}')

        for shape in self.shapes:
            if shape.hover:
                shape.release_event(mode)
            
            shape.press = False
        
        #bpy.context.area.tag_redraw()

        '''
        picker.tooltip_event(event='SHOW')
        region.tag_redraw()



            #context.region.tag_redraw()

        picker.tooltip_event(event='HIDE')
        bpy.app.timers.register(partial(tooltip, context.region), first_interval=1)
        '''
    '''
    def tooltip_event(self, event):



        self.tooltip = ''

        if event == 'SHOW':
            if self.hover_shape.type == 'bone':
                self.tooltip = self.hover_shape.bone.name
   
        #bpy.context.region.tag_redraw()
        '''

    def border_select(self, border, mode):
        border = [Vector(b) - Vector(self.translation) for b in border]

        for shape in self.shapes:
            shape.press = False
            shape.hover = False
            if shape.type != 'bone':
                continue

            shape.border_select(border, mode)

    def move_event(self, location):
        self.mouse = Vector(location) - Vector(self.translation)

        self.hover_shape = None
        for shape in reversed(self.shapes):
            if self.hover_shape:
                shape.hover = False
            elif shape.move_event(self.mouse):
                self.hover_shape = shape

        #if point_inside_rect(self.end, rect):
        #    over = point_over_shape(self.end,points, edges)

        #if bpy.app.timers.is_registered(self.tooltip_event):
        #try:
        #    bpy.app.timers.unregister(self.tooltip_event)
        #except:
        #    pass

        #bpy.app.timers.register(self.tooltip_event, first_interval=1)

    def draw(self):
        with gpu.matrix.push_pop():
            gpu.matrix.translate(self.translation)
            for shape in self.shapes:
                shape.draw()

    def under_mouse(self, location):
        location = Vector(location) - Vector(self.translation)
        return intersect_point_quad_2d(location, *self.rect)

    @property
    def center(self):
        relative_center = (self.rect[1] + self.rect[3]) * 0.5
        return relative_center + self.translation

    @property
    def index(self):
        return self.parent.pickers.index(self)


class PickerGroup:
    def __init__(self, rig, pickers):

        #self.view_location = Vector((0, 0))
        self.region = bpy.context.region
        self.rig = rig
        self.pickers = []

        self.mouse = Vector((0, 0))
        self.location = Vector((0, 0))

        self.hover_shape = None

        self.tooltip_shape = None
        self.tooltip_mouse = Vector((0, 0))
        self.tooltip = ''

        self.timer = None

        for picker in pickers:
            self.add_picker(picker)
        
    def add_picker(self, picker):
        self.pickers.append(Picker(self, self.rig, picker))

    def draw(self):
        y = 0
        for picker in self.pickers:
            height = picker.rect[1][1] - picker.rect[-1][1]

            picker.translation = Vector((0, -y-height*0.5))
            picker.draw()

            y += height + 50

            #break #TODO for now only draw first picker

    def move_event(self, mouse):
        self.mouse = mouse
        self.location = self.region.view2d.region_to_view(*mouse)

        # Try to detect view pan to remove tooltip
        # view_location = self.region.view2d.region_to_view(0, 0)
        # if view_location != self.view_location:
        #     self.view_location = view_location
        #     self.tooltip = ''
        #     if self.timer:
        #         self.timer.cancel()
        #     return

        hover_shape = None
        for picker in self.pickers:
            if not picker.under_mouse(self.location):
                continue

            picker.move_event(self.location)
            
            if picker.hover_shape:
                hover_shape = picker.hover_shape

        self.hover_shape = hover_shape

        if self.tooltip_shape is not self.hover_shape:
            self.tooltip = ''

        if self.timer:
            self.timer.cancel()

        self.timer = threading.Timer(0.4, self.tooltip_event)
        self.timer.start()

    def press_event(self, mode='SET'):
        #self.clear_tooltip()

        for picker in self.pickers:
            if picker.under_mouse(self.location):
                picker.press_event(mode)
            else:
                for shape in picker.shapes:
                    shape.press = False

    def release_event(self, mode='SET'):
        if mode == 'SET':
            for bone in self.rig.data.bones:
                bone.select = False

        for picker in self.pickers:
            if picker.under_mouse(self.location):
                picker.release_event(mode)
            else:
                for shape in picker.shapes:
                    shape.press = False

    def assign_bone_event(self):
        for picker in self.pickers:
            if picker.under_mouse(self.location):
                picker.assign_bone_event()

    def border_select(self, border, mode):
        border = [self.region.view2d.region_to_view(*b) for b in border]

        if mode == 'SET':
            for bone in self.rig.data.bones:
                bone.select = False

        for picker in self.pickers:
            picker.border_select(border, mode)

    def clear_tooltip(self):
        self.tooltip = ''
        self.tooltip_shape = None
        self.timer.cancel()
        self.region.tag_redraw()
    
    def tooltip_event(self):
        #print('Tooltip Event', self)

        #print(self.hover_shape, self.hover_shape.type)

        if self.hover_shape and self.hover_shape.type != 'display':
            if self.hover_shape.type == 'bone' and self.hover_shape.bone:
                self.tooltip = self.hover_shape.bone.name
            else:
                self.tooltip = self.hover_shape.tooltip
            self.tooltip_shape = self.hover_shape
        else:
            return self.clear_tooltip()
            

        self.tooltip_mouse = self.mouse

        self.timer.cancel()

        #print(self.tooltip)

        self.region.tag_redraw()

    @property
    def rect(self):
        return bounding_rect([co - Vector(p.translation) for p in self.pickers for co in p.rect])

    @property
    def center(self):
        center = sum(self.rect, Vector((0, 0))) / len(self.rect)
        center[1] = -center[1]
        return center


def load_picker_data(rig):
    if 'pickers' in rig.data.rig_picker:
        picker_datas = [[s.to_dict() for s in p] for p in rig.data.rig_picker['pickers']]
    else:
        picker_datas = []

        for picker in rig.data.rig_picker.sources:
            picker_path = Path(bpy.path.abspath(picker.source, library=rig.data.library))

            if not picker_path.exists():
                print(f'Picker path not exists: {picker_path.resolve()}')
                continue

            print('Load picker from', picker_path.resolve())
            picker_data = json.loads(picker_path.read_text(encoding='utf-8'))
            picker_datas.append(picker_data)
    
    PICKERS[rig] = PickerGroup(rig, picker_datas)


def get_picker_path(rig, source, start=None):
    picker_path = bpy.path.abspath(source, library=rig.data.library, start=start)
    return Path(os.path.abspath(picker_path))


def pack_picker(rig, start=None):
    if not 'rig_picker' in rig.data:
        return

    pickers = []
    for picker_source in rig.data['rig_picker'].get('sources', []):
        picker_path = get_picker_path(rig, picker_source['source'], start)
        if not picker_path.exists():
            print(f'{picker_path} not exists')
            continue
        picker_data = json.loads(picker_path.read_text(encoding='utf-8'))
        pickers.append(picker_data)

    rig.data['rig_picker']['pickers'] = pickers


def unpack_picker(rig):
    if 'rig_picker' not in rig.data.keys():
        return

    if 'pickers' in rig.data['rig_picker'].keys():
        del rig.data['rig_picker']['pickers']