
from .exception import BreatheError

import os


class ProjectError(BreatheError):
    pass


class NoDefaultProjectError(ProjectError):
    pass


class AutoProjectInfo(object):
    """Created as a temporary step in the automatic xml generation process"""

    def __init__(
            self,
            name,
            source_path,
            build_dir,
            reference,
            source_dir,
            config_dir,
            domain_by_extension,
            domain_by_file_pattern,
            match,
            show_define_initializer,
            project_refids,
            ):

        self._name = name
        self._source_path = source_path
        self._build_dir = build_dir
        self._reference = reference
        self._source_dir = source_dir
        self._config_dir = config_dir
        self._domain_by_extension = domain_by_extension
        self._domain_by_file_pattern = domain_by_file_pattern
        self._match = match
        self._show_define_initializer = show_define_initializer
        self._project_refids = project_refids

    def name(self):
        return self._name

    def build_dir(self):
        return self._build_dir

    def project_refids(self):
        return self._project_refids

    def abs_path_to_source_file(self, file_):
        """
        Returns full path to the provide file assuming that the provided path is relative to the
        projects conf.py directory as specified in the breathe_projects_source config variable.
        """

        # os.path.join does the appropriate handling if _source_path is an absolute path
        return os.path.join(self._config_dir, self._source_path, file_)

    def create_project_info(self, project_path):
        """Creates a proper ProjectInfo object based on the information in this AutoProjectInfo"""

        return ProjectInfo(
            self._name,
            project_path,
            self._source_path,
            self._reference,
            self._source_dir,
            self._config_dir,
            self._domain_by_extension,
            self._domain_by_file_pattern,
            self._match,
            self._show_define_initializer,
            self._project_refids,
            )


class ProjectInfo(object):

    def __init__(
            self,
            name,
            path,
            source_path,
            reference,
            source_dir,
            config_dir,
            domain_by_extension,
            domain_by_file_pattern,
            match,
            show_define_initializer,
            project_refids,
            ):

        self._name = name
        self._project_path = path
        self._source_path = source_path
        self._reference = reference
        self._source_dir = source_dir
        self._config_dir = config_dir
        self._domain_by_extension = domain_by_extension
        self._domain_by_file_pattern = domain_by_file_pattern
        self._match = match
        self._show_define_initializer = show_define_initializer
        self._project_refids = project_refids

    def name(self):
        return self._name

    def project_path(self):
        return self._project_path

    def source_path(self):
        return self._source_path

    def project_refids(self):
        return self._project_refids

    def relative_path_to_xml_file(self, file_):
        """
        Returns relative path from Sphinx documentation top-level source directory to the specified
        file assuming that the specified file is a path relative to the doxygen xml output
        directory.
        """

        # os.path.join does the appropriate handling if _project_path is an absolute path
        full_xml_project_path = os.path.join(self._config_dir, self._project_path, file_)

        return os.path.relpath(
            full_xml_project_path,
            self._source_dir
        )

    def sphinx_abs_path_to_file(self, file_):
        """
        Prepends os.path.sep to the value returned by relative_path_to_file.

        This is to match Sphinx's concept of an absolute path which starts from the top-level source
        directory of the project.
        """
        return os.path.sep + self.relative_path_to_xml_file(file_)

    def reference(self):
        return self._reference

    def domain_for_file(self, file_):

        domain = ""
        extension = file_.split(".")[-1]

        try:
            domain = self._domain_by_extension[extension]
        except KeyError:
            pass

        for pattern, pattern_domain in self._domain_by_file_pattern.items():
            if self._match(file_, pattern):
                domain = pattern_domain

        return domain

    def show_define_initializer(self):
        return self._show_define_initializer


class ProjectInfoFactory(object):

    def __init__(self, source_dir, build_dir, config_dir, match):

        self.source_dir = source_dir
        self.build_dir = build_dir
        self.config_dir = config_dir
        self.match = match

        self.projects = {}
        self.default_project = None
        self.domain_by_extension = {}
        self.domain_by_file_pattern = {}
        self.project_refids = False

        self.show_define_initializer = False

        self.project_count = 0
        self.project_info_store = {}
        self.project_info_for_auto_store = {}
        self.auto_project_info_store = {}

    def update(
            self,
            projects,
            default_project,
            domain_by_extension,
            domain_by_file_pattern,
            projects_source,
            build_dir,
            show_define_initializer,
            project_refids,
            ):

        self.projects = projects
        self.default_project = default_project
        self.domain_by_extension = domain_by_extension
        self.domain_by_file_pattern = domain_by_file_pattern
        self.projects_source = projects_source
        self.show_define_initializer = show_define_initializer
        self.project_refids = project_refids

        # If the breathe config values has a non-empty value for build_dir then use that otherwise
        # stick with the default
        if build_dir:
            self.build_dir = build_dir

    def default_path(self):

        if not self.default_project:
            raise NoDefaultProjectError(
                "No breathe_default_project config setting to fall back on "
                "for directive with no 'project' or 'path' specified."
            )

        try:
            return self.projects[self.default_project]
        except KeyError:
            raise ProjectError(
                ("breathe_default_project value '%s' does not seem to be a valid key for the "
                 "breathe_projects dictionary") % self.default_project
            )

    def create_project_info(self, options):

        name = self.default_project

        if "project" in options:
            try:
                path = self.projects[options["project"]]
                name = options["project"]
            except KeyError:
                raise ProjectError("Unable to find project '%s' in breathe_projects dictionary"
                                   % options["project"])

        elif "path" in options:
            path = options["path"]

        else:
            path = self.default_path()

        try:
            return self.project_info_store[path]
        except KeyError:

            reference = name

            if not name:
                name = "project%s" % self.project_count
                reference = path
                self.project_count += 1

            project_info = ProjectInfo(
                name,
                path,
                "NoSourcePath",
                reference,
                self.source_dir,
                self.config_dir,
                self.domain_by_extension,
                self.domain_by_file_pattern,
                self.match,
                self.show_define_initializer,
                self.project_refids,
            )

            self.project_info_store[path] = project_info

            return project_info

    def store_project_info_for_auto(self, name, project_info):
        """Stores the project info by name for later extraction by the auto directives.

        Stored separately to the non-auto project info objects as they should never overlap.
        """

        self.project_info_for_auto_store[name] = project_info

    def retrieve_project_info_for_auto(self, options):
        """Retrieves the project info by name for later extraction by the auto directives.

        Looks for the 'project' entry in the options dictionary. This is a less than ideal API but
        it is designed to match the use of 'create_project_info' above for which it makes much more
        sense.
        """

        name = options.get('project', self.default_project)

        if name is None:
            raise NoDefaultProjectError(
                "No breathe_default_project config setting to fall back on "
                "for directive with no 'project' or 'path' specified."
            )

        return self.project_info_for_auto_store[name]

    def create_auto_project_info(self, name, source_path):

        key = source_path

        try:
            return self.auto_project_info_store[key]
        except KeyError:

            reference = name

            if not name:
                name = "project%s" % self.project_count
                reference = source_path
                self.project_count += 1

            auto_project_info = AutoProjectInfo(
                name,
                source_path,
                self.build_dir,
                reference,
                self.source_dir,
                self.config_dir,
                self.domain_by_extension,
                self.domain_by_file_pattern,
                self.match,
                self.show_define_initializer,
                self.project_refids,
            )

            self.auto_project_info_store[key] = auto_project_info

            return auto_project_info
