# -*- coding:utf-8 -*-

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  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 2
#  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, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

# ----------------------------------------------------------
# Author: Stephen Leger (s-leger)
#
# ----------------------------------------------------------
# noinspection PyUnresolvedReferences
import bpy
import os
# noinspection PyUnresolvedReferences
from bpy.types import (
    Panel, PropertyGroup,
    Object, Operator
    )
from bpy.props import (
    EnumProperty, CollectionProperty,
    StringProperty
    )


setman = None
libman = None


class MatLib():
    """
        A material library .blend file
        Store material name
        Apply material to objects
    """
    def __init__(self, matlib_path, name):
        self.name = name
        try:
            self.path = os.path.join(matlib_path, name)
        except:
            pass
        self.materials = []

    def cleanup(self):
        self.materials.clear()

    def load_list(self, sort=False):
        """
            list material names
        """
        # print("MatLib.load_list(%s)" % (self.name))
        self.materials.clear()
        try:
            with bpy.data.libraries.load(self.path) as (data_from, data_to):
                for mat in data_from.materials:
                    self.materials.append(mat)
            if sort:
                self.materials = list(sorted(self.materials))
        except:
            pass

    def has(self, name):
        return name in self.materials

    def load_mat(self, name, link):
        """
            Load a material from library
        """
        try:
            # print("MatLib.load_mat(%s) linked:%s" % (name, link))
            with bpy.data.libraries.load(self.path, link, False) as (data_from, data_to):
                data_to.materials = [name]
        except:
            pass

    def get_mat(self, name, link):
        """
            apply a material by name to active_object
            into slot index
            lazy load material list on demand
            return material or None
        """

        # Lazy load material names
        if len(self.materials) < 1:
            self.load_list()

        # material belongs to this libraray
        if self.has(name):

            # load material
            self.load_mat(name, link)

            return bpy.data.materials.get(name)

        return None


class MatlibsManager():
    """
        Manage multiple library
        Lazy load
    """
    def __init__(self):
        self.matlibs = []

    def cleanup(self):
        for lib in self.matlibs:
            lib.cleanup()
        self.matlibs.clear()

    def get_prefs(self, context):
        """
            let raise error if any
        """
        global __name__
        prefs = None
        # retrieve addon name from imports
        addon_name = __name__.split('.')[0]
        prefs = context.preferences.addons[addon_name].preferences
        return prefs

    @property
    def loaded_path(self):
        """
            Loaded matlibs filenames
        """
        return [lib.path for lib in self.matlibs]

    def from_data(self, name):
        return bpy.data.materials.get(name)

    def add_to_list(self, path):
        """
            Add material library to list
            only store name of lib
            reloading here doesn't make sense
        """
        loaded_path = self.loaded_path

        if os.path.exists(path):
            self.matlibs.extend(
                    [
                    MatLib(path, f) for f in os.listdir(path)
                    if f.endswith(".blend") and os.path.join(path, f) not in loaded_path
                    ]
            )

    def load_list(self, context):
        """
            list available library path
        """
        # default library
        dir_path = os.path.dirname(os.path.realpath(__file__))
        mat_path = os.path.join(dir_path, "materials")
        self.add_to_list(mat_path)

        # user def library path from addon prefs
        try:
            prefs = self.get_prefs(context)
            self.add_to_list(prefs.matlib_path)
        except:
            print("Archipack: Unable to load default material library, please check path in addon prefs")
            pass

    def apply(self, context, slot_index, name, link=False):

        o = context.active_object
        o.select_set(state=True)

        # material with same name exist in scene
        mat = self.from_data(name)

        # mat not in scene: try to load from lib
        if mat is None:
            # print("mat %s not found in scene, loading" % (name))
            # Lazy build matlibs list
            if len(self.matlibs) < 1:
                self.load_list(context)

            for lib in self.matlibs:
                mat = lib.get_mat(name, link)
                if mat is not None:
                    break

        # nothing found, build a default mat
        if mat is None:
            mat = bpy.data.materials.new(name)

        if slot_index < len(o.material_slots):
            o.material_slots[slot_index].material = None
            o.material_slots[slot_index].material = mat
            o.active_material_index = slot_index

        if not link:
            # break link
            bpy.ops.object.make_local(type="SELECT_OBDATA_MATERIAL")


class MaterialSetManager():
    """
        Manage material sets for objects
        Store material names for each set
        Lazy load at enumerate time
    """
    def __init__(self):
        """
            Store sets for each object type
        """
        self.objects = {}
        # hold reference of dynamic enumerator
        self.enums = {}
        self.default_enum = [('DEFAULT', 'Default', '', 0)]


    def get_filename(self, object_type):

        target_path = os.path.join("presets", "archipack_materials")
        target_path = bpy.utils.user_resource('SCRIPTS',
                                                target_path,
                                                create=True)
        return os.path.join(target_path, object_type) + '.txt'

    def cleanup(self):
        self.objects.clear()
        self.enums.clear()

    def register_set(self, object_type, set_name, materials_names):

        if object_type not in self.objects.keys():
            self.objects[object_type] = {}

        self.objects[object_type][set_name.upper()] = materials_names

    def load(self, object_type):

        filename = self.get_filename(object_type)

        # preset not found in user prefs, load from archipack's default
        if not os.path.exists(filename):
            rel_filepath = \
                os.path.sep + "presets" + os.path.sep + \
                "archipack_materials" + os.path.sep + object_type + '.txt'

            filename = os.path.dirname(os.path.realpath(__file__)) + rel_filepath

        # print("load filename %s" % filename)

        material_sets = {}

        # create file object, and set open mode

        try:
            with open(filename, 'r') as f:
                lines = f.readlines()

                for line in lines:
                    s_key, mat_name = line.split("##|##")
                    if str(s_key) not in material_sets.keys():
                        material_sets[s_key] = []
                    material_sets[s_key].append(mat_name.strip())
        except:
            print("Archipack: material preset for {} not found".format(object_type))
            pass

        s_keys = material_sets.keys()
        for s_key in s_keys:
            self.register_set(object_type, s_key, material_sets[s_key])

        self.make_enum(object_type, s_keys)

    def save(self, object_type):
        # always save in user prefs
        filename = self.get_filename(object_type)
        # print("filename:%s" % filename)
        o_dict = self.objects[object_type]
        lines = []
        s_keys = o_dict.keys()
        for s_key in s_keys:
            for mat in o_dict[s_key]:
                lines.append("{}##|##{}\n".format(s_key, mat))
        try:
            f = open(filename, 'w')
            f.writelines(lines)
        except:
            print("Archipack: An error occurred while saving {}".format(filename))
            pass
        finally:
            f.close()

        self.make_enum(object_type, s_keys)

    def add(self, context, set_name):
        o = context.active_object
        if "archipack_material" in o:
            object_type = o.archipack_material[0].category
            materials_names = [slot.name for slot in o.material_slots if slot.name != '']
            # print("%s " % materials_names)
            self.register_set(object_type, set_name, materials_names)
            self.save(object_type)

    def remove(self, context):
        o = context.active_object
        if "archipack_material" in o:
            d = o.archipack_material[0]
            object_type = d.category
            set_name = d.material
            s_keys = self.objects[object_type].keys()
            if set_name in s_keys:
                self.objects[object_type].pop(set_name)
                self.save(object_type)
            self.make_enum(object_type, s_keys)

    def get_materials(self, object_type, set_name):
        if object_type not in self.objects.keys():
            self.load(object_type)
        if object_type not in self.objects.keys():
            # print("Archipack: Unknown object type {}".format(object_type))
            return None
        if set_name not in self.objects[object_type].keys():
            # print("Archipack: set {} not found".format(set_name))
            return None
        return self.objects[object_type][set_name]

    def make_enum(self, object_type, s_keys):
        if len(s_keys) > 0:
            self.enums[object_type] = [(s.upper(), s.capitalize(), '', i) for i, s in enumerate(s_keys)]

    def get_enum(self, object_type):

        if object_type not in self.objects.keys():
            self.load(object_type)

        if object_type not in self.objects.keys():
            self.objects[object_type] = {}

        if object_type in self.enums:
            return self.enums[object_type]

        return self.default_enum


def material_enum(self, context):
    global setman
    if setman is None:
        setman = MaterialSetManager()
    return setman.get_enum(self.category)


def update(self, context):
    self.update(context)


class archipack_material(PropertyGroup):

    category : StringProperty(
        name="Category",
        description="Archipack object name",
        default=""
        )
    material : EnumProperty(
        name="Material",
        description="Material Set name",
        items=material_enum,
        update=update
        )

    def apply_material(self, context, slot_index, name):
        global libman

        if libman is None:
            libman = MatlibsManager()

        libman.apply(context, slot_index, name, link=False)

    def update(self, context):
        global setman

        if setman is None:
            setman = MaterialSetManager()

        o = context.active_object
        sel = [
            c for c in o.children
            if 'archipack_material' in c and c.archipack_material[0].category == self.category]

        # handle wall's holes
        if o.data and "archipack_wall2" in o.data:
            if o.parent is not None:
                for child in o.parent.children:
                    if ('archipack_hybridhole' in child or
                            'archipack_robusthole' in child or
                            'archipack_hole' in child):
                        sel.append(child)

        sel.append(o)

        mats = setman.get_materials(self.category, self.material)

        if mats is None or len(mats) < 1:
            return False

        for ob in sel:
            context.view_layer.objects.active = ob
            for slot_index, mat_name in enumerate(mats):
                if slot_index >= len(ob.material_slots):
                    bpy.ops.object.material_slot_add()
                self.apply_material(context, slot_index, mat_name)

        context.view_layer.objects.active = o

        return True


class ARCHIPACK_PT_material(Panel):
    bl_idname = "ARCHIPACK_PT_material"
    bl_label = "Archipack Material"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Archipack'

    @classmethod
    def poll(cls, context):
        return context.active_object is not None and 'archipack_material' in context.active_object

    def draw(self, context):
        layout = self.layout
        props = context.active_object.archipack_material[0]
        row = layout.row(align=True)
        row.prop(props, 'material', text="")
        row.operator('archipack.material_add', icon="ADD", text="")
        row.operator('archipack.material_remove', icon="REMOVE", text="")


class ARCHIPACK_OT_material(Operator):
    bl_idname = "archipack.material"
    bl_label = "Material"
    bl_description = "Add archipack material"
    bl_options = {'REGISTER', 'UNDO'}

    category : StringProperty(
        name="Category",
        description="Archipack object name",
        default=""
        )
    material : StringProperty(
        name="Material",
        description="Material Set name",
        default=""
        )

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):

        o = context.active_object

        if 'archipack_material' in o:
            m = o.archipack_material[0]
        else:
            m = o.archipack_material.add()

        m.category = self.category
        try:
            m.material = self.material
            res = m.update(context)
        except:
            res = False
            pass

        if not res:
            print("Archipack: unable to add material {} for {}".format(self.material, self.category))
            # self.report({'WARNING'}, 'Material {} for {} not found'.format(self.material, self.category))

        return {'FINISHED'}


class ARCHIPACK_OT_material_add(Operator):
    bl_idname = "archipack.material_add"
    bl_label = "Material"
    bl_description = "Add a set of archipack material"
    bl_options = {'REGISTER', 'UNDO'}

    material : StringProperty(
        name="Material",
        description="Material Set name",
        default=""
        )

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

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

    def execute(self, context):

        global setman

        if setman is None:
            setman = MaterialSetManager()

        setman.add(context, self.material)

        return {'FINISHED'}


class ARCHIPACK_OT_material_remove(Operator):
    bl_idname = "archipack.material_remove"
    bl_label = "Material"
    bl_description = "Remove a set of archipack material"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):

        global setman

        if setman is None:
            setman = MaterialSetManager()

        setman.remove(context)

        return {'FINISHED'}


class ARCHIPACK_OT_material_library(Operator):
    bl_idname = "archipack.material_library"
    bl_label = "Material Library"
    bl_description = "Add all archipack materials on a single object"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):

        global setman

        if setman is None:
            setman = MaterialSetManager()

        o = context.active_object

        if 'archipack_material' in o:
            m = o.archipack_material[0]
        else:
            m = o.archipack_material.add()
        o.data.materials.clear()

        for category in setman.objects.keys():
            prefix = category.capitalize() + "_"
            for part in setman.objects[category]["DEFAULT"]:
                name = prefix + part
                mat = m.get_material(name)
                o.data.materials.append(mat)

        return {'FINISHED'}


def register():
    bpy.utils.register_class(archipack_material)
    Object.archipack_material = CollectionProperty(type=archipack_material)
    bpy.utils.register_class(ARCHIPACK_OT_material)
    bpy.utils.register_class(ARCHIPACK_OT_material_add)
    bpy.utils.register_class(ARCHIPACK_OT_material_remove)
    bpy.utils.register_class(ARCHIPACK_OT_material_library)
    bpy.utils.register_class(ARCHIPACK_PT_material)


def unregister():
    global libman
    global setman
    if libman is not None:
        libman.cleanup()
    if setman is not None:
        setman.cleanup()
    bpy.utils.unregister_class(ARCHIPACK_PT_material)
    bpy.utils.unregister_class(ARCHIPACK_OT_material)
    bpy.utils.unregister_class(ARCHIPACK_OT_material_add)
    bpy.utils.unregister_class(ARCHIPACK_OT_material_remove)
    bpy.utils.unregister_class(ARCHIPACK_OT_material_library)
    del Object.archipack_material
    bpy.utils.unregister_class(archipack_material)
