import bpy
import bgl
import gpu
import math
import sys
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from . import shader_utils
from . import batch_utils
from . import buffer_utils
from .math_utils import *
from .gl_utils import gl_utils


IS_MAC_OS = sys.platform.startswith("darwin")

G_points_batch = None

def get_points_batch():
    global G_points_batch
    if G_points_batch is None:
        fmt = gpu.types.GPUVertFormat()
        fmt.attr_add(id="dummy", comp_type='F32', len=1, fetch_mode='FLOAT')
        vbuf = buffer_utils.GLVertBuf(1, fmt)
        G_points_batch = batch_utils.GLBatch(bgl.GL_POINTS, vbuf)
    return G_points_batch


G_dummy_texbuf = None
G_dummy_vbuf = None

def get_dummy_texbuf():
    global G_dummy_texbuf, G_dummy_vbuf
    if G_dummy_texbuf is None:
        fmt = gpu.types.GPUVertFormat()
        fmt.attr_add(id="dummy", comp_type='F32', len=4, fetch_mode='FLOAT')
        G_dummy_vbuf = buffer_utils.GLVertBuf(1, fmt)
        G_dummy_vbuf.attr_fill("dummy", [(0, 0, 0, 0)])
        G_dummy_vbuf.use_data()
        G_dummy_texbuf = buffer_utils.GLTextureBuf(G_dummy_vbuf)
    return G_dummy_texbuf


G_hair_frand_texbuf = None
G_hair_frand_vbuf = None

def get_hair_frand_texbuf():
    global G_hair_frand_texbuf, G_hair_frand_vbuf
    if G_hair_frand_texbuf is None:
        fmt = gpu.types.GPUVertFormat()
        fmt.attr_add(id="frand_seed", comp_type='U32', len=4, fetch_mode='INT')
        G_hair_frand_vbuf = buffer_utils.GLVertBuf(1024, fmt)
        gl_utils.fill_hair_frand_buffer(G_hair_frand_vbuf.get_vert_buffer())
        G_hair_frand_vbuf.use_data()
        G_hair_frand_texbuf = buffer_utils.GLTextureBuf(G_hair_frand_vbuf)
    return G_hair_frand_texbuf


def get_viewvecs_list(winmat):
    is_presp = winmat[3][3] == 0
    viewvecs = [[0, 0, 0, 0]] * 2
    if is_presp:
        viewvecs[0][3] = -winmat[2][3] / (winmat[2][2] - 1)
        viewvecs[1][3] = -winmat[2][3] / (winmat[2][2] + 1)
    else:
        viewvecs[0][3] = -(winmat[2][3] + 1) / winmat[2][2]
        viewvecs[1][3] = -(winmat[2][3] - 1) / winmat[2][2]
    wininv = winmat.inverted()
    _viewvecs = [
        Vector((-1, -1, -1)),
        Vector((1, -1, -1)),
        Vector((-1, 1, -1)),
        Vector((-1, -1, 1)),
    ]

    for i in range(4):
        vec = mul_project_m4_v3(wininv, _viewvecs[i])
        if is_presp:
            vec.xy *= 1 / vec[2]
        _viewvecs[i] = vec

    viewvecs[0][0], viewvecs[0][1], viewvecs[0][2] = _viewvecs[0].xyz
    viewvecs[1][0] = _viewvecs[1][0] - _viewvecs[0][0]
    viewvecs[1][1] = _viewvecs[2][1] - _viewvecs[0][1]
    viewvecs[1][2] = _viewvecs[3][2] - _viewvecs[0][2]
    return [v for vv in viewvecs for v in vv]


def get_view_block_buf(viewmat, winmat):
    vblist = mat_to_list(viewmat)
    vblist.extend(mat_to_list(winmat))
    # vblist.extend(self.get_viewvecs_list(winmat))
    return gpu.types.Buffer('FLOAT', len(vblist), vblist)


def get_defalut_light_data_list(shading, viewmat):
    if shading.use_world_space_lighting:
        rotmat = viewmat @ Matrix.Rotation(-shading.studiolight_rotate_z, 4, 'Z')
        rotmat.col[1].xyz, rotmat.col[2].xyz = rotmat.col[2].xyz, rotmat.col[1].xyz
        rotmat.col[2].xyz = -rotmat.col[2].xyz
        rotmat.col[3].xyz = (0, 0, 0)
    else:
        rotmat = Matrix.Identity(4)

    ldlist = [
        *(rotmat @ Vector((-0.352546, 0.170931, -0.920051))).to_tuple(), 0,     # direction
        0.266761, 0.266761, 0.266761, 0,                                        # spec
        0.033103, 0.033103, 0.033103, 0.526620,                                 # diffuse
        *(rotmat @ Vector((-0.408163, 0.346939, 0.844415))).to_tuple(), 0,
        0.599030, 0.599030, 0.599030, 0,
        0.521083, 0.538226, 0.538226, 0,
        *(rotmat @ Vector((0.521739, 0.826087, 0.212999))).to_tuple(), 0,
        0.106102, 0.125981, 0.158523, 0,
        0.038403, 0.034357, 0.049530, 0.478261,
        *(rotmat @ Vector((0.624519, -0.562067, -0.542269))).to_tuple(), 0,
        0.106535, 0.084771, 0.066080, 0,
        0.090838, 0.082080, 0.072255, 0.2
    ]
    return ldlist


def get_world_data_buf(region, v3d, rv3d):
    wdlist = [region.width, region.height, 1.0 / region.width, 1.0 / region.height]
    wdlist.extend(get_defalut_light_data_list(v3d.shading, rv3d.view_matrix))
    wdlist.extend([0, 0, 0, 0, 0, 1 if v3d.shading.show_specular_highlight else 0, 0, 0])
    return gpu.types.Buffer('FLOAT', len(wdlist), wdlist)


class ColorUpdateFlag:
    FORCE_TEX = 1
    FROM_DEPS = 2
    CLEAR_TEX = 4
    LOADED = 8


class EditHairColorDrawData():
    COLOR_TEX_WIDTH = 1024

    def __init__(self, context, hair_color_getter):
        self._hair_color_getter = hair_color_getter
        if context.object:
            deps = context.evaluated_depsgraph_get()
            self._obj = context.object.evaluated_get(depsgraph=deps)
            self._psys = context.object.particle_systems.active
        else:
            self._obj = self._psys = None
        self.view_block_ubo = gpu.types.GPUUniformBuf(gpu.types.Buffer('FLOAT', 32))
        self._edit_hair_color_shader = gpu.types.GPUShader(
            vertexcode=shader_utils.get_shader_src(shader_utils.DataType.EDIT_HAIR, shader_utils.ShaderType.VERT),
            fragcode=shader_utils.get_shader_src(shader_utils.DataType.EDIT_HAIR, shader_utils.ShaderType.FRAG))
        self._color_tex_loc = self._edit_hair_color_shader.uniform_from_name("strandsColorTex")
        self._shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
        self._batch = batch_for_shader(self._shader, 'POINTS', {"pos": ((1, 1, 1),)})
        self._color_tex = None
        self._color_tex_id = -1
        self._is_color_tex_dirty = False
        self._strands_len = 0
        self._pause = False

    def update_color_texture(self, strands_len):
        self._is_color_tex_dirty = False
        if strands_len <= 0:
            self._strands_len = 0
            return

        width = self.COLOR_TEX_WIDTH
        height = math.ceil(strands_len / width)
        color_buf = bgl.Buffer(bgl.GL_FLOAT, width * height * 3)
        if not self._hair_color_getter(self._psys.as_pointer(), color_buf):
            self._strands_len = 0
            return

        if self._color_tex is None or self._color_tex.height < height:
            self._color_tex = gpu.types.GPUTexture((width, height), format='RGB16F')
            self._color_tex_id = gl_utils.get_texture_id(self._color_tex)
        elif strands_len < self._strands_len:
            self._color_tex.clear(format='FLOAT', value=(0.0, 0.0, 0.0, 1.0))

        bgl.glActiveTexture(bgl.GL_TEXTURE0)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._color_tex_id)
        bgl.glTexSubImage2D(bgl.GL_TEXTURE_2D, 0, 0, 0, width, height,
                            bgl.GL_RGB, bgl.GL_FLOAT, color_buf)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
        self._strands_len = strands_len

    def uniform_texture(self):
        bgl.glActiveTexture(bgl.GL_TEXTURE0)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._color_tex_id)
        bgl.glUniform1i(self._color_tex_loc, 0)

    def update_data(self, context, update_flag=0):
        self._pause = True
        need_update = False
        if not getattr(context, "object", None):
            return

        if update_flag & ColorUpdateFlag.CLEAR_TEX and self._color_tex:
            self._color_tex.clear(format='FLOAT', value=(0.0, 0.0, 0.0, 1.0))

        deps = context.evaluated_depsgraph_get()
        obj = context.object.evaluated_get(depsgraph=deps)
        if (context.object.mode != 'PARTICLE_EDIT' or context.object.type != 'MESH'
                or context.tool_settings.particle_edit.tool == 'WEIGHT'):
            if update_flag & ColorUpdateFlag.LOADED:
                self._is_color_tex_dirty = True
            return

        if obj != self._obj:
            self._obj = obj
        if self._psys != context.object.particle_systems.active:
            self._psys = context.object.particle_systems.active
            need_update = True
        if need_update or update_flag & ColorUpdateFlag.FORCE_TEX:
            pset = self._psys.settings
            if "hair_color__" in pset and pset['hair_color__']:
                _, _, strands_len = gl_utils.get_hair_cache_info(self._psys.as_pointer(), True)
                if strands_len > 0:
                    self.update_color_texture(strands_len)
                else:
                    self._strands_len = 0
                    if update_flag & ColorUpdateFlag.FROM_DEPS:
                        self._is_color_tex_dirty = True
            else:
                self._strands_len = 0
        if update_flag & ColorUpdateFlag.LOADED:
            self._is_color_tex_dirty = True
        self._pause = False

    def draw(self, rv3d, strands_res):
        if self._pause or not self._obj or not self._psys:
            return

        if self._is_color_tex_dirty:
            _, _, strands_len = gl_utils.get_hair_cache_info(self._psys.as_pointer(), True)
            self.update_color_texture(strands_len)

        if self._strands_len == 0 or not self._color_tex:
            return

        ori_batch = gl_utils.set_edit_hair_gpu_batch(
            self._obj.particle_systems.active.as_pointer(), self._batch)
        if ori_batch is None:
            return

        self.view_block_ubo.update(get_view_block_buf(rv3d.view_matrix, rv3d.window_matrix))

        bgl.glEnable(bgl.GL_DEPTH_TEST)
        bgl.glDepthFunc(bgl.GL_LEQUAL)

        self._edit_hair_color_shader.bind()
        self._edit_hair_color_shader.uniform_block("viewBlock", self.view_block_ubo)
        self.uniform_texture()
        self._edit_hair_color_shader.uniform_int("hairStrandsRes", strands_res)
        self._edit_hair_color_shader.uniform_int("colorTexWidth", self._color_tex.width)
        self._batch.draw(self._edit_hair_color_shader)
        gl_utils.restore_gpu_batch(self._batch, ori_batch)

        bgl.glDisable(bgl.GL_DEPTH_TEST)


class MaterialDataManager():
    MAT_COUNT_PER_UBO = 100

    class MaterialData:
        __slots__ = ("ubo", "buffer")

        def __init__(self) -> None:
            self.buffer = gpu.types.Buffer('FLOAT', 4 * MaterialDataManager.MAT_COUNT_PER_UBO)
            self.ubo = gpu.types.GPUUniformBuf(self.buffer)

        def update(self):
            self.ubo.update(self.buffer)


    def __init__(self, shading: bpy.types.View3DShading) -> None:
        data = MaterialDataManager.MaterialData()
        self._color_type = shading.color_type
        def_mat_info = [0.8, 0.8, 0.8, 1.0, 0.632455532, 0.0]
        if shading.color_type == 'SINGLE':
            def_mat_info[0] = shading.single_color.r
            def_mat_info[1] = shading.single_color.g
            def_mat_info[2] = shading.single_color.b
        gl_utils.pack_material_info_to_buffer(data.buffer, 0, *def_mat_info)
        data.update()
        self._datas = [data]
        self._mats = dict()
        self.init()

    def init(self):
        self._mats.clear()
        self._cur_data_idx = 0
        self._cur_data_mat_idx = 1

    def add_material(self, mat):
        if self._color_type == 'SINGLE':
            return (0, 0)
        if mat in self._mats:
            return self._mats[mat]
        self._cur_data_mat_idx += 1
        if self._cur_data_mat_idx >= self.MAT_COUNT_PER_UBO:
            self._cur_data_idx += 1
            if self._cur_data_idx >= len(self._datas):
                self._datas.append(MaterialDataManager.MaterialData())
            self._cur_data_mat_idx = 0
        data = self._datas[self._cur_data_idx]
        gl_utils.pack_material_info_to_buffer(data.buffer,
                                              self._cur_data_mat_idx,
                                              *mat.diffuse_color,
                                              math.sqrt(mat.roughness),
                                              mat.metallic)
        ret = (self._cur_data_idx, self._cur_data_mat_idx)
        self._mats[mat] = (self._cur_data_idx, self._cur_data_mat_idx)
        return ret

    def bind_material_data(self, shader, mat_data):
        data_idx, mat_idx = mat_data
        if data_idx >= len(self._datas):
            data_idx = 0
        if mat_idx >= self.MAT_COUNT_PER_UBO:
            mat_idx = 0
        shader.uniform_block("material_block", self._datas[data_idx].ubo)
        shader.uniform_int("materialIndex", mat_idx)

    def update(self):
        if self._cur_data_idx == 0 and self._cur_data_mat_idx == 1:
            return
        for data in self._datas:
            data.update()


class HairDrawData():
    class HairFinalCache:
        __slots__ = ("buf", "tex", "hairs", "strands_res")

        def __init__(self) -> None:
            self.buf = None
            self.tex = None
            self.hairs = [None, None]
            self.strands_res = 0

    class HairSettings:
        __slots__ = ("shape", "root_rad", "tip_rad", "close_tip", "rad_scale",
                     "use_shape_curve", "use_shape_random", "random_seed",
                     "shape_curve_cur", "radius_range_start",
                     "random_threshold", "shape_curve_mt",
                     "shape_curve_buffer", "shape_curve_ubo")

        def __init__(self, part) -> None:
            self.shape_curve_buffer = None
            self.shape_curve_cur = -1
            self.shape_curve_mt = 0
            self.update(part)

        def update(self, part):
            self.shape = part.shape
            self.root_rad = part.root_radius * part.radius_scale * 0.5
            self.tip_rad = part.tip_radius * part.radius_scale * 0.5
            self.close_tip = part.use_close_tip
            self.rad_scale = part.radius_scale * 0.5
            cycles = part.cycles
            self.use_shape_curve = cycles.use_shape_curve
            self.use_shape_random = cycles.use_shape_random
            self.random_seed = cycles.random_seed
            self.radius_range_start = cycles.radius_range_start
            self.random_threshold = cycles.random_threshold
            if not self.use_shape_curve:
                return

            node = part.cycles.shape_curve.nodes[0]
            node.mapping.initialize()
            if self.shape_curve_buffer is None:
                self.shape_curve_buffer = gpu.types.Buffer('FLOAT', 260)
                _, self.shape_curve_cur, self.shape_curve_mt = gl_utils.update_curve_mapping_table(
                    node.mapping.as_pointer(), self.shape_curve_buffer,
                    self.shape_curve_cur, self.shape_curve_mt)
                self.shape_curve_ubo = gpu.types.GPUUniformBuf(self.shape_curve_buffer)
            else:
                updated, self.shape_curve_cur, self.shape_curve_mt = gl_utils.update_curve_mapping_table(
                    node.mapping.as_pointer(), self.shape_curve_buffer,
                    self.shape_curve_cur, self.shape_curve_mt)
                if updated:
                    self.shape_curve_ubo.update(self.shape_curve_buffer)

    def __init__(self, psys, tf_shader: shader_utils.GLShader) -> None:
        self.init(psys)
        self._settings = HairDrawData.HairSettings(psys.settings)
        self._tf_shader = tf_shader
        self._is_cache_dirty = False
        self._need_update_tf = False
        self._is_edit_update = False

    def init(self, psys):
        self._psys: bpy.types.ParticleSystem = psys
        self._point_buf = None
        self._strand_buf = None
        self._strand_seg_buf = None
        self._strand_color_index_buf = None
        self._point_tex = None
        self._strand_tex = None
        self._strand_seg_tex = None
        self._strand_color_index_tex = None
        self._color_group_buf = None
        self._color_group_ubo = None
        self._finals = [None, None, None, None]
        self._point_len = 0
        self._strands_len = 0
        self._mat_data = (0, 0)

    def ensure_positions(self):
        if self._point_buf and self._is_cache_dirty is False:
            return

        if IS_MAC_OS:
            self._point_tex = None
        fmt = gpu.types.GPUVertFormat()
        fmt.attr_add(id="posTime", comp_type='F32', len=4, fetch_mode='FLOAT')
        self._point_buf = buffer_utils.GLVertBuf(self._point_len, fmt)
        gl_utils.fill_hair_position_buffer(
            self._psys.as_pointer(), self._point_buf.get_vert_buffer())
        self._point_buf.use_data()
        if self._point_tex:
            self._point_tex.rebind_vertbuf(self._point_buf)
        else:
            self._point_tex = buffer_utils.GLTextureBuf(self._point_buf)
        self._need_update_tf = True

    def get_material_data(self):
        return self._mat_data

    def ensure_strand_data(self):
        if self._strand_tex and self._is_cache_dirty is False:
            return

        if IS_MAC_OS:
            self._strand_tex = None
            self._strand_seg_tex = None
        data_fmt = gpu.types.GPUVertFormat()
        data_fmt.attr_add(id="data", comp_type='U32', len=1, fetch_mode='INT')
        self._strand_buf = buffer_utils.GLVertBuf(self._strands_len, data_fmt)
        data_fmt = gpu.types.GPUVertFormat()
        data_fmt.attr_add(id="data", comp_type='U16', len=1, fetch_mode='INT')
        self._strand_seg_buf = buffer_utils.GLVertBuf(self._strands_len, data_fmt)
        gl_utils.fill_hair_strand_data(self._psys.as_pointer(),
                                       self._strand_buf.get_vert_buffer(),
                                       self._strand_seg_buf.get_vert_buffer())
        self._strand_buf.use_data()
        if self._strand_tex:
            self._strand_tex.rebind_vertbuf(self._strand_buf)
        else:
            self._strand_tex = buffer_utils.GLTextureBuf(self._strand_buf)
        self._strand_seg_buf.use_data()
        if self._strand_seg_tex:
            self._strand_seg_tex.rebind_vertbuf(self._strand_seg_buf)
        else:
            self._strand_seg_tex = buffer_utils.GLTextureBuf(self._strand_seg_buf)

    def ensure_strand_color(self, show_color):
        if show_color and self._strand_color_index_tex and self._is_cache_dirty is False:
            return

        part = self._psys.settings
        if part.original:
            part = part.original
        if not show_color or "hair_color__" not in part or not part['hair_color__']:
            self._strand_color_index_tex = None
            self._strand_color_index_buf = None
            return

        if IS_MAC_OS:
            self._strand_color_index_tex = None
        data_fmt = gpu.types.GPUVertFormat()
        data_fmt.attr_add(id="data", comp_type='U8', len=1, fetch_mode='INT')
        self._strand_color_index_buf = buffer_utils.GLVertBuf(self._strands_len, data_fmt)
        if self._color_group_buf is None:
            self._color_group_buf = gpu.types.Buffer('FLOAT', 4 * 256)
        if not gl_utils.fill_hair_strand_color_buf(self._psys.as_pointer(),
                self._strand_color_index_buf.get_vert_buffer(), self._color_group_buf):
            self._strand_color_index_tex = None
            self._strand_color_index_buf = None
            return

        if self._color_group_ubo is None:
            self._color_group_ubo = gpu.types.GPUUniformBuf(self._color_group_buf)
        else:
            self._color_group_ubo.update(self._color_group_buf)
        self._strand_color_index_buf.use_data()
        if self._strand_color_index_tex:
            self._strand_color_index_tex.rebind_vertbuf(self._strand_color_index_buf)
        else:
            self._strand_color_index_tex = buffer_utils.GLTextureBuf(self._strand_color_index_buf)

    def ensure_final_cache_buffer(self, final):
        if final.buf and self._is_cache_dirty is False:
            return

        if IS_MAC_OS:
            final.tex = None
        fmt = gpu.types.GPUVertFormat()
        fmt.attr_add(id="pos", comp_type='F32', len=4, fetch_mode='FLOAT')
        final.buf = buffer_utils.GLVertBuf(final.strands_res * self._strands_len, fmt)
        final.buf.use_data()
        if final.tex:
            final.tex.rebind_vertbuf(final.buf)
        else:
            final.tex = buffer_utils.GLTextureBuf(final.buf)
        self._need_update_tf = True

    def ensure_final_cache_batch(self, final, thickness_res):
        if final.hairs[thickness_res - 1] and self._is_cache_dirty is False:
            return

        verts_per_hair = final.strands_res * thickness_res
        elem_count = (verts_per_hair + 1) * self._strands_len
        if thickness_res == 1:
            prim_type = bgl.GL_LINE_STRIP
        else:
            prim_type = bgl.GL_TRIANGLE_STRIP
        fmt = gpu.types.GPUVertFormat()
        fmt.attr_add(id="dummy", comp_type='U8', len=1, fetch_mode='INT_TO_FLOAT_UNIT')
        vbuf = buffer_utils.GLVertBuf(1, fmt)
        ibuf = buffer_utils.GLIndexBuf(elem_count)
        real_len = gl_utils.fill_hair_segments_indices(
            self._psys.as_pointer(), ibuf.get_index_buffer(), verts_per_hair, elem_count)
        assert real_len <= elem_count
        ibuf.set_len(real_len)
        final.hairs[thickness_res - 1] = batch_utils.GLBatch(prim_type, vbuf, ibuf)

    def calc_final_point_tex(self, final):
        final_points_len = final.strands_res * self._strands_len
        if final_points_len > 0:
            batch = get_points_batch()
            self._tf_shader.bind()
            self._tf_shader.transform_feedback_enable(final.buf.get_vbo_id())
            self._tf_shader.uniform_texture("hairPointBuffer", self._point_tex)
            self._tf_shader.uniform_texture("hairStrandBuffer", self._strand_tex)
            self._tf_shader.uniform_texture("hairStrandSegBuffer", self._strand_seg_tex)
            self._tf_shader.uniform_int("hairStrandsRes", final.strands_res)
            batch.draw(self._tf_shader, 0, final_points_len)
            self._tf_shader.transform_feedback_disable()
            self._tf_shader.unbind()

    def update_hair_cache(self, psys, subdiv, thickness_res, show_color):
        part = psys.settings
        final = self._finals[subdiv]
        final.strands_res = 1 << (part.display_step + subdiv)
        self._point_len, _, self._strands_len = gl_utils.get_hair_cache_info(self._psys.as_pointer(), False)
        if self._point_len == 0 or self._strands_len == 0:
            return
        self.ensure_positions()
        self.ensure_strand_data()
        self.ensure_strand_color(show_color)
        self.ensure_final_cache_buffer(final)
        self.ensure_final_cache_batch(final, thickness_res)
        self._is_cache_dirty = False
        if self._need_update_tf:
            self.calc_final_point_tex(final)
            self._need_update_tf = False

    def update_data(self, ob, psys, subdiv, thickness_res, mat_data_mgr, show_color):
        self._psys = psys
        part = psys.settings
        if self._finals[subdiv] is None:
            self._finals[subdiv] = HairDrawData.HairFinalCache()

        if part.material <= len(ob.material_slots) and ob.material_slots[part.material - 1].material:
            self._mat_data = mat_data_mgr.add_material(ob.material_slots[part.material - 1].material)
        else:
            self._mat_data = (0, 0)

        self._settings.update(part)
        self._is_cache_dirty = gl_utils.hair_cache_is_dirty(psys.as_pointer())

        if ob.mode == 'PARTICLE_EDIT' and ob.particle_systems.active.name == psys.name:
            self._is_edit_update = True
            if self._point_buf is None:
                self._is_cache_dirty = True
            return

        self._need_update_tf = False
        self.update_hair_cache(psys, subdiv, thickness_res, show_color)

    def draw_update_hair_cache(self, subdiv, thickness_res, show_color):
        if self._is_edit_update:
            if self._is_cache_dirty is False:
                self._is_cache_dirty = gl_utils.hair_cache_is_dirty(self._psys.as_pointer())
            if self._is_cache_dirty is False:
                self._is_edit_update = False
                return
            self.update_hair_cache(self._psys, subdiv, thickness_res, show_color)
            self._is_edit_update = False

    def draw(self, hair_shader, subdiv, thickness_res, show_color):
        if self._finals[subdiv] is None or self._point_len == 0 or self._strands_len == 0:
            return

        final = self._finals[subdiv]
        part = self._settings
        hair_shader.bind()
        hair_shader.uniform_texture("hairPointBuffer", final.tex)
        hair_shader.uniform_int("hairStrandsRes", final.strands_res)
        hair_shader.uniform_int("hairThicknessRes", thickness_res)
        hair_shader.uniform_float("hairRadScale", part.rad_scale)
        hair_shader.uniform_float("hairRadShape", part.shape)
        hair_shader.uniform_float("hairDupliMatrix", Matrix.Identity(4))
        hair_shader.uniform_float("hairRadRoot", part.root_rad)
        hair_shader.uniform_float("hairRadTip", part.tip_rad)
        hair_shader.uniform_bool("hairCloseTip", part.close_tip)
        hair_shader.uniform_bool("hairUseShapeCurve", part.use_shape_curve)
        hair_shader.uniform_bool("hairUseShapeRandom", part.use_shape_random)
        hair_shader.uniform_int("hairRandomSeed", part.random_seed)
        hair_shader.uniform_float("hairRadStart", part.radius_range_start)
        hair_shader.uniform_float("hairRandomThres", part.random_threshold)
        hair_shader.uniform_int("hairTotCurves", self._strands_len)
        if part.use_shape_curve:
            hair_shader.uniform_block("shapeCurveBlock", part.shape_curve_ubo)
        if part.use_shape_random:
            hair_shader.uniform_texture("hairFrandBuffer", get_hair_frand_texbuf())
        if show_color and self._strand_color_index_tex and self._color_group_ubo:
            hair_shader.uniform_bool("hairShowColor", True)
            hair_shader.uniform_block("colorGroupBlock", self._color_group_ubo)
            hair_shader.uniform_texture("hairStrandColorBuffer", self._strand_color_index_tex)
        else:
            hair_shader.uniform_texture("hairStrandColorBuffer", get_dummy_texbuf())
            hair_shader.uniform_bool("hairShowColor", False)
        final.hairs[thickness_res - 1].draw(hair_shader)


class ObjectDrawData():
    def __init__(self, obj, hair_refine_shader):
        self._hair_refine_shader = hair_refine_shader
        self.obj = obj
        self.mat_datas = [(0, 0)]
        self.valid_mat_len = 1
        self.hair_datas = {}

    def get_material_data(self, idx=0):
        if idx < 0 or idx >= len(self.mat_datas):
            return (0, 0)
        return self.mat_datas[idx]

    def get_valid_material_len(self):
        return self.valid_mat_len

    def update_data(self, scene: bpy.types.Scene, mat_data_mgr):
        if self.obj.mode not in ('OBJECT', 'PARTICLE_EDIT'):
            return

        self.mat_datas.clear()
        self.valid_mat_len = 0
        if self.obj.material_slots:
            for slot in self.obj.material_slots:
                if slot.material:
                    self.mat_datas.append(mat_data_mgr.add_material(slot.material))
                    self.valid_mat_len += 1
                else:
                    self.mat_datas.append((0, 0))
        else:
            self.mat_datas = [(0, 0)]
        subdiv = scene.render.hair_subdiv
        thickness_res = 1 if scene.render.hair_type == 'STRAND' else 2
        show_color = scene.hair_brush_3d.show_hair_color
        pre_keys = set(self.hair_datas.keys())
        cur_keys = set()
        for mod in self.obj.modifiers:
            if mod.type != 'PARTICLE_SYSTEM' or not mod.show_viewport:
                continue
            psys: bpy.types.ParticleSystem = mod.particle_system
            part = psys.settings
            if (part.type != 'HAIR' or len(psys.particles) == 0 or
                part.display_method != 'RENDER' or part.render_type != 'PATH'):
                continue

            cur_keys.add(psys.name)
            if (self.obj.mode == 'PARTICLE_EDIT' and self.obj.particle_systems.active.name == psys.name and
                    not scene.tool_settings.particle_edit.show_particles):
                continue

            if psys.name in self.hair_datas:
                self.hair_datas[psys.name].update_data(
                    self.obj, psys, subdiv, thickness_res, mat_data_mgr, show_color)
            else:
                hair_draw_data = HairDrawData(psys, self._hair_refine_shader)
                hair_draw_data.update_data(self.obj, psys, subdiv, thickness_res, mat_data_mgr, show_color)
                self.hair_datas[psys.name] = hair_draw_data

        diff = pre_keys.difference(cur_keys)
        for name in diff:
            del self.hair_datas[name]


class SceneDrawData:
    def __init__(self, context):
        self.region = context.region
        self.v3d: bpy.types.SpaceView3D = context.space_data
        self.rv3d = context.region_data
        self.scene = context.scene
        self.obj_datas = {}
        self.support_modes = ('OBJECT', 'PARTICLE_EDIT', 'PARTICLE')

        # self.init_frame_buf()
        self.init_uniform_block_buf()
        self.init_shaders()

        self.shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
        self.common_batch = batch_for_shader(self.shader, 'POINTS', {"pos": ((1, 1, 1),)})
        self.is_single_color = self.v3d.shading.color_type == 'SINGLE'

    def init_frame_buf(self):
        size = (self.region.width, self.region.height)
        self.depth_tex = gpu.types.GPUTexture(size, format='DEPTH24_STENCIL8')
        self.material_tex = gpu.types.GPUTexture(size, format='RGBA16F')
        self.normal_tex = gpu.types.GPUTexture(size, format='RG16F')
        self.frame_buf = gpu.types.GPUFrameBuffer(depth_slot=self.depth_tex,
            color_slots=(self.material_tex, self.normal_tex))

    def init_uniform_block_buf(self):
        # view_block
        self.view_block_buf = get_view_block_buf(self.rv3d.view_matrix, self.rv3d.window_matrix)
        self.view_block_ubo = gpu.types.GPUUniformBuf(self.view_block_buf)
        # worldData
        self.world_data_buf = get_world_data_buf(self.region, self.v3d, self.rv3d)
        self.world_block_ubo = gpu.types.GPUUniformBuf(self.world_data_buf)
        # material_data
        self.material_data_mgr = MaterialDataManager(self.v3d.shading)

    def get_shader_defines(self):
        defines = "#define WORKBENCH_ENCODE_NORMALS\n"
        shading = self.v3d.shading
        if shading.light == 'FLAT':
            defines += "#define V3D_LIGHTING_FLAT\n"
        # elif shading.light == 'MATCAP':
        #     defines += "#define V3D_LIGHTING_MATCAP\n"
        else:
            defines += "#define V3D_LIGHTING_STUDIO\n"
        return defines

    def init_shaders(self):
        defines = self.get_shader_defines()
        self.mesh_shader = gpu.types.GPUShader(
            vertexcode=shader_utils.get_shader_src(shader_utils.DataType.MESH, shader_utils.ShaderType.VERT),
            fragcode=shader_utils.get_shader_src(shader_utils.DataType.MESH, shader_utils.ShaderType.FRAG),
            defines=defines)
        self.hair_shader = shader_utils.GLShader(
            shader_utils.get_shader_src(shader_utils.DataType.HAIR, shader_utils.ShaderType.VERT),
            shader_utils.get_shader_src(shader_utils.DataType.HAIR, shader_utils.ShaderType.FRAG),
            defines=defines)
        self.hair_refine_shader = shader_utils.get_hair_refine_shader()
        # self.comp_shader = gpu.types.GPUShader(
        #   vertexcode=shader_utils.get_shader_src(shader_utils.DataType.COMP, shader_utils.ShaderType.VERT),
        #   fragcode=shader_utils.get_shader_src(shader_utils.DataType.COMP, shader_utils.ShaderType.FRAG),
        #   defines=defines)

    def update_uniform_block(self):
        self.view_block_ubo.update(get_view_block_buf(self.rv3d.view_matrix, self.rv3d.window_matrix))
        if self.v3d.shading.use_world_space_lighting:
            self.world_block_ubo.update(get_world_data_buf(self.region, self.v3d, self.rv3d))

    def update_common_shader_uniform(self, shader):
        shader.uniform_block("viewBlock", self.view_block_ubo)
        shader.uniform_block("world_block", self.world_block_ubo)

    def update_scene_data(self, context, depsgraph: bpy.types.Depsgraph):
        if self.obj_datas and len(depsgraph.updates) == 0 and context.object:
            act_obj = context.object.evaluated_get(depsgraph=depsgraph)
            if act_obj in self.obj_datas:
                self.obj_datas[act_obj].update_data(context.scene, self.material_data_mgr)
                return

        self.material_data_mgr.init()

        pre_keys = set(self.obj_datas.keys())
        cur_keys = set()
        for obj in depsgraph.objects:
            if obj.type != 'MESH':
                continue

            if obj in self.obj_datas:
                self.obj_datas[obj].update_data(context.scene, self.material_data_mgr)
            else:
                obj_data = ObjectDrawData(obj, self.hair_refine_shader)
                obj_data.update_data(context.scene, self.material_data_mgr)
                self.obj_datas[obj] = obj_data
            cur_keys.add(obj)

        diff = pre_keys.difference(cur_keys)
        for key in diff:
            del self.obj_datas[key]

        self.material_data_mgr.update()

    def draw(self, context, depsgraph):
        if (context.mode not in self.support_modes or
            self.v3d.show_object_viewport_mesh is False or not self.obj_datas):
            return

        scene = depsgraph.scene
        subdiv = scene.render.hair_subdiv
        hide_child = not scene.tool_settings.particle_edit.show_particles
        edit_show_hair = context.mode == 'PARTICLE' and not hide_child
        thickness_res = 1 if scene.render.hair_type == 'STRAND' else 2
        show_color = scene.hair_brush_3d.show_hair_color
        if edit_show_hair:
            for obj, obj_data in self.obj_datas.items():
                if obj.mode == 'PARTICLE_EDIT':
                    for name, hair_data in obj_data.hair_datas.items():
                        if obj.particle_systems.active.name == name:
                            hair_data.draw_update_hair_cache(subdiv, thickness_res, show_color)
                            break
                    break

        self.update_uniform_block()

        bgl.glEnable(bgl.GL_DEPTH_TEST)
        bgl.glEnable(bgl.GL_LINE_SMOOTH)
        bgl.glDepthFunc(bgl.GL_LEQUAL)

        self.mesh_shader.bind()
        self.update_common_shader_uniform(self.mesh_shader)
        ori_batch = gl_utils.get_gpu_batch(self.common_batch)
        for obj, obj_data in self.obj_datas.items():
            if obj.mode not in ('OBJECT', 'PARTICLE_EDIT'):
                continue

            if obj.visible_in_viewport_get(self.v3d) and obj.show_instancer_for_viewport:
                self.mesh_shader.uniform_float("ModelMatrix", obj.matrix_world)
                if self.is_single_color or obj_data.get_valid_material_len() == 0:
                    self.material_data_mgr.bind_material_data(self.mesh_shader, obj_data.get_material_data())
                    if gl_utils.set_mesh_surface_gpu_batch(self.common_batch, obj.data.as_pointer()):
                        self.common_batch.draw(self.mesh_shader)
                else:
                    for i, mat_data in enumerate(obj_data.mat_datas):
                        self.material_data_mgr.bind_material_data(self.mesh_shader, mat_data)
                        if gl_utils.set_mesh_surface_material_gpu_batch(self.common_batch, obj.data.as_pointer(), i):
                            self.common_batch.draw(self.mesh_shader)

        gl_utils.restore_gpu_batch(self.common_batch, ori_batch)

        self.hair_shader.bind()
        self.update_common_shader_uniform(self.hair_shader)
        for obj, obj_data in self.obj_datas.items():
            if not obj_data.hair_datas or not obj.visible_in_viewport_get(self.v3d):
                continue

            self.hair_shader.uniform_float("ModelMatrix", obj.matrix_world)
            for name, hair_data in obj_data.hair_datas.items():
                if obj.mode == 'PARTICLE_EDIT' and obj.particle_systems.active.name == name and hide_child:
                    continue

                self.material_data_mgr.bind_material_data(self.hair_shader, hair_data.get_material_data())
                hair_data.draw(self.hair_shader, subdiv, thickness_res, show_color)

        bgl.glDisable(bgl.GL_LINE_SMOOTH)
        bgl.glDisable(bgl.GL_DEPTH_TEST)

    def __del__(self):
        for obj in self.obj_datas:
            if obj.visible_in_viewport_get(self.v3d):
                gl_utils.clearup_mesh_surface_gpu_batch(obj.data.as_pointer())
        global G_points_batch, G_hair_frand_texbuf, G_hair_frand_vbuf, G_dummy_texbuf, G_dummy_vbuf
        G_points_batch = None
        G_hair_frand_texbuf = None
        G_hair_frand_vbuf = None
        G_dummy_texbuf = None
        G_dummy_vbuf = None
