# Copyright (C) 2022 Thomas Hoppe (h0bB1T). All rights reserved.
#
# Unauthorized copying of this file via any medium is strictly prohibited.
# Proprietary and confidential.

import bpy

from typing import List, Tuple, Union

from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty, IntProperty
from bpy.props import PointerProperty, StringProperty, FloatProperty
from bpy.types import PropertyGroup, WindowManager

from .operators.helpers.setup_grunge import setup_grunge
from .operators.helpers.setup_pbr import setup_pbr
from .operators.helpers.setup_image_texture import setup_image_texture
from .utils.node_builder_cycles import NodeBuilderCycles
from .utils.blender import is_ls_410
from .registries.textures.texture_entry import TextureEntry
from .registries.texture_registry import RepositoryType, TextureRegistry
from .registries.enum_registry import EnumRegistry
from .constants import uv_mappings, image_interpolations
from .snw.node_info import update_anti_repeat_value, update_blend_value


class MappingProperties(PropertyGroup):
    # These are used for update information.
    init: BoolProperty() # type: ignore
    current_tree: StringProperty() # type: ignore

    categories: EnumProperty(
        name='', 
        description='Category',
        items=lambda self, __: self.get_categories(),
        update=lambda self, _: self.select_category()
    ) # type: ignore

    previews: EnumProperty(
        name='',
        description='Texture',
        items=lambda self, _: self.get_previews(),
        update=lambda self, _: self.rebuild('SET')
    ) # type: ignore

    mapping: EnumProperty(
        name='UV Mapping',
        description='How this image is mapped to the Geometry',
        items=uv_mappings,
        update=lambda self, _: self.rebuild('MAPPING')
    ) # type: ignore

    blend: FloatProperty(
        name='Box Blend',
        default=0.5,
        min=0.0,
        max=1.0,
        update=lambda self, _: self.update_blend()
    ) # type: ignore

    interpolation: EnumProperty(
        name='Image Interpolation',
        description='Image Interpolation used for the Textures',
        items=image_interpolations,
        update=lambda self, _: self.rebuild('INTERPOLATION')
    ) # type: ignore

    bevel_samples: IntProperty(
        name='Number of Bevel Samples (0=Off)',
        description='Enable/Disable Bevel and Number of Samples (0=Disabled)',
        min=0,
        max=16,
        update=lambda self, _: self.rebuild('BEV')
    ) # type: ignore

    anti_repeat_style: EnumProperty(
        name='Anti-Repeat Style',
        default='NOISE',
        items=[
            ( 'OFF', 'Off', 'Disabled', 'RADIOBUT_OFF', 0),
            ( 'NOISE', 'Noise', 'Noise Based', 'MOD_NOISE', 1 ),
            ( 'VORONOI', 'Voronoi', 'Voronoi Based', 'LIGHTPROBE_PLANAR' if is_ls_410() else 'LIGHTPROBE_PLANE', 2 ),
        ],
        update=lambda self, _: self.rebuild('ARS')
    ) # type: ignore

    anti_repeat_scale: FloatProperty(
        name='Scale', 
        default=2, 
        min=0.5, 
        max=20, 
        update=lambda self, _: self.update_ar()
    ) # type: ignore
    anti_repeat_distortion: FloatProperty(
        name='Distortion', 
        default=0.1, 
        min=0, 
        max=1, 
        update=lambda self, _: self.update_ar()
    ) # type: ignore
    anti_repeat_style_param_0: FloatProperty(
        name='Style-Param', 
        default=0.1, 
        soft_min=0, 
        soft_max=1, 
        update=lambda self, _: self.update_ar()
    ) # type: ignore
    anti_repeat_seed: FloatProperty(
        name='Seed', 
        default=0, 
        min=0, 
        max=1, 
        update=lambda self, _: self.update_ar()
    ) # type: ignore


class BaseMappingProperties(MappingProperties, PropertyGroup):
    def get_categories(self) -> List[Tuple]:
        return TextureRegistry.instance().get_category_enums(self.type())


    def get_previews(self) -> List[Tuple]:
        return TextureRegistry.instance().get_image_enums(self.type(), self.categories)


    def select_category(self):
        if TextureRegistry.instance().get_image_enums(self.type(), self.categories):
            self.previews = TextureRegistry.instance().get_image_enums(self.type(), self.categories)[0][0]


    def texture_entry(self) -> Union[TextureEntry, None]:
        return TextureRegistry.instance().get_image(self.type(), self.categories, self.previews)


    def update_blend(self):
        nb = NodeBuilderCycles.edit_tree_by_name(self.current_tree, False)
        if nb:
            nb.update_projection_blend(self.blend)
            nb.tree.snw_node_info = update_blend_value(nb.tree.snw_node_info, self.blend)


    def update_ar(self):
        nb = NodeBuilderCycles.edit_tree_by_name(self.current_tree, False)
        if nb:
            nb.update_uv_anti_repeat_settings(
                self.anti_repeat_scale,
                self.anti_repeat_distortion,
                self.anti_repeat_style_param_0,
                self.anti_repeat_seed
            )
            nb.tree.snw_node_info = update_anti_repeat_value(
                nb.tree.snw_node_info,
                self.anti_repeat_scale,
                self.anti_repeat_distortion,
                self.anti_repeat_style_param_0,
                self.anti_repeat_seed
            )


class PBRMappingProperties(BaseMappingProperties, PropertyGroup):
    def type(self):
        return RepositoryType.PBR


    variants: EnumProperty(
        name='Variants',
        description='Variants of this Set',
        items=lambda self, _: self.get_variants(),
        update=lambda self, _: self.rebuild('VARIANT')
    ) # type: ignore


    depth_mode: EnumProperty(
        name='Depth Mode',
        description='Mode for material depth',
        items=(
            ( 'NONE', 'None', 'No depth' ),
            ( 'PARALLAX', 'Parallax Basic', 'Very cheap parallax mapping' ),
            ( 'POM', 'Parallax Occlusion', 'Parallax occlusion mapping' ),
            ( 'DISPLACE', 'Displacement mapping', 'Real displacement mapping' ),
        ),
        update=lambda self, _: self.rebuild('DEPTH MODE'),
        default='NONE'
    ) # type: ignore


    pom_levels: EnumProperty(
        name='POM Levels',
        description='Number of iterations',
        items=(
            ( '4', '4', '4 Levels, lowest quality' ),
            ( '8', '8', '8 Levels, low quality' ),
            ( '16', '16', '16 Levels, medium quality' ),
            ( '32', '32', '32 Levels, good quality' ),
            ( '64', '64', '64 Levels, high quality' ),
        ),
        update=lambda self, _: self.rebuild('POM LEVELS'),
        default='8'
    ) # type: ignore

    pom_fine_levels: EnumProperty(
        name='POM Fine Levels',
        description='Number of fine iterations',
        items=(
            ( '0', '0', 'Off, no fine levels' ),
            ( '4', '4', '4 Levels, medium quality fine levels' ),
            ( '8', '8', '8 Levels, max quality fine levels' ),
        ),
        update=lambda self, _: self.rebuild('POM FINE LEVELS'),
        default='4'
    ) # type: ignore

    invert_normal: BoolProperty(
        name='Invert Normal (OpenGL <-> DirectX)',
        update=lambda self, _: self.rebuild('NOR_INV'),
        default=False
    ) # type: ignore


    def get_variants(self) -> List[Tuple]:
        entry = self.texture_entry()
        if entry:
            return [ (e, e, e) for i, e in enumerate(entry.variants())]
        return []


    def rebuild(self, mode: str):
        #print(f'Change {mode} in {self.current_tree}')
        if not self.init and self.current_tree:
            entry = self.texture_entry()
            if entry:
                variant_id = (entry.variants().index(self.variants)) if len(self.get_variants()) > 1 else 0
                bin_hash = entry.get_bin_hash(variant_id)

                nb = NodeBuilderCycles.edit_tree_by_name(self.current_tree, True)
                if nb:
                    setup_pbr(
                        nb,
                        self.categories,
                        self.previews,
                        variant_id,
                        bin_hash,
                        self.mapping,
                        self.blend,
                        self.interpolation,
                        self.anti_repeat_style,
                        self.anti_repeat_scale,
                        self.anti_repeat_distortion,
                        self.anti_repeat_style_param_0,
                        self.anti_repeat_seed,
                        self.depth_mode,
                        int(self.pom_levels),
                        int(self.pom_fine_levels),
                        self.bevel_samples,
                        self.invert_normal,
                    )

                    # Tree is renamed setup_pbr()
                    self.current_tree = nb.tree.name


class ImageMappingProperties(BaseMappingProperties, PropertyGroup):
    def type(self):
        return RepositoryType.Image


    def rebuild(self, mode: str):
        #print(f'Change {mode} in {self.current_tree}')
        if not self.init and self.current_tree:
            entry = self.texture_entry()
            if entry:
                nb = NodeBuilderCycles.edit_tree_by_name(self.current_tree, True)
                if nb:
                    setup_image_texture(
                        nb,
                        self.categories,
                        self.previews,
                        entry.get_bin_hash(),
                        self.mapping,
                        self.blend,
                        self.interpolation,
                        self.anti_repeat_style,
                        self.anti_repeat_scale,
                        self.anti_repeat_distortion,
                        self.anti_repeat_style_param_0,
                        self.anti_repeat_seed,
                    )

                    # Tree is renamed setup_grunge()
                    self.current_tree = nb.tree.name


class GrungeMappingProperties(BaseMappingProperties, PropertyGroup):
    def type(self):
        return RepositoryType.Gray


    def rebuild(self, mode: str):
        #print(f'Change {mode} in {self.current_tree}')
        if not self.init and self.current_tree:
            entry = self.texture_entry()
            if entry:
                nb = NodeBuilderCycles.edit_tree_by_name(self.current_tree, True)
                if nb:
                    setup_grunge(
                        nb,
                        self.categories,
                        self.previews,
                        entry.get_bin_hash(),
                        self.mapping,
                        self.blend,
                        self.interpolation,
                        self.anti_repeat_style,
                        self.anti_repeat_scale,
                        self.anti_repeat_distortion,
                        self.anti_repeat_style_param_0,
                        self.anti_repeat_seed,
                        self.bevel_samples,
                    )

                    # Tree is renamed setup_grunge()
                    self.current_tree = nb.tree.name


class BrushMappingProperties(BaseMappingProperties, PropertyGroup):
    def type(self):
        return RepositoryType.Brush


    def rebuild(self, _): pass


class Properties(PropertyGroup):
    show_material_replacer: BoolProperty(name='', description='Toggle Material Replacer', default=False) # type: ignore
    source_material_back: EnumProperty(
        name='Source Material',
        description='The Material to Replace with the one on the right',
        items=lambda _, __: [ EnumRegistry.replace_for_enum3(m.name_full, m.name_full, m.name) for m in bpy.data.materials ]
    ) # type: ignore

    @property
    def source_material(self, string):
        self.source_material_back = EnumRegistry.id_for_str(string)

    @property
    def source_material(self) -> str:
        return EnumRegistry.str_for_id(self.source_material_back)   


    replacement_material_back: EnumProperty(
        name='Replace Material',
        description='The Material to Replace to the one on the left',
        items=lambda _, __: [ EnumRegistry.replace_for_enum3(m.name_full, m.name_full, m.name) for m in bpy.data.materials ]
    ) # type: ignore

    @property
    def replacement_material(self, string):
        self.replacement_material_back = EnumRegistry.id_for_str(string)

    @property
    def replacement_material(self) -> str:
        return EnumRegistry.str_for_id(self.replacement_material_back)       

    # -------------------------

    pbr: PointerProperty(type=PBRMappingProperties) # type: ignore
    pbr_edit_0: PointerProperty(type=PBRMappingProperties) # type: ignore
    pbr_edit_1: PointerProperty(type=PBRMappingProperties) # type: ignore
    pbr_edit_2: PointerProperty(type=PBRMappingProperties) # type: ignore
    pbr_edit_3: PointerProperty(type=PBRMappingProperties) # type: ignore
    pbr_manage: PointerProperty(type=PBRMappingProperties) # type: ignore

    def pbr_edit(self, index: int) -> PBRMappingProperties:
        return [
            self.pbr_edit_0,
            self.pbr_edit_1,
            self.pbr_edit_2,
            self.pbr_edit_3,
        ][index]
    

    image: PointerProperty(type=ImageMappingProperties) # type: ignore
    image_edit_0: PointerProperty(type=ImageMappingProperties) # type: ignore
    image_edit_1: PointerProperty(type=ImageMappingProperties) # type: ignore
    image_edit_2: PointerProperty(type=ImageMappingProperties) # type: ignore
    image_edit_3: PointerProperty(type=ImageMappingProperties) # type: ignore
    image_manage: PointerProperty(type=ImageMappingProperties) # type: ignore

    def image_edit(self, index: int) -> ImageMappingProperties:
        return [
            self.image_edit_0,
            self.image_edit_1,
            self.image_edit_2,
            self.image_edit_3,
        ][index]


    grunge: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_0: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_1: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_2: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_3: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_4: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_5: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_6: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_edit_7: PointerProperty(type=GrungeMappingProperties) # type: ignore
    grunge_manage: PointerProperty(type=GrungeMappingProperties) # type: ignore

    brush: PointerProperty(type=BrushMappingProperties) # type: ignore
    brush_manage: PointerProperty(type=BrushMappingProperties) # type: ignore


    def grunge_edit(self, index: int) -> GrungeMappingProperties:
        return [
            self.grunge_edit_0,
            self.grunge_edit_1,
            self.grunge_edit_2,
            self.grunge_edit_3,
            self.grunge_edit_4,
            self.grunge_edit_5,
            self.grunge_edit_6,
            self.grunge_edit_7,
        ][index]

    # -------------------------

    collection_group: StringProperty() # type: ignore

    # -------------------------

    vertex_show_header_controls: BoolProperty(name='SNW: Toggle extended controls') # type: ignore
    vertex_select: BoolProperty(name='Selected Faces Only', description='Change colors for selected faces only', default=True) # type: ignore
    vertex_color: FloatVectorProperty(name='Color', subtype='COLOR', size=4, min=0.0, max=1.0, default=(1, 1, 1, 1)) # type: ignore
    vertex_seed: IntProperty(name='Seed') # type: ignore
    vertex_color_set: EnumProperty(
        name='Vertex Color Set',
        description='Vertex Color Set to adjust',
        items=lambda _, __: [ ( n.name, n.name, n.name) for n in bpy.context.active_object.data.vertex_colors ]
    ) # type: ignore

    # -------------------------

    bake_size: EnumProperty(name='Output size', description='',
        items=(
            ( '256', '256', '256x256' ),
            ( '512', '512', '512x512' ),
            ( '1024', '1024', '1024x1024' ),
            ( '2048', '2048', '2048x2048' ),
            ( '4096', '4096', '4096x4096' ),
        ),
        default='512'
    ) # type: ignore
    bake_uv_gen_margin_auto: BoolProperty(name='Auto UV Margin', description='Calc Margin from current resolution and bake margin', default=True) # type: ignore
    bake_uv_gen_margin: FloatProperty(name='UV Margin', description='Margin to use for quick UV unwrap', min=0, max=0.1, default=0.01) # type: ignore
    bake_ao_samples: IntProperty(name='AO Samples', description='Number of samples for AO (1-128)', min=1, max=128, default=16)  # type: ignore
    bake_ao_distance: FloatProperty(name='AO Distance', description='Geometrical AO distance in world space', min=0.0001, default=0.1) # type: ignore
    bake_bevel_samples: IntProperty(name='Bevel Samples', description='Number of samples for Edge Bevel', min=1, max=16, default=4) # type: ignore
    bake_bevel_distance: FloatProperty(name='Bevel Distance', description='Geometrical Bevel distance in world space', min=0.0001, default=0.02) # type: ignore

    bake_cycles_samples: IntProperty(name='Cycles Samples', description='Render samples', min=1, default=16) # type: ignore
    bake_cycles_denoise: BoolProperty(name='Bake Denoise', description='Enable denoiser during bake', default=False) # type: ignore
    bake_bake_margin: IntProperty(name='Bake Margin', description='Margin to use for baking', min=0, max=64, default=4) # type: ignore

    bake_post_blur: BoolProperty(name='Post Blur', description='Apply blur to final image', default=False) # type: ignore
    bake_disable_modifiers: BoolProperty(name='Disable modifiers', description='Disable modifiers during bake', default=True) # type: ignore

    # -------------------------

    pbr_mask_map: PointerProperty(type=PBRMappingProperties) # type: ignore
    pbr_mask_metallic_default: FloatProperty(name='Metallic', description='Metallic value if no map is present', min=0, max=1, default=0) # type: ignore
    pbr_mask_specular_default: FloatProperty(name='Specular', description='Specular value if no map is present', min=0, max=1, default=0.5) # type: ignore
    pbr_mask_roughness_default: FloatProperty(name='Roughness', description='Roughness value if no map is present', min=0, max=1, default=0.5) # type: ignore
    pbr_mask_colors: IntProperty(name='Max Colors', description='Number of colors for ramp', min=4, max=32, default=32) # type: ignore
    pbr_mask_max_cluster_size: FloatProperty(name='Max Cluster Size', description='Max size of one PBR cluster in color map', default=0.1, min=0.03, max=0.5) # type: ignore
    pbr_mask_order: EnumProperty(name='Ramp Order', description='How to sort the extracted PBR entries',
        items=(
            ( 'H', 'Hue', 'Hue' ),
            ( 'S', 'Saturation', 'Saturation' ),
            ( 'V', 'Value', 'Value' ),
            ( 'R', 'Red', 'Red' ),
            ( 'G', 'Green', 'Green' ),
            ( 'B', 'Blue', 'Blue' ),
            ( 'RGB', 'R+G+B', 'R+G+B' ),
            ( 'Me', 'Metallic', 'Metallic' ),
            ( 'Sp', 'Specular', 'Specular' ),
            ( 'Ro', 'Roughness', 'Roughness' ),
        ), 
        default='Ro'
    ) # type: ignore

    # -------------------------

    create_image_uv_map: EnumProperty(
        name='UVMap',
        description='UV Map to use for paint texture',
        items=lambda _, __: [ ( n.name, n.name, n.name ) for n in bpy.context.active_object.data.uv_layers ]
    ) # type: ignore
    create_image_alpha: BoolProperty(name='Alpha', description='Add alpha channel', default=True) # type: ignore
    create_image_32bit: BoolProperty(name='32-Bit Float', description='Create image with 32 bit per pixel', default=False) # type: ignore
    create_image_im_color: FloatVectorProperty(name='Color', subtype='COLOR', size=4, min=0.0, max=1.0, default=(0, 0, 0, 1)) # type: ignore
    create_image_fx_color: FloatVectorProperty(name='Color', subtype='COLOR', size=4, min=0.0, max=1.0, default=(0, 0, 0, 1)) # type: ignore

    @staticmethod
    def initialize():
        WindowManager.snw_properties = PointerProperty(type=Properties)
        

    @staticmethod
    def get() -> 'Properties':
        return bpy.context.window_manager.snw_properties       


    @staticmethod
    def dispose():
        if WindowManager.snw_properties:
            del(WindowManager.snw_properties)

