#
# Copyright 2011-2013 Blender Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import time
import bpy
from bpy.props import (
    BoolProperty,
    FloatProperty,
    IntProperty,
    PointerProperty,
    EnumProperty,
    StringProperty,
)
import bgl
import gpu
import typing
import nodeitems_utils
import nodeitems_builtins
import cycles.engine as cycles_engine

try:
    from .draw_engine.draw_data import SceneDrawData
    from .draw_engine.gl_utils import gl_utils
except SystemError:
    SceneDrawData = None
    gl_utils = None


class Suffix:
    @classmethod
    def del_numeral_str(cls, name):
        number_str = '0123456789'
        for s in number_str:
            if name[-1] == s:
                del_location = name.find('.', -1)
                if cls.exist_find(del_location):
                    return name[0:del_location]
        return name

    @staticmethod
    def numeral(index, base_number=2):
        index_str = str(index)

        for i in range(1, base_number + 1):
            if len(index_str) == i:
                return '.' + '0' * (base_number - i) + index_str
        return "." + index_str

    @classmethod
    def collision_name(cls, collision, fixation_name: str, use_name="", base_number=2):
        if collision.get(use_name) or not use_name:
            fixation_name = cls.del_numeral_str(fixation_name)
            name_str = fixation_name
            name_index = 1
            while collision.get(name_str):
                name_str = fixation_name + cls.numeral(name_index, base_number)
                name_index += 1
            return name_str
        return use_name

    @staticmethod
    def exist_find(find_value):
        return find_value >= 0

    @staticmethod
    def abspath(path,
                start: typing.Union[bytes, str] = None,
                library: 'bpy.types.Library' = None):
        return bpy.path.abspath(path, start, library)

    @staticmethod
    def clean_name(name, replace="_"):
        return bpy.path.clean_name(name, replace)

    @staticmethod
    def display_name(*args, **kwargs):
        return bpy.path.display_name(*args, **kwargs)


class CyclesHair_OT_shape_curve_preset(bpy.types.Operator):
    bl_idname = "cycles_hair.shape_curve_preset"
    bl_label = "Radius Curve Preset"
    bl_options = {"INTERNAL"}

    @classmethod
    def poll(cls, context):
        psys = context.object.particle_systems.active

        return psys and psys.settings.cycles.use_shape_curve

    def invoke(self, context, event):
        cycles = context.object.particle_systems.active.settings.cycles
        if 'shape_curve_presets_' not in cycles:
            cycles['shape_curve_presets_'] = {'Default': [(0, 1), (1, 1)]}

        wm = context.window_manager
        return wm.invoke_popup(self, width=250)

    def draw(self, context):
        layout = self.layout
        cycles = context.object.particle_systems.active.settings.cycles
        presets = dict(cycles['shape_curve_presets_'])
        for name in presets:
            row = layout.row()
            row.operator(CyclesHair_OT_shape_curve_preset_apply.bl_idname, text=name).name = name
            row.operator(CyclesHair_OT_shape_curve_preset_remove.bl_idname, text="", icon="REMOVE").name = name

        row = layout.row()
        row.prop(cycles, "preset_name")
        row.operator(CyclesHair_OT_shape_curve_preset_new.bl_idname, text="", icon='ADD')

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


class CyclesHair_OT_shape_curve_preset_apply(bpy.types.Operator):
    bl_idname = "cycles_hair.shape_curve_preset_apply"
    bl_label = "Apply"
    bl_description = "Apply current preset"
    bl_options = {"INTERNAL"}

    name: StringProperty(name="", default="")

    def execute(self, context):
        cycles = context.object.particle_systems.active.settings.cycles
        presets = cycles['shape_curve_presets_']
        if self.name in presets:
            self.apply_preset(context, cycles, list(presets[self.name]))
        return {"FINISHED"}

    def apply_preset(self, context, cycles, preset_points):
        if not gl_utils:
            return

        mapping = cycles.shape_curve.nodes[0].mapping
        shape_curve_cur = -1
        shape_curve_buffer = gpu.types.Buffer('FLOAT', 260)

        _, shape_curve_cur, _ = gl_utils.update_curve_mapping_table(
            mapping.as_pointer(), shape_curve_buffer, shape_curve_cur, 0)

        points = mapping.curves[shape_curve_cur].points
        while len(points) > 2:
            points.remove(points[-1])

        for i, p in enumerate(preset_points):
            if i >= 2:
                points.new(p[0], p[1])
            else:
                points[i].location = p

        mapping.update()
        context.scene.update_tag()
        for area in context.screen.areas:
            if area.type == "PROPERTIES":
                area.tag_redraw()


class CyclesHair_OT_shape_curve_preset_new(bpy.types.Operator):
    bl_idname = "cycles_hair.shape_curve_preset_new"
    bl_label = "New"
    bl_description = "Create a new preset"
    bl_options = {"INTERNAL"}

    def execute(self, context):
        cycles = context.object.particle_systems.active.settings.cycles
        name = cycles.preset_name.strip()
        if name and gl_utils:
            self.save_preset(cycles, name)

        return {"FINISHED"}

    def save_preset(self, cycles, name):
        presets = dict(cycles['shape_curve_presets_'])
        preset = []
        mapping = cycles.shape_curve.nodes[0].mapping

        shape_curve_cur = -1
        shape_curve_buffer = gpu.types.Buffer('FLOAT', 260)
        _, shape_curve_cur, _ = gl_utils.update_curve_mapping_table(
            mapping.as_pointer(), shape_curve_buffer, shape_curve_cur, 0)

        points = mapping.curves[shape_curve_cur].points
        for point in points:
            preset.append((point.location.x, point.location.y))

        if name in presets:
            name = Suffix.collision_name(presets, name, name, 3)
        presets[name] = preset
        cycles['shape_curve_presets_'] = presets


class CyclesHair_OT_shape_curve_preset_remove(bpy.types.Operator):
    bl_idname = "cycles_hair.shape_curve_preset_remove"
    bl_label = "Remove"
    bl_description = "Remove the current preset"
    bl_options = {"INTERNAL"}

    name: StringProperty(name="", default="")

    def execute(self, context):
        cycles = context.object.particle_systems.active.settings.cycles
        presets = dict(cycles['shape_curve_presets_'])
        if self.name != "Default" and self.name in presets:
            presets.pop(self.name)
            cycles['shape_curve_presets_'] = presets

        return {"FINISHED"}


class CyclesHairRenderEngine(bpy.types.RenderEngine):
    bl_idname = 'CYCLESHAIR'
    bl_label = "CyclesHair"
    bl_use_eevee_viewport = False
    bl_use_preview = True
    bl_use_exclude_layers = True
    bl_use_save_buffers = True
    bl_use_spherical_stereo = True
    bl_use_custom_freestyle = True
    bl_use_shading_nodes_custom = False
    bl_use_alembic_procedural = True

    def __init__(self):
        self.session = None
        self.hair_shape_hook = None
        self.draw_data = None

    def __del__(self):
        cycles_engine.free(self)
        if hasattr(self, "viewport_session"):
            self.session = self.viewport_session
            cycles_engine.free(self)
        if hasattr(self, "hair_shape_hook") and self.hair_shape_hook:
            from .cycles_hook import cycles_hook
            cycles_hook.free_hair_shape_hook(self.hair_shape_hook)

    def get_view3d(self, screen):
        v3darea = None
        v3d = None
        region = None
        rv3d = None
        for a in screen.areas:
            if a.type == 'VIEW_3D':
                v3darea = a
                break
        if v3darea is None:
            for s in bpy.data.screens:
                for a in s.areas:
                    if a.type == 'VIEW_3D':
                        v3darea = a
                        break
                if v3darea:
                    break
        if v3darea:
            v3d = v3darea.spaces[0]
            rv3d = v3d.region_3d
            for r in a.regions:
                if r.type == 'WINDOW':
                    region = r
                    break
        return region, v3d, rv3d

    def sync(self, depsgraph, data):
        from .cycles_hook import cycles_hook
        cycles_engine.sync(self, depsgraph, data)
        if cycles_hook:
            if self.hair_shape_hook is None:
                self.hair_shape_hook = cycles_hook.create_hair_shape_hook()
            if cycles_hook.modifiy_hair_shape(self.session, self.hair_shape_hook, len(depsgraph.updates)):
                if depsgraph.mode != 'RENDER':
                    self.tag_redraw()
        else:
            print("CyclesHairRender not support this blender version!")

    # final render
    def update(self, data, depsgraph):
        from .cycles_hook import cycles_hook
        if not self.session:
            if self.is_preview:
                cscene = bpy.context.scene.cycles
                use_osl = cscene.shading_system and cscene.device == 'CPU'

                cycles_engine.create(self, data, preview_osl=use_osl)
            else:
                if cycles_hook:
                    region, v3d, rv3d = self.get_view3d(bpy.context.window_manager.windows[0].screen)
                    if v3d:
                        cycles_engine.create(self, data, region, v3d, rv3d)
                        self.viewport_session = self.session
                        cycles_hook.switch_session_to_render_mode(self.session)
                        cycles_engine.reset(self, data, depsgraph)
                        cycles_hook.increase_session_reset_time(self.session)
                        self.sync(depsgraph, data)
                        cycles_engine.reset(self, data, depsgraph)
                    else:
                        self.report({'ERROR'}, "Enable a 3D view when rendering.")
                cycles_engine.create(self, data)

        cycles_engine.reset(self, data, depsgraph)
        if not self.is_preview and hasattr(self, "viewport_session"):
            while not cycles_hook.is_all_hair_updated(self.viewport_session):
                time.sleep(0.1)
            cycles_hook.cancel_session(self.viewport_session)
            cycles_hook.swap_session_data(self.session, self.viewport_session)
            # engine.reset(self, data, depsgraph)

    def render(self, depsgraph):
        cycles_engine.render(self, depsgraph)

    def render_frame_finish(self):
        cycles_engine.render_frame_finish(self)

    def draw(self, context, depsgraph):
        cycles_engine.draw(self, depsgraph, context.space_data)

    # viewport render
    def view_update(self, context, depsgraph):
        v3d = context.space_data
        if v3d and v3d.shading.type == 'MATERIAL' and SceneDrawData:
            if self.draw_data is None:
                self.draw_data = SceneDrawData(context)
            self.draw_data.update_scene_data(context, depsgraph)
            self.tag_redraw()
        else:
            if not self.session:
                cycles_engine.create(self, context.blend_data, context.region, context.space_data, context.region_data)

            cycles_engine.reset(self, context.blend_data, depsgraph)
            self.sync(depsgraph, context.blend_data)

    def view_draw(self, context, depsgraph):
        v3d = context.space_data
        if v3d and v3d.shading.type == 'MATERIAL' and SceneDrawData:
            if self.draw_data is None:
                return

            # Bind shader that converts from scene linear to display space,
            bgl.glEnable(bgl.GL_BLEND)
            self.bind_display_space_shader(depsgraph.scene)
            self.draw_data.draw(context, depsgraph)
            self.unbind_display_space_shader()
            bgl.glDisable(bgl.GL_BLEND)
        else:
            if bpy.app.version >= (3, 0):
                cycles_engine.view_draw(self, depsgraph, context.region, context.space_data, context.region_data)
            else:
                cycles_engine.draw(self, depsgraph, context.region, context.space_data, context.region_data)

    def update_script_node(self, node):
        if cycles_engine.with_osl():
            from cycles import osl
            osl.update_script_node(node, self.report)
        else:
            self.report({'ERROR'}, "OSL support disabled in this build.")

    def update_render_passes(self, scene, srl):
        cycles_engine.register_passes(self, scene, srl)

    def bake(self, depsgraph, obj, pass_type, pass_filter, width, height):
        cycles_engine.bake(self, depsgraph, obj, pass_type, pass_filter, width, height)


class CloseOverlayOptions(bpy.types.Operator):
    bl_idname = "view3d.close_overlay_options"
    bl_label = "Hide Viewport Overlays"
    bl_options = {"REGISTER", "UNDO"}

    @staticmethod
    def toggle_options(context, value):
        options = ('show_axis_x', 'show_axis_y', 'show_floor', 'show_cursor',
                   'show_annotation', 'show_relationship_lines', 'show_outline_selected',
                   'show_bones', 'show_motion_paths', 'show_object_origins', 'show_extras',
                   'show_ortho_grid')
        context.space_data.overlay.show_overlays = True
        for opt in options:
            context.space_data.overlay.__setattr__(opt, value)

    def execute(self, context):
        self.toggle_options(context, False)
        context.space_data.overlay.show_axis_z = False
        context.space_data.overlay.show_object_origins_all = False
        return {'FINISHED'}


class OpenOverlayOptions(bpy.types.Operator):
    bl_idname = "view3d.open_overlay_options"
    bl_label = "Show Viewport Overlays"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        CloseOverlayOptions.toggle_options(context, True)
        return {'FINISHED'}


def deal_overlay_func(self, context):
    row = self.layout.row(align=True)
    row.operator(CloseOverlayOptions.bl_idname, icon='PROP_CON', text='')
    row.operator(OpenOverlayOptions.bl_idname, icon='PROP_ON', text='')


def use_shape_curve_updated(self, context):
    if self.shape_curve is None and self.use_shape_curve:
        ntree = bpy.data.node_groups.new(name=self.id_data.name, type='TextureNodeTree')
        curve = ntree.nodes.new(type='TextureNodeCurveRGB')
        curve.name = "hair_shape_curve"
        for c in curve.mapping.curves:
            if len(c.points) > 0:
                c.points[0].location[1] = 1.0
        self.shape_curve = ntree
    if not self.use_shape_curve:
        self.id_data.use_advanced_hair = self.id_data.use_advanced_hair


class CyclesHairSettings(bpy.types.PropertyGroup):
    def use_shape_random_updated(self, context):
        if not self.use_shape_random:
            self.id_data.use_advanced_hair = self.id_data.use_advanced_hair

    preset_name: StringProperty(name="", default="New Preset", options={'SKIP_SAVE'}, description="Preset Name")

    use_shape_curve: BoolProperty(
        name="Use Radius Curve",
        description="Use curve to control radius of the hair",
        default=False,
        options=set(),
        update=use_shape_curve_updated
    )

    use_shape_random: BoolProperty(
        name="Use Diameter Random",
        description="Randomize radius of the hair",
        default=False,
        update=use_shape_random_updated
    )

    shape_curve: PointerProperty(
        name="Shape Curve",
        type=bpy.types.NodeTree,
    )

    random_seed: IntProperty(
        name="Random Seed",
        min=0
    )

    radius_range_start: FloatProperty(
        name="Start",
        description="The start of random range",
        min=0.0,
        max=1.0,
        subtype='FACTOR'
    )

    random_threshold: FloatProperty(
        name="Threshold",
        description="Threshold of random thickness",
        min=-1.0,
        max=1.0,
        subtype='FACTOR'
    )

    @classmethod
    def register(cls):
        bpy.types.ParticleSettings.cycles = PointerProperty(
            name="Cycles Hair Settings",
            description="Cycles Hair settings",
            type=cls,
        )

    @classmethod
    def unregister(cls):
        del bpy.types.ParticleSettings.cycles


def engine_exit():
    cycles_engine.exit()


def object_shader_nodes_poll(context):
    snode = context.space_data
    return (snode.tree_type == 'ShaderNodeTree' and
            snode.shader_type == 'OBJECT')


def cycles_shader_nodes_poll(context):
    return context.engine in ('CYCLES', 'CYCLESHAIR')


def eevee_shader_nodes_poll(context):
    return context.engine == 'BLENDER_EEVEE'


def eevee_cycles_shader_nodes_poll(context):
    return (cycles_shader_nodes_poll(context) or
            eevee_shader_nodes_poll(context))


def object_cycles_shader_nodes_poll(context):
    return (object_shader_nodes_poll(context) and
            cycles_shader_nodes_poll(context))


def object_eevee_cycles_shader_nodes_poll(context):
    return (object_shader_nodes_poll(context) and
            eevee_cycles_shader_nodes_poll(context))


def patch_builtin_nodeitems_poll():
    if "SHADER" not in nodeitems_utils._node_categories:
        return

    shader_categories = nodeitems_utils._node_categories['SHADER']
    for cat in shader_categories[0]:
        for item in cat.items(None):
            if item.poll is None:
                continue

            if item.poll == nodeitems_builtins.eevee_cycles_shader_nodes_poll:
                item.poll = eevee_cycles_shader_nodes_poll
            elif item.poll == nodeitems_builtins.object_cycles_shader_nodes_poll:
                item.poll = object_cycles_shader_nodes_poll
            elif item.poll == nodeitems_builtins.object_eevee_cycles_shader_nodes_poll:
                item.poll = object_eevee_cycles_shader_nodes_poll


def restore_builtin_nodeitems_poll():
    if "SHADER" not in nodeitems_utils._node_categories:
        return

    shader_categories = nodeitems_utils._node_categories['SHADER']
    for cat in shader_categories[0]:
        for item in cat.items(None):
            if item.poll is None:
                continue

            if item.poll == eevee_cycles_shader_nodes_poll:
                item.poll = nodeitems_builtins.eevee_cycles_shader_nodes_poll
            elif item.poll == object_cycles_shader_nodes_poll:
                item.poll = nodeitems_builtins.object_cycles_shader_nodes_poll
            elif item.poll == object_eevee_cycles_shader_nodes_poll:
                item.poll = nodeitems_builtins.object_eevee_cycles_shader_nodes_poll


classes = [CyclesHair_OT_shape_curve_preset, CyclesHair_OT_shape_curve_preset_new,
           CyclesHair_OT_shape_curve_preset_remove, CyclesHair_OT_shape_curve_preset_apply,
           CyclesHairSettings, CloseOverlayOptions, OpenOverlayOptions, CyclesHairRenderEngine]


def register():
    from bpy.utils import register_class
    from bpy.types import VIEW3D_HT_header
    from . import ui
    import atexit

    # Make sure we only registered the callback once.
    atexit.unregister(engine_exit)
    atexit.register(engine_exit)
    cycles_engine.init()

    ui.register()
    for cls in classes:
        register_class(cls)
    patch_builtin_nodeitems_poll()
    VIEW3D_HT_header.append(deal_overlay_func)


def unregister():
    from bpy.utils import unregister_class
    from bpy.types import VIEW3D_HT_header
    from . import ui
    VIEW3D_HT_header.remove(deal_overlay_func)
    restore_builtin_nodeitems_poll()
    ui.unregister()
    for cls in classes:
        unregister_class(cls)
