# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from __future__ import unicode_literals

import os

from mozpack import path as mozpath
from mozpack.files import FileFinder


class FilterPath(object):
    """Helper class to make comparing and matching file paths easier."""
    def __init__(self, path, exclude=None):
        self.path = os.path.normpath(path)
        self._finder = None
        self.exclude = exclude

    @property
    def finder(self):
        if self._finder:
            return self._finder
        self._finder = FileFinder(
            self.path, find_executables=False, ignore=self.exclude)
        return self._finder

    @property
    def ext(self):
        return os.path.splitext(self.path)[1]

    @property
    def exists(self):
        return os.path.exists(self.path)

    @property
    def isfile(self):
        return os.path.isfile(self.path)

    @property
    def isdir(self):
        return os.path.isdir(self.path)

    def join(self, *args):
        return FilterPath(os.path.join(self, *args))

    def match(self, patterns):
        return any(mozpath.match(self.path, pattern.path) for pattern in patterns)

    def contains(self, other):
        """Return True if other is a subdirectory of self or equals self."""
        if isinstance(other, FilterPath):
            other = other.path
        a = os.path.abspath(self.path)
        b = os.path.normpath(os.path.abspath(other))

        if b.startswith(a):
            return True
        return False

    def __repr__(self):
        return repr(self.path)


def filterpaths(paths, linter, **lintargs):
    """Filters a list of paths.

    Given a list of paths, and a linter definition plus extra
    arguments, return the set of paths that should be linted.

    :param paths: A starting list of paths to possibly lint.
    :param linter: A linter definition.
    :param lintargs: Extra arguments passed to the linter.
    :returns: A list of file paths to lint.
    """
    include = linter.get('include', [])
    exclude = lintargs.get('exclude', [])
    exclude.extend(linter.get('exclude', []))
    root = lintargs['root']

    if not lintargs.get('use_filters', True) or (not include and not exclude):
        return paths

    def normalize(path):
        if not os.path.isabs(path):
            path = os.path.join(root, path)
        return FilterPath(path)

    include = map(normalize, include)
    exclude = map(normalize, exclude)

    # Paths with and without globs will be handled separately,
    # pull them apart now.
    includepaths = [p for p in include if p.exists]
    excludepaths = [p for p in exclude if p.exists]

    includeglobs = [p for p in include if not p.exists]
    excludeglobs = [p for p in exclude if not p.exists]

    extensions = linter.get('extensions')
    keep = set()
    discard = set()
    for path in map(FilterPath, paths):
        # Exclude bad file extensions
        if extensions and path.isfile and path.ext not in extensions:
            continue

        if path.match(excludeglobs):
            continue

        # First handle include/exclude directives
        # that exist (i.e don't have globs)
        for inc in includepaths:
            # Only excludes that are subdirectories of the include
            # path matter.
            excs = [e for e in excludepaths if inc.contains(e)]

            if path.contains(inc):
                # If specified path is an ancestor of include path,
                # then lint the include path.
                keep.add(inc)

                # We can't apply these exclude paths without explicitly
                # including every sibling file. Rather than do that,
                # just return them and hope the underlying linter will
                # deal with them.
                discard.update(excs)

            elif inc.contains(path):
                # If the include path is an ancestor of the specified
                # path, then add the specified path only if there are
                # no exclude paths in-between them.
                if not any(e.contains(path) for e in excs):
                    keep.add(path)

        # Next handle include/exclude directives that
        # contain globs.
        if path.isfile:
            # If the specified path is a file it must be both
            # matched by an include directive and not matched
            # by an exclude directive.
            if not path.match(includeglobs):
                continue

            keep.add(path)
        elif path.isdir:
            # If the specified path is a directory, use a
            # FileFinder to resolve all relevant globs.
            path.exclude = [e.path for e in excludeglobs]
            for pattern in includeglobs:
                for p, f in path.finder.find(pattern.path):
                    keep.add(path.join(p))

    # Only pass paths we couldn't exclude here to the underlying linter
    lintargs['exclude'] = [f.path for f in discard]
    return [f.path for f in keep]
