# -*- coding: utf-8 -*-
"""
Inspector
=========

Inspector is in charge to inspect a project about Sass stylesheets to search
for their dependencies.
"""
import io
import os

from collections import defaultdict

from .exceptions import CircularImport
from .parser import ScssImportsParser, SassImportsParser
from .resolver import ImportPathsResolver


class ScssInspector(ImportPathsResolver, ScssImportsParser):
    """
    Project inspector for SCSS sources

    Inspector is stateful, meaning you will need to invoke ``reset()`` then
    ``inspect()`` each time a project change, else the parents and children
    maps will be eventually incorrects.

    ``__init__`` method use ``reset`` method to initialize some internal
    buffers.

    Attributes:
        _CHILDREN_MAP: Dictionnary of finded direct children for each
            inspected sources.
        _PARENTS_MAP: Dictionnary of finded direct parents for each inspected
            sources.
        parsers: Dictionnary of available Sass format parsers, where key is the
            file extension related to the format and value is a parser
            instance.
    """
    parsers = {
        "scss": ScssImportsParser(),
        "sass": SassImportsParser(),
    }

    def __init__(self, *args, **kwargs):
        self.reset()

    def get_parser(self, path):
        """
        Returns parser depending of extension from given file path.

        Arguments:
            path (str): Path to split to find extension which will select the
                right parser. If file path does not have any extension or does not
                match any enabled extension, ``scss`` will be assumed on default.

        Returns:
            parser: Either ``ScssImportsParser`` or ``SassImportsParser``
            instance depending of filepath.
        """
        filepath, ext = os.path.splitext(path)

        # Remove leading dot from splitext to be able to match to
        # ``self.parsers`` keys
        if ext and ext.startswith("."):
            ext = ext[1:]

        if not ext or ext not in self.parsers:
            ext = "scss"

        return self.parsers.get(ext)

    def reset(self):
        """
        Reset internal buffers ``_CHILDREN_MAP`` and ``_PARENTS_MAP``.
        """
        self._CHILDREN_MAP = {}
        self._PARENTS_MAP = defaultdict(set)

    def look_source(self, sourcepath, library_paths=None):
        """
        Open a SCSS file (sourcepath) and find all involved files from
        import rules.

        This will fill internal buffers ``_CHILDREN_MAP`` and ``_PARENTS_MAP``.

        Arguments:
            sourcepath (str): Source file path to start searching for imports.

        Keyword Arguments:
            library_paths (list): List of directory paths for libraries to
                resolve paths if resolving fails on the base source path.
                Default to None.
        """
        # Don't inspect again source that has allready be inspected as a
        # children of a previous source
        if sourcepath not in self._CHILDREN_MAP:
            parser = self.get_parser(sourcepath)
            with io.open(sourcepath, "r", encoding="utf-8") as fp:
                finded_paths = parser.parse(fp.read())

            children = self.resolve(sourcepath, finded_paths,
                                    library_paths=library_paths)

            # Those files that are imported by the sourcepath
            self._CHILDREN_MAP[sourcepath] = children

            # Those files that import the sourcepath
            for p in children:
                self._PARENTS_MAP[p].add(sourcepath)

            # Start recursive finding through each resolved path that has not
            # been collected yet
            for path in children:
                if path not in self._CHILDREN_MAP:
                    self.look_source(path, library_paths=library_paths)

        return

    def inspect(self, *args, **kwargs):
        """
        Recursively inspect all given SCSS files to find imported dependencies.

        This does not return anything. Just fill internal buffers about
        inspected files.

        Note:
            This will ignore orphan files (files that are not imported from
            any of given SCSS files).

        Arguments:
            *args: One or multiple arguments, each one for a source file path
                to inspect.

        Keyword Arguments:
            library_paths (list): List of directory paths for libraries to
                resolve paths if resolving fails on the base source path.
                Default to None.
        """
        library_paths = kwargs.get("library_paths", None)

        for sourcepath in args:
            self.look_source(sourcepath, library_paths=library_paths)

    def _get_recursive_dependancies(self, dependencies_map, sourcepath,
                                    recursive=True):
        """
        Return all dependencies of a source, recursively searching through its
        dependencies.

        This is a common method used by ``children`` and ``parents`` methods.

        Arguments:
            dependencies_map (dict): Internal buffer (internal buffers
                ``_CHILDREN_MAP`` or ``_PARENTS_MAP``) to use for searching.
            sourcepath (str): Source file path to start searching for
                dependencies.

        Keyword Arguments:
            recursive (bool): Switch to enable recursive finding (if True).
                Default to True.

        Raises:
            CircularImport: If circular error is detected from a source.

        Returns:
            set: List of dependencies paths.
        """
        # Direct dependencies
        collected = set([])
        collected.update(dependencies_map.get(sourcepath, []))

        # Sequence of 'dependencies_map' items to explore
        sequence = collected.copy()
        # Exploration list
        walkthrough = []

        # Recursive search starting from direct dependencies
        if recursive:
            while True:
                if not sequence:
                    break
                item = sequence.pop()

                # Add current source to the explorated source list
                walkthrough.append(item)

                # Current item children
                current_item_dependancies = dependencies_map.get(item, [])

                for dependency in current_item_dependancies:
                    # Allready visited item, ignore and continue to the new
                    # item
                    if dependency in walkthrough:
                        continue
                    # Unvisited item yet, add its children to dependencies and
                    # item to explore
                    else:
                        collected.add(dependency)
                        sequence.add(dependency)

                # Sourcepath has allready been visited but present itself
                # again, assume it's a circular import
                if sourcepath in walkthrough:
                    msg = "A circular import has occured by '{}'"
                    raise CircularImport(msg.format(current_item_dependancies))

                # No more item to explore, break loop
                if not sequence:
                    break

        return collected

    def children(self, sourcepath, recursive=True):
        """
        Recursively find all children that are imported from the given source
        path.

        Arguments:
            sourcepath (str): Source file path to search for.

        Keyword Arguments:
            recursive (bool): Switch to enabled recursive finding (if True).
                Default to True.

        Returns:
            set: List of finded parents path.
        """
        return self._get_recursive_dependancies(
            self._CHILDREN_MAP,
            sourcepath,
            recursive=True
        )

    def parents(self, sourcepath, recursive=True):
        """
        Recursively find all parents that import the given source path.

        Arguments:
            sourcepath (str): Source file path to search for.

        Keyword Arguments:
            recursive (bool): Switch to enabled recursive finding (if True).
                Default to True.

        Returns:
            set: List of finded parents path.
        """
        return self._get_recursive_dependancies(
            self._PARENTS_MAP,
            sourcepath,
            recursive=True
        )
