# ##### BEGIN GPL LICENSE BLOCK #####
#
#  Copyright (C) 2022 VFX Grace - All Rights Reserved
#
#  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 <https://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####

import os
import sys
import json
from enum import Enum, auto
from math import sin, cos, pi, hypot, atan2, degrees

import blf
import bpy
import gpu
import imbuf
from bgl import *
from bl_ui import space_toolsystem_common
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper, item_from_id
from bpy.props import (BoolProperty, EnumProperty, IntProperty, StringProperty, CollectionProperty)
from bpy.types import (Panel, Menu, UIList, PropertyGroup, Operator, SpaceView3D)
from gpu.shader import from_builtin
from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d

from .particle_hook import particle_hook
from .pie_menu_glsl import *
from .props import HairColorFlag
from ..cycles_hair_engine.draw_engine.gl_utils import gl_utils

DISPLAY_WIDTH = 1920
DISPLAY_HEIGHT = 1080
if os.name == 'nt':  # windows
    from ctypes import windll


    def get_display_size():
        display_width = windll.user32.GetSystemMetrics(0)
        display_height = windll.user32.GetSystemMetrics(1)
        return display_width, display_height


    # def get_display_size():
    #     DESKTOPVERTRES = 117
    #     DESKTOPHORZRES = 118
    #     hdc = windll.user32.GetDC(None)
    #     display_width = windll.Gdi32.GetDeviceCaps(hdc, DESKTOPHORZRES)
    #     display_height = windll.Gdi32.GetDeviceCaps(hdc, DESKTOPVERTRES)
    #     windll.user32.ReleaseDC(None, hdc)
    #
    #     return display_width, display_height

    DISPLAY_WIDTH, DISPLAY_HEIGHT = get_display_size()
elif os.name == 'posix':  # linux or mac os
    import re
    import platform
    import subprocess

    if platform.platform().lower().startswith('mac'):  # mac os
        def get_display_size():
            global DISPLAY_WIDTH, DISPLAY_HEIGHT
            results = subprocess.run(('system_profiler',
                                      'SPDisplaysDataType'),
                                     stdout=subprocess.PIPE).stdout.decode()
            result = re.search(r'Resolution: (\d+) x (\d+)', results)

            default_size = (DISPLAY_WIDTH, DISPLAY_HEIGHT)
            display_width, display_height = map(int, result.groups()) if result else default_size
            return display_width, display_height
    else:  # linux
        # def get_display_size():
        #     libx11 = ctypes.cdll.LoadLibrary('libX11.so.6')
        #     libc = ctypes.cdll.LoadLibrary('libc.so.6')
        #
        #     display_name = libc.getenv("DISPLAY")
        #     display = libx11.XOpenDisplay(display_name)
        #     screen_num = libx11.XDefaultScreen(display)
        #     display_width = libx11.XDisplayWidth(display, screen_num)
        #     display_height = libx11.XDisplayHeight(display, screen_num)
        #     return display_width, display_height
        def get_display_size():
            global DISPLAY_WIDTH, DISPLAY_HEIGHT
            output = subprocess.run('xrandr', stdout=subprocess.PIPE).stdout.decode()
            result = re.search(r'current (\d+) x (\d+)', output)

            default_size = (DISPLAY_WIDTH, DISPLAY_HEIGHT)
            display_width, display_height = map(int, result.groups()) if result else default_size
            return display_width, display_height
    try:
        DISPLAY_WIDTH, DISPLAY_HEIGHT = get_display_size()
    except:
        pass
else:
    DISPLAY_WIDTH = 1920
    DISPLAY_HEIGHT = 1080


def adaptation_resolution_value(value):
    global DISPLAY_WIDTH
    return int(value * (DISPLAY_WIDTH / 1920))


class BaseOperator(Operator):
    bl_label = ""
    bl_options = {"REGISTER"}

    @classmethod
    def poll(cls, context: bpy.types.Context):
        return context.mode == 'PARTICLE'


class PieArea(Enum):
    Center = 0
    BrushOption = auto()
    ColorGroup = auto()
    ShowColor = auto()
    ScaleBrush = auto()
    Switch2D3D = auto()
    Enable3DBrush = auto()
    BRUSH_START = auto()


option_attr = {
    PieArea.ShowColor.value: 'show_hair_color',
    PieArea.ScaleBrush.value: 'scale_size',
    PieArea.Switch2D3D.value: 'auto_switch_mode',
    PieArea.Enable3DBrush.value: 'use_3d_brush',
}
default_brush = (
    ('hair_brush_3d.Comb', 'brush3d.particle.comb'),
    ('hair_brush_3d.Smooth', 'brush3d.particle.smooth'),
    ('hair_brush_3d.Add', 'brush3d.particle.add'),
    ('hair_brush_3d.Length', 'brush3d.particle.length'),
    ('hair_brush_3d.Puff', 'brush3d.particle.puff'),
    ('hair_brush_3d.Orient', 'brush3d.particle.orient'),
    ('hair_brush_3d.Bend', 'brush3d.particle.bend'),
    ('hair_brush_3d.Part', 'brush3d.particle.part'),
    ('hair_brush_3d.Noise', 'brush3d.particle.noise'),
    ('hair_brush_3d.Attract', 'brush3d.particle.attract'),
    ('hair_brush_3d.Weight', 'brush3d.particle.weight'),
    ('hair_brush_3d.Clone', 'brush3d.particle.clone'),
    ('hair_brush_3d.Color', 'brush3d.particle.color'),
    ('builtin_brush.Cut', 'brush.particle.cut')
)


class GlobalData:
    pie_menu = None
    show_popup = False
    icon_manager = None
    is_load_brush = False
    config = ''


def draw_callback(op):
    GlobalData.pie_menu.draw_pie(op)


def activate_by_item__(context, space_type, item, index, *, as_fallback=False):
    cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
    tool = ToolSelectPanelHelper._tool_active_from_context(context, space_type, create=True)
    tool_fallback_id = cls.tool_fallback_id

    if as_fallback:
        _item, _select_index, item_group = cls._tool_get_by_id_active_with_group(context, tool_fallback_id)
        if item_group is None:
            raise Exception("Fallback tool doesn't exist")
        index_new = -1
        for i, sub_item in enumerate(item_group):
            if sub_item.idname == item.idname:
                index_new = i
                break
        if index_new == -1:
            raise Exception("Fallback tool not found in group")

        cls._tool_group_active[tool_fallback_id] = index_new
        # Done, now get the current tool to replace the item & index.
        tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type)
        item, index = cls._tool_get_by_id(context, getattr(tool_active, "idname", None))
    else:
        # Ensure the active fallback tool is read from saved state (even if the fallback tool is not in use).
        stored_idname_fallback = tool.idname_fallback
        if stored_idname_fallback:
            cls._tool_group_active_set_by_id(context, tool_fallback_id, stored_idname_fallback)
        del stored_idname_fallback

    # Find fallback keymap.
    item_fallback = None
    _item, select_index = cls._tool_get_by_id(context, tool_fallback_id)
    if select_index != -1:
        item_fallback, _index = cls._tool_get_active_by_index(context, select_index)
    # End calculating fallback.
    tool.setup(
        idname=item.idname,
        keymap=item.keymap[0] if item.keymap is not None else "",
        cursor='DEFAULT',
        gizmo_group=item.widget or "",
        data_block="",
        operator=item.operator or "",
        index=index,
        idname_fallback=(item_fallback and item_fallback.idname) or "",
        keymap_fallback=(item_fallback and item_fallback.keymap and item_fallback.keymap[0]) or "",
    )


class GeometryIconMgr:
    textures = {}
    batches = {}
    icon_none = (0, 0)

    def __init__(self):
        self.shader = gpu.types.GPUShader(icon_2d_vs, icon_2d_fs)
        dirname = os.path.dirname
        self.builtin_icon_dir = bpy.utils.system_resource('DATAFILES', path="icons")
        self.custom_icon_dir = f'{dirname(dirname(__file__))}/icons'
        if "icon_none" not in GeometryIconMgr.textures:
            width = 40
            icon_none = gpu.types.GPUTexture((width, width), data=gpu.types.Buffer('FLOAT', (width, width, 4)))
            GeometryIconMgr.icon_none = (icon_none, gl_utils.get_texture_id(icon_none))

    def draw_icon(self, center, icon_name):
        tex = GeometryIconMgr.textures[icon_name]
        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, tex[1])
        self.shader.bind()
        self.shader.uniform_float("center", center)
        GeometryIconMgr.batches[tex[0].width].draw(self.shader)

    def geom_icon_parse(self, filepath):
        with open(filepath, 'rb') as f:
            data = f.read()
            header = data[:4]
            if not header.startswith(b'VCO'):
                return None
            tri_len = int((len(data) - 8) / 18)
            return tri_len, data[8:8 + 6 * tri_len], data[8 + 6 * tri_len:], data[4], data[5]

    def load_icons(self, width, icon_names):
        if width not in GeometryIconMgr.batches:
            GeometryIconMgr.batches[width] = batch_for_shader(self.shader, 'TRI_FAN',
                                                              {"vert_pos": ((0, 0), (width, 0), (width, width),
                                                                            (0, width)),
                                                               "tex_coord": ((0, 0), (1, 0), (1, 1), (0, 1))})
        for name in icon_names:
            if name in GeometryIconMgr.textures:
                continue
            if name.startswith("brush.particle."):
                filepath = f'{self.builtin_icon_dir}/{name}.dat'
            else:
                filepath = f'{self.custom_icon_dir}/{name}.dat'

            ret = self.geom_icon_parse(filepath)
            if ret:
                ibuf = imbuf.new((ret[3] + 1, ret[4] + 1))
                if particle_hook.geom_icon_to_image(ret[1], ret[2], ret[0], ibuf):
                    ibuf.resize((width, width), method='BILINEAR')
                    buf = gpu.types.Buffer('FLOAT', (width, width, 4))
                    if particle_hook.imbuf_to_float_buffer(ibuf, buf):
                        tex = gpu.types.GPUTexture((width, width), data=buf)
                        GeometryIconMgr.textures[name] = (tex, gl_utils.get_texture_id(tex))
                        continue
            GeometryIconMgr.textures[name] = GeometryIconMgr.icon_none


class PiePainter:
    _circle_color = (0.95, 0.95, 0.95, 1)
    _filled_color = (0.2, 0.2, 0.2, 1)
    _line_width = 2
    _brush_rad = 26
    _brush_seg = 25
    _brush_wrap_seg = 40
    _brush_width = 48
    _brush_bias = _brush_width / 2
    _brush_bg = (0.1, 0.1, 0.1, 1)
    _brush_bg_active = (0.22, 0.32, 0.5, 1)
    _brush_bg_enable = (0.9, 0.9, 0.1, 1)
    _center_rad = 40
    _center_seg = 32
    _center_wrap_seg = 50
    _center_color = (0.4, 0.4, 0.4, 1)
    _center_active_color = (1, 0.6, 0.2, 1)
    _outer_rad = 144
    _outer_seg = 100
    _inner_rad = 90
    _inner_seg = 60
    _inner_wrap_seg = 64
    _opt_rad = 0
    _opt_width = 44
    _opt_bias = _opt_width / 2
    _text_size = 14

    @property
    def circle_color(self):
        return self._circle_color

    @circle_color.setter
    def circle_color(self, value):
        self._circle_color = value

    @property
    def filled_color(self):
        return self._filled_color

    @filled_color.setter
    def filled_color(self, value):
        self._filled_color = value

    @property
    def line_width(self):
        return self._line_width

    @line_width.setter
    def line_width(self, value):
        self._line_width = value

    @property
    def brush_rad(self):
        return adaptation_resolution_value(self._brush_rad)

    @brush_rad.setter
    def brush_rad(self, value):
        self._brush_rad = value

    @property
    def brush_seg(self):
        return self._brush_seg

    @brush_seg.setter
    def brush_seg(self, value):
        self._brush_seg = value

    @property
    def brush_wrap_seg(self):
        return self._brush_wrap_seg

    @brush_wrap_seg.setter
    def brush_wrap_seg(self, value):
        self._brush_wrap_seg = value

    @property
    def brush_width(self):
        return adaptation_resolution_value(self._brush_width)

    @brush_width.setter
    def brush_width(self, value):
        self._brush_width = value

    @property
    def brush_bias(self):
        return adaptation_resolution_value(self._brush_bias)

    @brush_bias.setter
    def brush_bias(self, value):
        self._brush_bias = value

    @property
    def brush_bg(self):
        return self._brush_bg

    @brush_bg.setter
    def brush_bg(self, value):
        self._brush_bg = value

    @property
    def brush_bg_active(self):
        return self._brush_bg_active

    @brush_bg_active.setter
    def brush_bg_active(self, value):
        self._brush_bg_active = value

    @property
    def brush_bg_enable(self):
        return self._brush_bg_enable

    @brush_bg_enable.setter
    def brush_bg_enable(self, value):
        self._brush_bg_enable = value

    @property
    def center_rad(self):
        return adaptation_resolution_value(self._center_rad)

    @center_rad.setter
    def center_rad(self, value):
        self._center_rad = value

    @property
    def center_seg(self):
        return self._center_seg

    @center_seg.setter
    def center_seg(self, value):
        self._center_seg = value

    @property
    def center_wrap_seg(self):
        return self._center_wrap_seg

    @center_wrap_seg.setter
    def center_wrap_seg(self, value):
        self._center_wrap_seg = value

    @property
    def center_color(self):
        return self._center_color

    @center_color.setter
    def center_color(self, value):
        self._center_color = value

    @property
    def center_active_color(self):
        return self._center_active_color

    @center_active_color.setter
    def center_active_color(self, value):
        self._center_active_color = value

    @property
    def outer_rad(self):
        return adaptation_resolution_value(self._outer_rad)

    @outer_rad.setter
    def outer_rad(self, value):
        self._outer_rad = value

    @property
    def outer_seg(self):
        return self._outer_seg

    @outer_seg.setter
    def outer_seg(self, value):
        self._outer_seg = value

    @property
    def inner_rad(self):
        return adaptation_resolution_value(self._inner_rad)

    @inner_rad.setter
    def inner_rad(self, value):
        self._inner_rad = value

    @property
    def inner_seg(self):
        return self._inner_seg

    @inner_seg.setter
    def inner_seg(self, value):
        self._inner_seg = value

    @property
    def inner_wrap_seg(self):
        return self._inner_wrap_seg

    @inner_wrap_seg.setter
    def inner_wrap_seg(self, value):
        self._inner_wrap_seg = value

    @property
    def opt_rad(self):
        return adaptation_resolution_value(self._opt_rad)

    @opt_rad.setter
    def opt_rad(self, value):
        self._opt_rad = value

    @property
    def opt_width(self):
        return adaptation_resolution_value(self._opt_width)

    @opt_width.setter
    def opt_width(self, value):
        self._opt_width = value

    @property
    def opt_bias(self):
        return adaptation_resolution_value(self._opt_bias)

    @opt_bias.setter
    def opt_bias(self, value):
        self._opt_bias = value

    @property
    def text_size(self):
        return adaptation_resolution_value(self._text_size)

    @text_size.setter
    def text_size(self, value):
        self._text_size = value

    tips = {
        0: 'Size Strength',
        1: 'Brush Option',
        2: 'Color Group',
        3: 'Show Color',
        4: 'Scale Brush',
        5: 'Switch 2D/3D',
        6: 'Enable 3D*Brush',
    }
    arc_color = {
        'Active': (0.5, 0.5, 0.5, 1),
        'Enable': (0.3, 0.5, 0.8, 0.7),
        'ActiveEnable': (0.4, 0.6, 0.9, 1)
    }
    opt_icons = {
        0: 'brushopt.particle.options',
        1: 'brushopt.particle.color_group',
        2: 'brushopt.particle.show_color',
        3: 'brushopt.particle.scale',
        4: 'brushopt.particle.switch_mode',
        5: 'brushopt.particle.use3d'
    }

    def __init__(self, context, center, outer_rad=144, inner_rad=90):
        global DISPLAY_WIDTH, DISPLAY_HEIGHT
        display_size = get_display_size()
        is_equal_display = display_size == (DISPLAY_WIDTH, DISPLAY_HEIGHT)
        if not is_equal_display:
            DISPLAY_WIDTH, DISPLAY_HEIGHT = display_size
            GeometryIconMgr.textures.clear()

        self.context = context
        self.center = center
        self._outer_rad = outer_rad
        self._inner_rad = inner_rad
        self._opt_rad = (self._center_rad + inner_rad) / 2
        self.brush_icons = {}
        self.brushes = context.scene.hair_brush_pie_menu.brushs
        self.brush_angle = 360 / len(self.brushes)
        if len(self.brushes) <= 10:
            self._outer_rad = 136
            self._brush_rad = 28

        if not GlobalData.icon_manager:
            GlobalData.icon_manager = GeometryIconMgr()
        GlobalData.icon_manager.load_icons(self.opt_width, tuple(PiePainter.opt_icons.values()))
        for i, item in enumerate(self.brushes):
            self.brush_icons[i] = item.icon_name
        GlobalData.icon_manager.load_icons(self.brush_width, tuple(self.brush_icons.values()))
        self.init_data()

    def init_data(self):
        self.filled_shader = gpu.types.GPUShader(solid_circle_vs, solid_circle_fs)
        self.uniform_shader2d = from_builtin('2D_UNIFORM_COLOR')
        self.area_index = 0
        self.brush_bg_batch = self.__generate_filled_batch(self.brush_rad, self.brush_seg)

        self.inner_batch = self.__generate_filled_batch(self.inner_rad, self.inner_seg)

        self.center_batch = self.__generate_filled_batch(self.center_rad, self.center_seg)
        self.__generate_radial_bs()
        self.__generate_active_arc_bs()
        x, y = self.center
        base_pos = []
        num = len(self.brushes)
        rad = 2 * pi / num
        for i in range(num):
            base_pos.append((cos(rad * i) * self.outer_rad, sin(rad * i) * self.outer_rad))
        self.brush_base_pos = base_pos

        base_pos = []
        rad = pi / 3  # 2pi/6
        offset = pi / 6
        for i in range(6):
            base_pos.append((cos(rad * i + offset) * self.opt_rad + x - self.opt_bias,
                             sin(rad * i + offset) * self.opt_rad + y - self.opt_bias))
        self.opt_icon_pos = base_pos

    def __generate_radial_bs(self):
        points = []
        rad = 2 * pi / 6
        for i in range(6):
            points.append((0, 0))
            points.append((cos(rad * i) * self.inner_rad, sin(rad * i) * self.inner_rad))

        self.radial_shader = gpu.types.GPUShader(radial_lines_vs, radial_lines_fs)
        self.radial_batch = batch_for_shader(self.radial_shader, "LINES", {'vert_pos': points})

    def __generate_active_arc_bs(self):
        points = [(0, 0)]
        for i in range(13):
            points.append((i, self.inner_rad))

        self.arc_shader = gpu.types.GPUShader(acr_active_vs, acr_active_fs)
        self.arc_batch = batch_for_shader(self.arc_shader, 'TRI_FAN', {"vert_pos": points})

    def __generate_filled_batch(self, circle_rad, circle_seg):
        points = [(0, 0)]
        angle = 2 * pi / circle_seg
        for i in range(circle_seg):
            points.append((cos(angle * i) * circle_rad, sin(angle * i) * circle_rad))

        points.append(points[1])
        batch = batch_for_shader(self.filled_shader, "TRI_FAN", {'vert_pos': points})
        return batch

    def draw_brush_and_bg(self):
        cur_tool = ToolSelectPanelHelper._tool_active_from_context(self.context, self.context.space_data.type)
        brush_index = self.context.scene.hair_brush_pie_menu.brushs.find(cur_tool.idname)
        active_index = self.area_index - PieArea.BRUSH_START.value
        uniforms = {"center": None, "color": None}
        for i, base in enumerate(self.brush_base_pos):
            uniforms['center'] = (self.center[0] + base[0], self.center[1] + base[1])
            if i == active_index:
                uniforms['color'] = self.brush_bg_active
            else:
                uniforms['color'] = self.brush_bg
            self.__draw_cutom_shader(self.filled_shader, self.brush_bg_batch, uniforms)

        for i, base in enumerate(self.brush_base_pos):
            x, y = self.center[0] + base[0], self.center[1] + base[1]
            GlobalData.icon_manager.draw_icon((x - self.brush_bias, y - self.brush_bias), self.brush_icons[i])
            if i == brush_index:
                color = self.brush_bg_enable
            else:
                color = self.circle_color
            draw_circle_2d((x, y), color, self.brush_rad, segments=self.brush_wrap_seg)

    def __draw_cutom_shader(self, shader, batch, uniforms):
        shader.bind()
        for key in uniforms:
            shader.uniform_float(key, uniforms[key])
        batch.draw(shader)

    def draw_center_circle(self):
        self.__draw_cutom_shader(self.filled_shader, self.center_batch,
                                 {"center": self.center, "color": self.filled_color})
        color = self.center_color
        if self.area_index == 0:
            color = self.center_active_color
        draw_circle_2d(self.center, color, self.center_rad, segments=self.center_wrap_seg)

    def draw_inner_circle(self):
        hb3_props = self.context.scene.hair_brush_3d
        self.__draw_cutom_shader(self.filled_shader, self.inner_batch,
                                 {"center": self.center, "color": self.filled_color})

        active_index = self.area_index - 1
        for i in range(0, 6):
            key = ''
            if i == active_index:
                key += "Active"
            if i >= 2 and hb3_props.__getattribute__(option_attr[i + 1]):
                key += "Enable"
            if key:
                self.__draw_cutom_shader(self.arc_shader, self.arc_batch,
                                         {"center": self.center, "index": i, "color": self.arc_color[key]})

        for i, pos in enumerate(self.opt_icon_pos):
            GlobalData.icon_manager.draw_icon(pos, PiePainter.opt_icons[i])
        self.__draw_cutom_shader(self.radial_shader, self.radial_batch, {"center": self.center})
        draw_circle_2d(self.center, self.circle_color, self.inner_rad, segments=self.inner_wrap_seg)

    def gl_settings(self):
        glLineWidth(self.line_width)
        glEnable(GL_BLEND)
        glEnable(GL_LINE_SMOOTH)
        glEnable(GL_MULTISAMPLE)
        glEnable(GL_FRAMEBUFFER_SRGB)

    def restore_gl_settings(self):
        glLineWidth(1)
        glDisable(GL_BLEND)
        glDisable(GL_LINE_SMOOTH)
        glDisable(GL_MULTISAMPLE)
        glDisable(GL_FRAMEBUFFER_SRGB)

    def __calc_arc_index(self, dx, dy):
        angle = degrees(atan2(dy, dx))
        if 0 < angle <= 60:
            return 1
        elif 60 < angle <= 120:
            return 2
        elif 120 < angle <= 180:
            return 3
        elif -60 < angle <= 0:
            return 6
        elif -120 < angle <= -60:
            return 5
        else:
            return 4

    def __calc_brush_index(self, dx, dy):
        angle = degrees(atan2(dy, dx))
        if angle < 0:
            angle += 360

        offset = self.brush_angle / 2
        for i in range(len(self.brushes)):
            if i == 0 and (angle > 360 - offset or angle <= offset):
                return PieArea.BRUSH_START.value
            elif (self.brush_angle * i - offset) < angle <= (self.brush_angle * i + offset):
                return PieArea.BRUSH_START.value + i
        return 0

    def get_active_area(self, mouse_pos):
        dx, dy = mouse_pos[0] - self.center[0], mouse_pos[1] - self.center[1]
        distance = hypot(dx, dy)
        if distance <= self.center_rad:
            self.area_index = 0
        elif distance <= self.inner_rad:
            self.area_index = self.__calc_arc_index(dx, dy)
        else:
            self.area_index = self.__calc_brush_index(dx, dy)

        return self.area_index

    def draw_text(self, area0_text):
        if area0_text:
            txts = [area0_text]
        elif self.area_index <= 6:
            txts = self.tips[self.area_index].split()
        else:
            txts = self.brushes[self.area_index - PieArea.BRUSH_START.value].ui_name.split()

        dpi, font_id = 72, 0
        font_size = self.text_size
        if len(txts) == 1:
            font_size += 2
        blf.size(font_id, font_size, dpi)
        blf.color(font_id, 1, 1, 1, 1)
        for i, txt in enumerate(txts):
            w, h = blf.dimensions(0, txt)
            if len(txts) == 1:
                y = self.center[1] - h * 0.3
            else:
                y = self.center[1] - i * h * 1.4 + 7
            blf.position(font_id, self.center[0] - w / 2, y, 0)
            blf.draw(font_id, txt.replace('*', ' '))

    def draw_pie(self, op):
        self.gl_settings()
        draw_circle_2d(self.center, self.circle_color, self.outer_rad, segments=self.outer_rad)
        self.draw_inner_circle()
        self.draw_center_circle()
        self.draw_brush_and_bg()
        self.draw_text(op.size_strength)
        self.restore_gl_settings()


class PARTICLE_OT_BrushPieMenu(Operator):
    bl_idname = "particle.brush_pie_menu"
    bl_label = 'Particle Brush Pie'
    bl_options = {'REGISTER'}

    @classmethod
    def poll(cls, context: bpy.types.Context):
        area = context.area
        return context.mode == 'PARTICLE' and area and area.type == 'VIEW_3D' and context.region.type == "WINDOW" \
               and context.object.particle_systems.active.settings.type == 'HAIR'

    def set_brush_options(self, context, area_index):
        hb3 = context.scene.hair_brush_3d
        val = not hb3.__getattribute__(option_attr[area_index])
        hb3.__setattr__(option_attr[area_index], val)

    def on_exit(self, context):
        space_toolsystem_common._activate_by_item = self.__activate_by_item
        cur_tool = ToolSelectPanelHelper._tool_active_from_context(context, context.space_data.type).idname
        bpy.ops.wm.tool_set_by_id(name='builtin.cursor')
        bpy.ops.wm.tool_set_by_id(name=cur_tool)
        if self._handle:
            SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            self._handle = None

    def modal(self, context, event):
        context.area.tag_redraw()
        if GlobalData.show_popup:
            return {'RUNNING_MODAL'}
        self.size_strength = None
        area_index = GlobalData.pie_menu.get_active_area((event.mouse_region_x, event.mouse_region_y))
        if event.type in ('ESC', 'RIGHTMOUSE'):
            self.on_exit(context)
            return {'FINISHED'}
        elif self.key_first_press and event.type == self.key and event.value == "RELEASE":
            self.key_first_press = False
            if area_index >= PieArea.BRUSH_START.value:
                bpy.ops.wm.tool_set_by_id(name=self.brushes[area_index - PieArea.BRUSH_START.value].name)
                self.on_exit(context)
                return {'FINISHED'}
        elif event.type == "LEFTMOUSE" and event.value == "PRESS":
            if area_index == PieArea.Center.value:
                if event.shift:
                    self.size_strength = 'Strength'
                    data_path = 'tool_settings.particle_edit.brush.strength'
                else:
                    self.size_strength = 'Size'
                    data_path = 'tool_settings.particle_edit.brush.size'
                bpy.ops.wm.radial_control('INVOKE_DEFAULT', data_path_primary=data_path)
            elif area_index == PieArea.BrushOption.value:
                GlobalData.show_popup = True
                bpy.ops.particle.brush_args_popup('INVOKE_DEFAULT')
            elif area_index == PieArea.ColorGroup.value:
                GlobalData.show_popup = True
                bpy.ops.particle.color_group_popup('INVOKE_DEFAULT')
            elif PieArea.ShowColor.value <= area_index <= PieArea.Enable3DBrush.value:
                self.set_brush_options(context, area_index)
                if self.auto_exit:
                    self.on_exit(context)
                    return {'FINISHED'}
            else:
                bpy.ops.wm.tool_set_by_id(name=self.brushes[area_index - PieArea.BRUSH_START.value].name)
                if self.auto_exit:
                    self.on_exit(context)
                    return {'FINISHED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        cur_tool = ToolSelectPanelHelper._tool_active_from_context(context, context.space_data.type).idname
        bpy.ops.wm.tool_set_by_id(name='builtin.cursor')
        self.__activate_by_item = space_toolsystem_common._activate_by_item
        space_toolsystem_common._activate_by_item = activate_by_item__
        bpy.ops.wm.tool_set_by_id(name=cur_tool)

        self.size_strength = None
        self.brushes = context.scene.hair_brush_pie_menu.brushs
        if not GlobalData.is_load_brush or len(self.brushes) == 0:
            PARTICLE_OT_LoadBrushes.load_brush_list(context, self)
        GlobalData.pie_menu = PiePainter(context, self.correct_pie_center(context, event))
        kmi = context.window_manager.keyconfigs.user.keymaps['Window'].keymap_items.get(
            PARTICLE_OT_BrushPieMenu.bl_idname, None)
        self.key = kmi.type if kmi else 'V'
        self.key_first_press = True
        self._handle = SpaceView3D.draw_handler_add(draw_callback, (self,), 'WINDOW', 'POST_PIXEL')
        self.auto_exit = context.scene.hair_brush_pie_menu.auto_exit
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def correct_pie_center(self, context, event):
        x, y = event.mouse_x, event.mouse_y
        area = context.area
        win_x, win_y = area.x, area.y
        width, height = area.width, area.height
        spd = context.space_data
        half_pie = 180
        tool_panel_width = 0
        for reg in area.regions:
            if reg.type == "HEADER" and spd.show_region_header:
                height -= reg.height
            elif reg.type == "UI" and spd.show_region_ui:
                width -= reg.width
            elif reg.type == "TOOLS" and spd.show_region_toolbar:
                tool_panel_width = reg.width

        minx = half_pie
        if context.preferences.system.use_region_overlap:
            if tool_panel_width > 0:
                minx += (tool_panel_width - 15)
        else:
            x -= tool_panel_width
            width -= tool_panel_width

        return self.clip(x - win_x, minx, width - half_pie), self.clip(y - win_y, half_pie, height - half_pie)

    @staticmethod
    def clip(val, min, max):
        if val < min:
            return min
        if val > max:
            return max
        return val


class BasePopup(Operator):
    bl_label = ""
    bl_options = {"INTERNAL"}
    width = 200

    def __del__(self):
        GlobalData.show_popup = False

    def invoke(self, context, event):
        return context.window_manager.invoke_popup(self, width=self.width)

    def execute(self, context):
        return {'FINISHED'}


class PARTICLE_OT_ColorGroupPop(BasePopup):
    bl_idname = "particle.color_group_popup"

    @staticmethod
    def draw_layout(layout, context, show_in_piemenu):
        from . import VIEW3D_PT_hair_color_options
        VIEW3D_PT_hair_color_options.draw_layout(layout, context, show_in_piemenu)

    def draw(self, context):
        self.draw_layout(self.layout, context, False)


class PARTICLE_OT_BrushArgsPop(BasePopup):
    bl_idname = "particle.brush_args_popup"
    width = 150

    @staticmethod
    def draw_layout(layout, context):
        cls = ToolSelectPanelHelper._tool_class_from_space_type(context.space_data.type)
        # ToolDef, WorkSpaceTool,int
        item, tool, icon_value = cls._tool_get_active(context, context.space_data.type, context.mode, True)
        if not item or not (item.idname.startswith("hair_brush_3d.") or item.idname.startswith("builtin_brush.")):
            return
        col = layout.column()
        col.label(text="  " + item.label, icon_value=icon_value)
        if item.draw_settings:
            item.draw_settings(context, layout, tool)
        else:
            settings = context.tool_settings.particle_edit
            brush = settings.brush
            tool = item.label
            col.prop(brush, "size", slider=True)
            if tool == 'ADD':
                col.prop(brush, "count")
                col.prop(settings, "use_default_interpolate")
                col.prop(brush, "steps", slider=True)
                col.prop(settings, "default_key_count", slider=True)
            else:
                col.prop(brush, "strength", slider=True)
                if tool == 'LENGTH':
                    col.prop(brush, "length_mode", expand=True)
                elif tool == 'PUFF':
                    col.prop(brush, "puff_mode", expand=True)
                    col.prop(brush, "use_puff_volume")
                elif tool == 'COMB':
                    col.prop(settings, "use_emitter_deflect")
                    col = layout.column()
                    col.active = settings.use_emitter_deflect
                    col.prop(settings, "emitter_distance")

    def draw(self, context):
        self.draw_layout(self.layout, context)


def get_filtered_tool_items(context):
    def add_item(item):
        if item.idname in pie_menu.brushs:
            return
        pure_idname = os.path.splitext(item.idname)[1]
        if pure_idname.startswith((".cursor", ".select")):
            return
        items.append(item)

    cls = ToolSelectPanelHelper._tool_class_from_space_type(context.space_data.type)
    pie_menu = context.scene.hair_brush_pie_menu

    items = []
    for item in cls.tools_from_context(context, 'PARTICLE'):
        if item is None:
            continue

        if type(item) is tuple:
            for it in item:
                add_item(it)
        else:
            add_item(item)
    return items


def resolve_brush_name(idname):
    label = idname.split(".")[1]
    if idname.startswith("builtin_brush."):
        return f'Default {label}'
    else:
        ui_name = label[0]
        for c in label[1:]:
            if c >= 'A' and c <='Z':
                ui_name += f" {c}"
            else:
                ui_name += c
        return ui_name


class HairBrushPieMenuBrush(PropertyGroup):
    def idname_updated(self, context):
        if self.idname == 'NONE':
            self.idname = self.name
            return
        item = item_from_id(context, 'VIEW_3D', self.idname)
        self.name = item.idname
        self.icon_name = item.icon
        self.ui_name = resolve_brush_name(item.idname)

        PARTICLE_OT_LoadBrushes.save_config(context)

    def enum_idnames(self, context):
        enum_items = []
        i = 0
        items = get_filtered_tool_items(context)
        for item in items:
            ui_name = resolve_brush_name(item.idname)
            enum_items.append((item.idname, ui_name, "", i))
            i += 1
        if not enum_items:
            enum_items.append(('NONE', "There is nothing to change...", "", 0))
        return enum_items

    name: StringProperty(name="Brush Name")
    idname: EnumProperty(name="Change Brush",
                         items=enum_idnames,
                         update=idname_updated)
    icon_name: StringProperty(name="Brush Icon Name")
    ui_name: StringProperty(name="Ui Name")


class HairBrushPieMenuProps(PropertyGroup):
    brushs: CollectionProperty(
        type=HairBrushPieMenuBrush,
        name="Brushs",
        description="Brushs of hair brush pie menu")

    active_brush_index: IntProperty(name="Active Brush Index")
    auto_exit: BoolProperty(
        name="Auto Hiding Pie Menu",
        description="Enable it to hide Pie Menu after left-clicking, or press right button or 'ESC' to do so",
        default=True)

    @classmethod
    def register(cls):
        bpy.types.Scene.hair_brush_pie_menu = bpy.props.PointerProperty(type=cls)

    @classmethod
    def unregister(cls):
        del bpy.types.Scene.hair_brush_pie_menu


class PARTICLE_OT_ResetPieMenu(BaseOperator):
    bl_idname = "particle.reset_pie_menu"
    bl_label = "Reset"
    bl_description = "Reset pie menu to default"
    bl_options = {'INTERNAL'}

    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)

    def execute(self, context):
        try:
            if os.path.isfile(GlobalData.config):
                os.remove(GlobalData.config)
        except:
            pass
        PARTICLE_OT_LoadBrushes.load_brush_list(context, self)
        return {'FINISHED'}


class PARTICLE_OT_LoadBrushes(BaseOperator):
    bl_idname = "particle.piemenu_load_brush"
    bl_label = "Load Brush List"
    bl_description = "Load brush list to custom a pie menu"
    bl_options = {'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return GlobalData.is_load_brush == False

    @staticmethod
    def save_config(context, op=None):
        config = []
        pie_menu = context.scene.hair_brush_pie_menu.brushs
        for item in pie_menu:
            config.append([item.name, item.icon_name])
        try:
            if not os.path.exists(os.path.dirname(GlobalData.config)):
                os.makedirs(os.path.dirname(GlobalData.config))
            with open(GlobalData.config, "w", encoding="utf-8") as fp:
                json.dump(config, fp, indent=4)
        except Exception as e:
            if op:
                op.report({"ERROR"}, "Failed to save pie menu configuration!")
            print(repr(e))

    @staticmethod
    def load_config(filepath, op=None):
        try:
            with open(filepath, "r", encoding="utf-8") as fp:
                return json.load(fp)
        except Exception as e:
            if op:
                op.report({"ERROR"}, "Failed to load pie menu config, and already reset to default!")
            print(repr(e))
            return None

    @staticmethod
    def load_brush_list(context, op=None):
        GlobalData.is_load_brush = True
        pie_menu = context.scene.hair_brush_pie_menu.brushs
        pie_menu.clear()
        brushes = None
        if os.path.isfile(GlobalData.config):
            brushes = PARTICLE_OT_LoadBrushes.load_config(GlobalData.config, op)
        if not brushes:
            brushes = default_brush

        for brush in brushes:
            item = pie_menu.add()
            item.name = brush[0]
            item.icon_name = brush[1]
            item.ui_name = resolve_brush_name(brush[0])

    def execute(self, context):
        self.load_brush_list(context, self)
        return {'FINISHED'}


class PARTICLE_OT_AddBrush(BaseOperator):
    bl_idname = "particle.piemenu_add_brush"
    bl_label = "Add"
    bl_description = "Add brush to pie menu"
    bl_options = {'INTERNAL'}

    brush_idname: StringProperty(name="Brush Name", options={'HIDDEN', 'SKIP_SAVE'})

    @classmethod
    def poll(cls, context: bpy.types.Context):
        return len(context.scene.hair_brush_pie_menu.brushs) < 14

    def execute(self, context: bpy.types.Context):
        pie_menu = context.scene.hair_brush_pie_menu
        if self.brush_idname in pie_menu.brushs:
            self.report({'ERROR'}, "The brush had added!")
            return {'CANCELLED'}

        # pure_idname = os.path.splitext(self.brush_idname)[1]
        # if pure_idname.startswith((".cursor", ".select")):
        #     self.report({'ERROR'}, "The brush is not a hair brush!")
        #     return {'CANCELLED'}
        context.area.tag_redraw()
        item = item_from_id(context, 'VIEW_3D', self.brush_idname)
        brush = pie_menu.brushs.add()
        brush.name = self.brush_idname
        brush.icon_name = item.icon
        brush.ui_name = resolve_brush_name(self.brush_idname)

        PARTICLE_OT_LoadBrushes.save_config(context, self)
        return {'FINISHED'}

    def invoke(self, context, event):
        if not self.brush_idname:
            return context.window_manager.invoke_popup(self, width=200)
        return self.execute(context)

    def draw(self, context):
        col = self.layout.column(align=True)
        col.scale_y = 1.2
        items = get_filtered_tool_items(context)
        if not items:
            col.label(text="You have added all brushs...")
            return

        for item in items:
            col.operator(PARTICLE_OT_AddBrush.bl_idname, text=resolve_brush_name(item.idname),
                         icon_value=ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
                         ).brush_idname = item.idname


class PARTICLE_OT_DeleteBrush(BaseOperator):
    bl_idname = "particle.piemenu_delete_brush"
    bl_label = "Remove Brush"
    bl_description = "Remove brush"
    bl_options = {'INTERNAL'}

    index: IntProperty(name="Brush Index", default=0, options={'HIDDEN'})
    all: BoolProperty(name="Delete All", default=False, options={'SKIP_SAVE', 'HIDDEN'})

    @classmethod
    def poll(cls, context):
        return len(context.scene.hair_brush_pie_menu.brushs) > 1

    def execute(self, context: bpy.types.Context):
        pie_menu = context.scene.hair_brush_pie_menu
        if len(pie_menu.brushs) <= 1:
            return {'CANCELLED'}

        context.area.tag_redraw()
        if self.all:
            pie_menu.brushs.clear()
            pie_menu.active_brush_index = 0
            item = pie_menu.brushs.add()
            item.name = "hair_brush_3d.Comb"
            item.icon_name = 'brush3d.particle.comb'
            item.ui_name = "Comb"
        elif self.index < len(pie_menu.brushs):
            pie_menu.brushs.remove(self.index)

        PARTICLE_OT_LoadBrushes.save_config(context, self)
        return {'FINISHED'}

    def invoke(self, context, event):
        if self.all:
            return context.window_manager.invoke_props_dialog(self, width=200)
        return self.execute(context)

    def draw(self, context):
        self.layout.label(text="Remove all brushes?", icon='ERROR')


class PARTICLE_OT_MoveBrush(BaseOperator):
    bl_idname = "particle.piemenu_move_brush"
    bl_label = "Move Brush"

    direction: EnumProperty(
        name="Move Direction",
        items=[('UP', "Move up", ""),
               ('DOWN', "Move down", "")],
        options={'HIDDEN'})

    @classmethod
    def description(cls, context, props):
        if props.direction == 'UP':
            return "Move Up"
        else:
            return "Move Down"

    @classmethod
    def poll(cls, context):
        return len(context.scene.hair_brush_pie_menu.brushs) > 1

    def execute(self, context: bpy.types.Context):
        pie_menu = context.scene.hair_brush_pie_menu
        brushs = pie_menu.brushs
        active_index = pie_menu.active_brush_index
        if self.direction == 'UP':
            target = active_index - 1 if active_index - 1 > -1 else len(brushs) - 1
        else:
            target = active_index + 1 if active_index + 1 < len(brushs) else 0
        brushs.move(active_index, target)
        pie_menu.active_brush_index = target
        PARTICLE_OT_LoadBrushes.save_config(context, self)
        return {'FINISHED'}


class PARTICLE_UL_piemenu_brushs(UIList):
    def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_propname, index):
        row = layout.row()
        split = row.split(factor=0.3)
        split.template_icon(icon_value=ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon_name))
        split.label(text=item.ui_name)
        row.prop(item, "idname", text="", icon_only=True, emboss=False)
        row.operator(PARTICLE_OT_DeleteBrush.bl_idname, text="", icon='X', emboss=False).index = index


class VIEW3D_PT_hair_brush_piemenu(Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Brush Pie Menu"
    bl_context = ".particlemode"
    bl_label = "Brush Pie Menu"

    def draw(self, context):
        layout = self.layout
        if not GlobalData.is_load_brush:
            layout.operator(PARTICLE_OT_LoadBrushes.bl_idname, icon="BRUSH_DATA")
            return
        pie_menu = context.scene.hair_brush_pie_menu
        row = layout.row()
        row.template_list("PARTICLE_UL_piemenu_brushs", "", pie_menu, "brushs", pie_menu,
                          "active_brush_index", rows=max(len(pie_menu.brushs), 3), sort_lock=True)

        row = layout.row(align=True)
        row.operator(PARTICLE_OT_AddBrush.bl_idname)
        row.operator(PARTICLE_OT_MoveBrush.bl_idname, icon='TRIA_UP', text="").direction = 'UP'
        row.operator(PARTICLE_OT_MoveBrush.bl_idname, icon='TRIA_DOWN', text="").direction = 'DOWN'

        row = layout.row(align=True)
        row.operator(PARTICLE_OT_DeleteBrush.bl_idname, text="Clear").all = True
        row.operator(PARTICLE_OT_ResetPieMenu.bl_idname)

        col = layout.column()
        col.prop(pie_menu, 'auto_exit')


class VIEW3D_MT_HairBrushOption(Menu):
    bl_idname = "VIEW3D_MT_hair_brush_opt"
    bl_label = "Brush Option"

    @classmethod
    def poll(cls, context: bpy.types.Context):
        return context.mode == 'PARTICLE' and context.area and context.area.type == 'VIEW_3D' \
               and context.object.particle_systems.active.settings.type == 'HAIR'

    def draw(self, context):
        pie = self.layout.menu_pie()
        col = pie.box().column(align=True)
        col.scale_x = 0.8
        PARTICLE_OT_ColorGroupPop.draw_layout(col, context, True)

        col = pie.box().column(align=True)
        cur_tool = ToolSelectPanelHelper._tool_active_from_context(context, context.space_data.type).idname
        if cur_tool != 'hair_brush_3d.Clone':
            col.scale_x = 1.2
        PARTICLE_OT_BrushArgsPop.draw_layout(col, context)


classes = (
    PARTICLE_OT_AddBrush, PARTICLE_OT_DeleteBrush, PARTICLE_OT_MoveBrush,
    VIEW3D_PT_hair_brush_piemenu, HairBrushPieMenuBrush, HairBrushPieMenuProps,
    PARTICLE_UL_piemenu_brushs, PARTICLE_OT_BrushPieMenu, PARTICLE_OT_ColorGroupPop,
    PARTICLE_OT_BrushArgsPop, PARTICLE_OT_LoadBrushes, PARTICLE_OT_ResetPieMenu,
    VIEW3D_MT_HairBrushOption,
)


def register():
    GlobalData.config = os.path.join(bpy.utils.script_path_user(), 'presets/operator/3DHairBrush/pie_menu.cfg')
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)


def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)
