# 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/.

import os
from compare_locales import mozpath


REFERENCE_LOCALE = 'en-x-moz-reference'


class ConfigList(list):
    def maybe_extend(self, other):
        '''Add configs from other list if this list doesn't have this path yet.
        '''
        for config in other:
            if any(mine.path == config.path for mine in self):
                continue
            self.append(config)


class ProjectFiles:
    '''Iterable object to get all files and tests for a locale and a
    list of ProjectConfigs.

    If the given locale is None, iterate over reference files as
    both reference and locale for a reference self-test.
    '''
    def __init__(self, locale, projects, mergebase=None):
        self.locale = locale
        self.matchers = []
        self.exclude = None
        self.mergebase = mergebase
        configs = ConfigList()
        excludes = ConfigList()
        for project in projects:
            # Only add this project if we're not in validation mode,
            # and the given locale is enabled for the project.
            if locale is not None and locale not in project.all_locales:
                continue
            configs.maybe_extend(project.configs)
            excludes.maybe_extend(project.excludes)
        # If an excluded config is explicitly included, drop if from the
        # excludes.
        excludes = [
            exclude
            for exclude in excludes
            if not any(c.path == exclude.path for c in configs)
        ]
        if excludes:
            self.exclude = ProjectFiles(locale, excludes)
        for pc in configs:
            if locale and pc.locales is not None and locale not in pc.locales:
                continue
            for paths in pc.paths:
                if (
                    locale and
                    'locales' in paths and
                    locale not in paths['locales']
                ):
                    continue
                m = {
                    'l10n': paths['l10n'].with_env({
                        "locale": locale or REFERENCE_LOCALE
                    }),
                    'module': paths.get('module'),
                }
                if 'reference' in paths:
                    m['reference'] = paths['reference']
                if self.mergebase is not None:
                    m['merge'] = paths['l10n'].with_env({
                        "locale": locale,
                        "l10n_base": self.mergebase
                    })
                m['test'] = set(paths.get('test', []))
                if 'locales' in paths:
                    m['locales'] = paths['locales'][:]
                self.matchers.append(m)
        self.matchers.reverse()  # we always iterate last first
        # Remove duplicate patterns, comparing each matcher
        # against all other matchers.
        # Avoid n^2 comparisons by only scanning the upper triangle
        # of a n x n matrix of all possible combinations.
        # Using enumerate and keeping track of indexes, as we can't
        # modify the list while iterating over it.
        drops = set()  # duplicate matchers to remove
        for i, m in enumerate(self.matchers[:-1]):
            if i in drops:
                continue  # we're dropping this anyway, don't search again
            for i_, m_ in enumerate(self.matchers[(i+1):]):
                if (mozpath.realpath(m['l10n'].prefix) !=
                        mozpath.realpath(m_['l10n'].prefix)):
                    # ok, not the same thing, continue
                    continue
                if m['l10n'].pattern != m_['l10n'].pattern:
                    # We cannot guess whether same entry until the pattern is
                    # resolved, continue
                    continue
                # check that we're comparing the same thing
                if 'reference' in m:
                    if (mozpath.realpath(m['reference'].prefix) !=
                            mozpath.realpath(m_.get('reference').prefix)):
                        raise RuntimeError('Mismatch in reference for ' +
                                           mozpath.realpath(m['l10n'].prefix))
                drops.add(i_ + i + 1)
                m['test'] |= m_['test']
        drops = sorted(drops, reverse=True)
        for i in drops:
            del self.matchers[i]

    def __iter__(self):
        # The iteration is pretty different when we iterate over
        # a localization vs over the reference. We do that latter
        # when running in validation mode.
        inner = self.iter_locale() if self.locale else self.iter_reference()
        yield from inner

    def iter_locale(self):
        '''Iterate over locale files.'''
        known = {}
        for matchers in self.matchers:
            matcher = matchers['l10n']
            for path in self._files(matcher):
                if path not in known:
                    known[path] = {'test': matchers.get('test')}
                    if 'reference' in matchers:
                        known[path]['reference'] = matcher.sub(
                            matchers['reference'], path)
                    if 'merge' in matchers:
                        known[path]['merge'] = matcher.sub(
                            matchers['merge'], path)
            if 'reference' not in matchers:
                continue
            matcher = matchers['reference']
            for path in self._files(matcher):
                l10npath = matcher.sub(matchers['l10n'], path)
                if l10npath not in known:
                    known[l10npath] = {
                        'reference': path,
                        'test': matchers.get('test')
                    }
                    if 'merge' in matchers:
                        known[l10npath]['merge'] = \
                            matcher.sub(matchers['merge'], path)
        for path, d in sorted(known.items()):
            yield (path, d.get('reference'), d.get('merge'), d['test'])

    def iter_reference(self):
        '''Iterate over reference files.'''
        # unset self.exclude, as we don't want that for our reference files
        exclude = self.exclude
        self.exclude = None
        known = {}
        for matchers in self.matchers:
            if 'reference' not in matchers:
                continue
            matcher = matchers['reference']
            for path in self._files(matcher):
                refpath = matcher.sub(matchers['reference'], path)
                if refpath not in known:
                    known[refpath] = {
                        'reference': path,
                        'test': matchers.get('test')
                    }
        for path, d in sorted(known.items()):
            yield (path, d.get('reference'), None, d['test'])
        self.exclude = exclude

    def _files(self, matcher):
        '''Base implementation of getting all files in a hierarchy
        using the file system.
        Subclasses might replace this method to support different IO
        patterns.
        '''
        base = matcher.prefix
        if self._isfile(base):
            if self.exclude and self.exclude.match(base) is not None:
                return
            if matcher.match(base) is not None:
                yield base
            return
        for d, dirs, files in self._walk(base):
            for f in files:
                p = mozpath.join(d, f)
                if self.exclude and self.exclude.match(p) is not None:
                    continue
                if matcher.match(p) is not None:
                    yield p

    def _isfile(self, path):
        return os.path.isfile(path)

    def _walk(self, base):
        yield from os.walk(base)

    def match(self, path):
        '''Return the tuple of l10n_path, reference, mergepath, tests
        if the given path matches any config, otherwise None.

        This routine doesn't check that the files actually exist.
        '''
        if (
            self.locale is not None and
            self.exclude and self.exclude.match(path) is not None
        ):
            return
        for matchers in self.matchers:
            matcher = matchers['l10n']
            if self.locale is not None and matcher.match(path) is not None:
                ref = merge = None
                if 'reference' in matchers:
                    ref = matcher.sub(matchers['reference'], path)
                if 'merge' in matchers:
                    merge = matcher.sub(matchers['merge'], path)
                return path, ref, merge, matchers.get('test')
            if 'reference' not in matchers:
                continue
            matcher = matchers['reference']
            if matcher.match(path) is not None:
                merge = None
                l10n = matcher.sub(matchers['l10n'], path)
                if 'merge' in matchers:
                    merge = matcher.sub(matchers['merge'], path)
                return l10n, path, merge, matchers.get('test')
