import os
from dataclasses import dataclass
from typing import List, Optional, Dict

import fs
from fs.base import FS

import importlib_resources as resources
import bentoudev.dataclass.base as base

from zetsubou.project.model.cli_tool import CommandlineTool
from zetsubou.project.model.config_string import EDefaultConfigSlots
from zetsubou.project.model.configuration import Configuration
from zetsubou.project.model.kind import ETargetKind
from zetsubou.project.model.platform import Platform
from zetsubou.project.model.project_template import Project
from zetsubou.project.model.toolchain_profile import Profile
from zetsubou.project.model.rule import Rule
from zetsubou.project.model.target import Target, find_target
from zetsubou.project.model.toolchain import ToolchainDefinition, Toolchain
from zetsubou.utils import logger, yaml_loader
from zetsubou.utils.common import null_or_empty
from zetsubou.utils.error import ProjectError


class NoTraceError(Exception):
    pass


@dataclass
class ExternalConfig:
    profile_file: Optional[str] = None


@dataclass
class ProjectTemplate:
    project: Project
    profile: Optional[Profile]
    platforms: List[Platform]
    system_toolchains: List[ToolchainDefinition]
    platform_toolchains: Dict[str, List[Toolchain]]
    cli_tools: List[CommandlineTool]
    configurations: List[Configuration]
    rules: List[Rule]
    targets: List[Target]

    def find_target(self, name:str):
        return find_target(self.targets, name)

    def find_platform(self, name : str):
        for plat in self.platforms:
            if plat.platform == name:
                return plat
        return None

    def find_config(self, name : str):
        for conf in self.configurations:
            if conf.configuration == name:
                return conf
        return None

    def find_cli_tool(self, name : str):
        for tool in self.cli_tools:
            if tool.name == name:
                return tool
        return None

    def find_toolchain(self, platform_name: str, toolchain_name: str):
        toolchain_list : List[Toolchain] = self.platform_toolchains.get(platform_name, None)
        if toolchain_list is None:
            return None

        for tool in toolchain_list:
            if toolchain_name == tool.name:
                return tool
        return None

    def list_configurations(self):
        result = []
        for conf in self.configurations:
            result.append(conf.configuration)
        return result

    def list_platforms(self):
        result = []
        for plat in self.platforms:
            result.append(plat.platform)
        return result

    # This outputs invalid values - toolchains are dependent on their platforms!
    def list_toolchain(self):
        result = []
        for _, tools in self.platform_toolchains.items():
            names = [ t.name for t in tools ]
            result.extend(names)
        return result

    def slot_values(self):
        result = {
            EDefaultConfigSlots.platform.name : self.list_platforms(),
            EDefaultConfigSlots.configuration.name : self.list_configurations(),
            EDefaultConfigSlots.toolchain.name : self.list_toolchain()
        }

        for option in self.project.options:
            result[option.name] = option.values

        return result

    @staticmethod
    def compile_target_variant_name(kind: ETargetKind, name: str, config_string: str = ''):
        parts = []

        if kind != ETargetKind.INVALID:
            parts.append(f'{name}_{kind.name}')
        else:
            parts.append(name)

        if not null_or_empty(config_string):
            parts.append(config_string)

        return '-'.join(parts)

    @staticmethod
    def decompose_variant_name(variant:str, template_str:str):
        parts = variant.split('-')
        target = parts[0]
        config_parts = parts[1:]

        slots = template_str.split('-')

        if len(config_parts) != len(slots):
            return None, None

        result = {}
        for slot, val in zip(slots, config_parts):
            slot_name = slot.strip('}{')
            result[slot_name] = val

        return target, result





# def load_dataclass_single(clazz: type, obj_ref: Optional[str], proj_dir : str, project_fs : FS, loader, local_types):
#     if obj_ref is None:
#         return None

#     error_format = base.EErrorFormat.MSVC if logger.IsIde() else base.EErrorFormat.Pretty

#     if not base.is_loaded_from_file(clazz):
#         clazz = base.loaded_from_file(clazz)

#     obj_path = fs.path.join(proj_dir, obj_ref)

#     if not project_fs.exists(obj_path) or not project_fs.isfile(obj_path):
#         raise ProjectError(f"Unknown to locate file '{obj_path}'")

#     with project_fs.open(obj_path, 'r', encoding='utf-8') as obj_file:
#         obj_templ = loader.load_dataclass(clazz, obj_path, obj_file.read(), local_types, error_format=error_format)

#         if obj_templ is not None:
#             if not base.is_loaded_from_file(obj_templ):
#                 raise ProjectError(f"Unable to load class '{type(obj_templ)}' from file, missing attribute 'loadable_from_file'!")

#             if logger.IsVisible(logger.ELogLevel.Verbose):
#                 logger.Success(f"Loaded [{clazz.__name__}] '{obj_path}'")

#             obj_templ.set_loaded_from_file(obj_path)
#             return obj_templ
#         else:
#             raise NoTraceError()


def load_dataclass_list(clazz: type, obj_ref_list: Optional[List[str]], proj_dir : str, project_fs : FS, loader, local_types):
    result = []

    error_format = base.EErrorFormat.MSVC if logger.IsIde() else base.EErrorFormat.Pretty

    if not base.is_loaded_from_file(clazz):
        clazz = base.loaded_from_file(clazz)

    if obj_ref_list is not None and len(obj_ref_list) > 0:

        for obj_ref in obj_ref_list:
            obj_path = fs.path.join(proj_dir, obj_ref)

            if not project_fs.exists(obj_path) or not project_fs.isfile(obj_path):
                raise ProjectError(f"Unknown to locate file '{obj_path}'")

            with project_fs.open(obj_path, 'r', encoding='utf-8') as obj_file:
                obj_templ = loader.load_dataclass(clazz, obj_path, obj_file.read(), local_types, error_format=error_format)

                if obj_templ is not None:
                    if not base.is_loaded_from_file(obj_templ):
                        raise ProjectError(f"Unable to load class '{type(obj_templ)}' from file, missing attribute 'loadable_from_file'!")

                    if logger.IsVisible(logger.ELogLevel.Verbose):
                        logger.Success(f"Loaded [{clazz.__name__}] '{obj_path}'")

                    obj_templ.set_loaded_from_file(obj_path)
                    result.append(obj_templ)
                else:
                    raise NoTraceError()

    return result


def load_bundled_rules(loader, local_types) -> List[Rule]:
    result = []
    for res_path in [ 'MsvcRules.yml', 'ClangRules.yml' ]:
        res_content = resources.files('zetsubou.data.rules').joinpath(res_path).read_text()
        obj_templ = loader.load_dataclass(Rule, res_path, res_content, local_types)

        if obj_templ is not None:
            if not base.is_loaded_from_file(obj_templ):
                raise ProjectError(f"Unable to load class '{type(obj_templ)}' from file, missing attribute 'loadable_from_file'!")

            if logger.IsVisible(logger.ELogLevel.Verbose):
                logger.Success(f"Loaded [Rule] '{res_path}'")

            obj_templ.set_loaded_from_file(res_path)
            result.append(obj_templ)
    return result


def load_project_template(project_fs: FS, fs_root : str, filename: str, proj_file_content: str, *, loader=yaml_loader.YamlDataclassLoader(), external_config:Optional[ExternalConfig]=None) -> Optional[ProjectTemplate]:
    try:
        local_types = base.get_types_from_modules([__name__, 'zetsubou.project.model.target'])

        proj_dir = os.path.dirname(filename)
        proj_templ: Project = loader.load_dataclass(Project, filename, proj_file_content, local_types)
        if proj_templ is None:
            return

        proj_templ.set_loaded_from_file(filename)

        def load_dataclasses(clazz: type, obj_ref_list: Optional[List[str]]):
            return load_dataclass_list(clazz, obj_ref_list, proj_dir, project_fs, loader, local_types)

        targets = load_dataclasses(Target, proj_templ.targets)

        proj = ProjectTemplate(
            project=proj_templ,
            profile=None,
            platforms=load_dataclasses(Platform, proj_templ.config.platforms),
            system_toolchains=[],
            platform_toolchains={},
            cli_tools=load_dataclasses(CommandlineTool, proj_templ.config.cli_tools),
            configurations=load_dataclasses(Configuration, proj_templ.config.configurations),
            rules=load_dataclasses(Rule, proj_templ.config.rules),
            targets=targets,
        )

        # Profile handling
        if external_config is not None and external_config.profile_file is not None:
            proj.profile = load_dataclasses(Profile, [ external_config.profile_file ])[0]
        else:
            proj.profile = load_dataclasses(Profile, [ proj.project.config.dev_profile ])[0]

        # Filter by config
        if proj.profile is not None and proj.profile.build_type is not None:
            def is_from_base(config:Configuration):
                return config.base_configuration == proj.profile.build_type

            proj.configurations = list(filter(is_from_base, proj.configurations))
            if len(proj.configurations) == 0:
                raise ProjectError(f"No configurations of base type '{proj.profile.build_type}' found!")
            else:
                proj.configurations = [ proj.configurations[0] ]

        proj.rules += load_bundled_rules(loader, local_types)

        return proj

    except NoTraceError:
        logger.CriticalError(f'Failed to load project \'{filename}\'')
        return None

    except base.DataclassLoadError as error:
        logger.Error(error)
        logger.CriticalError(f'Failed to load project \'{filename}\'')
        return None

    except ProjectError as error:
        logger.Error(error)
        logger.CriticalError(f'Failed to load project \'{filename}\'')
        return None

    except Exception as error:
        logger.Exception(error)
        logger.CriticalError(f'Failed to load project  \'{filename}\'')
        return None


def load_project_from_file(project_fs: FS, fs_root : str, filename: str, *, external_config:Optional[ExternalConfig]=None) -> Optional[ProjectTemplate]:
    if not project_fs.exists(filename):
        logger.CriticalError(f"Unable to locate file '{filename}'")
        return None
    with project_fs.open(filename, 'r', encoding='utf-8') as proj_file:
        return load_project_template(project_fs, fs_root, filename, proj_file.read(), external_config=external_config)
