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

import os, bpy, json

from typing import Dict, List
from dataclasses import dataclass

from .previews_registry import PreviewsRegistry
from ..utils.io import decode_json
from ..utils.dev import dbg, err, log_exception
from ..utils.node_builder_cycles import NodeBuilderCycles
from ..operators.helpers.setup_grunge import setup_grunge
from .texture_registry import TextureRegistry


@dataclass
class CollectionItem:
    name: str
    description: str
    author: str

    blend_file: str
    node: str
    group: str
    copy: bool
    replace: bool
    op_icon: str
    menu_id: int

    icon_id: int

    def load_item(self) -> bpy.types.NodeTree:
        """
        (Eventually) load item and return it's node tree.
        """
        # If copy is false, try to find it's already loaded instance.
        if not self.copy:
            if self.node in bpy.data.node_groups:
                return bpy.data.node_groups[self.node]

        # Ok, load it.
        found = False
        with bpy.data.libraries.load(self.blend_file, link=False) as (data_from, data_to):
            if self.node in data_from.node_groups:
                data_to.node_groups = [self.name, ]
                found = True

        if found:
            grp = data_to.node_groups[0] # type: bpy.types.ShaderNodeTree

            # If replace is active, we have to remap textures 
            # based on their hash to the ones from texture repository.
            if self.replace:
                # Handle all node groups.
                for node in grp.nodes:
                    if node.bl_idname == 'ShaderNodeGroup':
                        try:
                            info = decode_json(node.node_tree.snw_node_info)
                            bin_hash = info.get('bin_hash', '')

                            category, tex = TextureRegistry.instance().get_by_bin_hash(bin_hash)
                            if category and tex:
                                nb = NodeBuilderCycles()
                                nb.tree_from_group(node.node_tree, create_io=False)
                                nb.clear_nodes([ 'NodeGroupInput', 'NodeGroupOutput' ])

                                setup_grunge(
                                    nb, 
                                    category, 
                                    tex, 
                                    bin_hash,
                                    info.get('mapping', 'LBOX'),
                                    info.get('blend', 0.5),
                                    info.get('interpolation', 'Linear'),
                                    info.get('anti_repeat_style', 'OFF'),
                                    info.get('anti_repeat_scale', 2),
                                    info.get('anti_repeat_distortion', 0.1),
                                    info.get('anti_repeat_style_param_0', 0.1),
                                    info.get('anti_repeat_seed', 0),
                                )
                        except Exception as e:
                            log_exception(e, context_msg="Failed to setup grunge")

            return grp


class CollectionGroup:
    def __init__(self, name: str, display: str):
        self.name = name
        self.display = display
        self.items = [] # type: List[CollectionItem]


class Collections:
    def _load_collection_from_blend(self, blend: str):
        """
        Load blend and extract all node group informations.
        Add all found nodes as CollectionItem to self._collections.
        """
        dbg(f'Load collection: {blend}')
        basename = os.path.splitext(os.path.split(blend)[1])[0]
        preview_path = os.path.join(os.path.dirname(blend), basename)

        # Read [basename].json, which contains all information.
        js_file = os.path.join(os.path.dirname(blend), f'{basename}.json')
        if os.path.exists(js_file):
            # Parse info, which is encoded as JSON.
            try:
                with open(js_file, 'r') as f:
                    js_raw = f.read()

                js = json.loads(js_raw.strip()) # type: Dict[str, any]
                author = js['author']
                group = js['group']
                display = js['display']
                group_op_icon = js['op_icon']
                for g in js['groups']:
                    # Extract info for item from json.
                    name = g['name']
                    description = g['description']
                    copy = g['copy']
                    replace = g['replace'] if 'replace' in g else False
                    op_icon = g['op_icon'] if 'op_icon' in g else group_op_icon
                    menu_id = g['menu_id'] if 'menu_id' in g else 0

                    # Load icon.
                    preview_image = os.path.join(preview_path, f'{name}.jpg')
                    if os.path.exists(preview_image):
                        icon_id = self._icons.load(f'SNWC_{basename}_{name}', preview_image)
                    else:
                        icon_id = -1

                    # Add to dict.
                    if group not in self._collections:
                        self._collections[group] = CollectionGroup(group, display)
                    self._collections[group].items.append( 
                        CollectionItem(
                            name,
                            description,
                            author,
                            blend,
                            name,
                            group,
                            copy,
                            replace,
                            op_icon,
                            menu_id,
                            icon_id
                        )
                    )
            except Exception as ex:
                err('Failed to load, verify JSON info')


    def _scan_collections(self, path: str):
        """
        Load all collections from given path.
        """
        dbg(f'Load collections in {path}')
        for _b in os.listdir(path):
            b = os.path.join(path, _b)
            if os.path.isfile(b) and b.endswith('.blend'):
                self._load_collection_from_blend(b)


    def _lazy_loading(self):
        """
        Initialize collection from all known paths.
        """
        if not self._loaded:
            self._loaded = True

            # Do this only once.        
            self._scan_collections(os.path.join(os.path.dirname(__file__), '..', 'data', 'collections'))


    def init(self):
        PreviewsRegistry.instance().register('__collections')
        self._icons = PreviewsRegistry.instance().collection('__collections')
        self._loaded = False
        self._collections = {} # type: Dict[str, CollectionGroup]
        self._static_collections = [ 'Masks', 'Materials', 'Utils'  ] # Always visible.


    def dispose(self):
        """
        Final cleanup.
        """


    def collections(self) -> List[str]:
        """
        Return all collection names.
        """
        self._lazy_loading()
        return self._collections.keys()


    def static_collections(self) -> List[CollectionGroup]:
        """
        Collections that are always fully visible.
        """
        self._lazy_loading()
        return [ self._collections[n] for n in self._static_collections ]


    def load_item(self, group: str, idx: int) -> bpy.types.NodeTree:
        """
        Load item from collection and return it's node tree.
        """
        return self._collections[group].items[idx].load_item()


    @staticmethod
    def instance() -> 'Collections':
        """
        Access as singleton.
        """
        global _collections_instance
        return _collections_instance


_collections_instance = Collections()        