#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015-2018, Fabian Greif
# Copyright (c) 2018, Niklas Hauser
# All Rights Reserved.
#
# The file is part of the lbuild project and is released under the
# 2-clause BSD license. See the file `LICENSE.txt` for the full license
# governing this code.

import os
import glob
import shutil
import logging
import fnmatch

import lbuild.filter
import lbuild.utils

from .facade import RepositoryInitFacade, RepositoryPrepareFacade
from .node import BaseNode, NameResolver
from .exception import LbuildException
from . import utils

LOGGER = logging.getLogger('lbuild.repository')


class Repository(BaseNode):
    """
    A repository is a set of modules.
    """

    def __init__(self, filename, name=None):
        """
        Construct a new repository object.

        At the construction time of the object, the name of repository may not
        be known e.g. if the repository is loaded from a `repo.lb` file.
        """
        BaseNode.__init__(self, name, self.Type.REPOSITORY, self)
        # Path to the repository file. All relative paths refer to this path.
        self._filename = os.path.realpath(filename)
        self._config_map = {}
        self._new_filters = {}

        # List of module filenames which are later transfered into
        # module objects
        self._module_files = []

    @property
    def modules(self):
        return {m.fullname:m for m in self.all_modules()}

    @staticmethod
    def parse_repository(repofilename: str):
        LOGGER.debug("Parse repository '%s'", repofilename)

        repo = Repository(repofilename)
        repo._functions = lbuild.node.load_functions_from_file(
                repo, repofilename, required=['init', 'prepare'], optional=['build'])

        # Execution init() function. In this function options are added.
        repo._functions['init'](RepositoryInitFacade(repo))

        if repo.name is None:
            raise LbuildException("The init(repo) function must set a repository name! "
                                "Please write the 'name' attribute.")

        # Prefix the global filters with the `repo.` name
        for name, func in repo._new_filters.items():
            repo._filters[repo.name + "." + name] = func

        # Prefix the global configuration map with the `repo:` name
        config_map = repo._config_map.copy()
        repo._config_map = {}
        for name, path in config_map.items():
            repo._config_map[repo.name + ":" + name] = path

        return repo

    def prepare(self, options):
        lbuild.utils.with_forward_exception(self,
                lambda: self._functions["prepare"](RepositoryPrepareFacade(self), self.option_value_resolver))

        modules = []
        # Parse the modules inside this repository
        for modulefile in self._module_files:
            module = lbuild.module.load_module_from_file(self, options, modulefile)
            modules.extend(module)

        return modules

    def build(self, env):
        build = self._functions.get("build", None)
        if build is not None:
            lbuild.utils.with_forward_exception(self, lambda: build(env))

    def add_modules_recursive(self, basepath="", modulefile="module.lb", ignore=None):
        """
        Find all module files following a specific pattern.

        Args:
            basepath: Rootpath for the search.
            modulefile: Filename pattern of the module files to search
                for (default: "module.lb").
            ignore: Filename pattern to ignore during search
        """
        ignore = utils.listify(ignore) + self._ignore_patterns
        ignore.append(os.path.relpath(self._filename, self._filepath))
        basepath = self._relocate_relative_path(basepath)
        for path, _, files in os.walk(basepath):
            for file in files:
                if any(fnmatch.fnmatch(file, i) for i in ignore):
                    continue
                if fnmatch.fnmatch(file, modulefile):
                    modulefilepath = os.path.normpath(os.path.join(path, file))
                    self._module_files.append(modulefilepath)

    def add_modules(self, *modules):
        """
        Add one or more module files.

        Args:
            modules: List of filenames
        """
        modules = [lbuild.utils.listify(m) for m in modules]
        modules = [inner for outer in modules for inner in outer]
        for file in modules:
            file = self._relocate_relative_path(file)

            if not os.path.isfile(file):
                raise LbuildException("Module file not found '%s'" % file)

            self._module_files.append(file)

    def glob(self, pattern):
        pattern = os.path.abspath(self._relocate_relative_path(pattern))
        return glob.glob(pattern)

    def __lt__(self, other):
        return self.fullname.__lt__(other.fullname)

    def __repr__(self):
        return "Repository({})".format(self._filepath)

    def __str__(self):
        """ Get string representation of repository.  """
        if self.name is not None:
            # The name must not always be set, e.g. during load
            # the repository name is only known after calling the
            # init function.
            return "{} at {}".format(self.name, self._filepath)
        else:
            return self._filepath
