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

'''gaia-style web apps support

This variant supports manifest.webapp localization as well as
.properties files with a naming scheme of locales/foo.*.properties.
'''

from collections import defaultdict
import json
import os
import os.path
import re

from compare_locales.paths import File, EnumerateDir
from compare_locales.compare import AddRemove, ContentComparer


class WebAppCompare(object):
    '''For a given directory, analyze
    /manifest.webapp
    /locales/*.*.properties

    Deduce the present locale codes.
    '''
    ignore_dirs = EnumerateDir.ignore_dirs
    reference_locale = 'en-US'

    def __init__(self, basedir):
        '''Constructor
        :param basedir: Directory of the web app to inspect
        '''
        self.basedir = basedir
        self.manifest = Manifest(basedir, self.reference_locale)
        self.files = FileComparison(basedir, self.reference_locale)
        self.watcher = None

    def compare(self, locales):
        '''Compare the manifest.webapp and the locales/*.*.properties
        '''
        if not locales:
            locales = self.locales()
        self.manifest.compare(locales)
        self.files.compare(locales)

    def setWatcher(self, watcher):
        self.watcher = watcher
        self.manifest.watcher = watcher
        self.files.watcher = watcher

    def locales(self):
        '''Inspect files on disk to find present languages.
        :rtype: List of locales, sorted, including reference.
        '''
        locales = set(self.manifest.strings.keys())
        locales.update(self.files.locales())
        locales = list(sorted(locales))
        return locales


class Manifest(object):
    '''Class that helps with parsing and inspection of manifest.webapp.
    '''

    def __init__(self, basedir, reference_locale):
        self.file = File(os.path.join(basedir, 'manifest.webapp'),
                         'manifest.webapp')
        self.reference_locale = reference_locale
        self._strings = None
        self.watcher = None

    @property
    def strings(self):
        if self._strings is None:
            self._strings = self.load_and_parse()
        return self._strings

    def load_and_parse(self):
        try:
            manifest = json.load(open(self.file.fullpath))
        except (ValueError, IOError), e:
            if self.watcher:
                self.watcher.notify('error', self.file, str(e))
            return False
        return self.extract_manifest_strings(manifest)

    def extract_manifest_strings(self, manifest_fragment):
        '''Extract localizable strings from a manifest dict.
        This method is recursive, and returns a two-level dict,
        first level being locale codes, second level being generated
        key and localized value. Keys are generated by concatenating
        each level in the json with a ".".
        '''
        rv = defaultdict(dict)
        localizable = manifest_fragment.pop('locales', {})
        if localizable:
            for locale, keyvalue in localizable.iteritems():
                for key, value in keyvalue.iteritems():
                    key = '.'.join(['locales', 'AB_CD', key])
                    rv[locale][key] = value
        for key, sub_manifest in manifest_fragment.iteritems():
            if not isinstance(sub_manifest, dict):
                continue
            subdict = self.extract_manifest_strings(sub_manifest)
            if subdict:
                for locale, keyvalue in subdict:
                    rv[locale].update((key + '.' + subkey, value)
                                      for subkey, value
                                      in keyvalue.iteritems())
        return rv

    def compare(self, locales):
        strings = self.strings
        if not strings:
            return
        # create a copy so that we can mock around with it
        strings = strings.copy()
        reference = strings.pop(self.reference_locale)
        for locale in locales:
            if locale == self.reference_locale:
                continue
            self.compare_strings(reference,
                                 strings.get(locale, {}),
                                 locale)

    def compare_strings(self, reference, l10n, locale):
        add_remove = AddRemove()
        add_remove.set_left(sorted(reference.keys()))
        add_remove.set_right(sorted(l10n.keys()))
        missing = obsolete = changed = unchanged = 0
        for op, item_or_pair in add_remove:
            if op == 'equal':
                if reference[item_or_pair[0]] == l10n[item_or_pair[1]]:
                    unchanged += 1
                else:
                    changed += 1
            else:
                key = item_or_pair.replace('.AB_CD.',
                                           '.%s.' % locale)
                if op == 'add':
                    # obsolete entry
                    obsolete += 1
                    self.watcher.notify('obsoleteEntity', self.file, key)
                else:
                    # missing entry
                    missing += 1
                    self.watcher.notify('missingEntity', self.file, key)


class FileComparison(object):
    '''Compare the locales/*.*.properties files inside a webapp.
    '''
    prop = re.compile('(?P<base>.*)\\.'
                      '(?P<locale>[a-zA-Z]+(?:-[a-zA-Z]+)*)'
                      '\\.properties$')

    def __init__(self, basedir, reference_locale):
        self.basedir = basedir
        self.reference_locale = reference_locale
        self.watcher = None
        self._reference = self._files = None

    def locales(self):
        '''Get the locales present in the webapp
        '''
        self.files()
        locales = self._files.keys()
        locales.sort()
        return locales

    def compare(self, locales):
        self.files()
        for locale in locales:
            l10n = self._files[locale]
            filecmp = AddRemove()
            filecmp.set_left(sorted(self._reference.keys()))
            filecmp.set_right(sorted(l10n.keys()))
            for op, item_or_pair in filecmp:
                if op == 'equal':
                    self.watcher.compare(self._reference[item_or_pair[0]],
                                         l10n[item_or_pair[1]])
                elif op == 'add':
                    # obsolete file
                    self.watcher.remove(l10n[item_or_pair])
                else:
                    # missing file
                    _path = '.'.join([item_or_pair, locale, 'properties'])
                    missingFile = File(
                        os.path.join(self.basedir, 'locales', _path),
                        'locales/' + _path)
                    self.watcher.add(self._reference[item_or_pair],
                                     missingFile)

    def files(self):
        '''Read the list of locales from disk.
        '''
        if self._reference:
            return
        self._reference = {}
        self._files = defaultdict(dict)
        path_list = self._listdir()
        for path in path_list:
            match = self.prop.match(path)
            if match is None:
                continue
            locale = match.group('locale')
            if locale == self.reference_locale:
                target = self._reference
            else:
                target = self._files[locale]
            fullpath = os.path.join(self.basedir, 'locales', path)
            target[match.group('base')] = File(fullpath, 'locales/' + path)

    def _listdir(self):
        'Monkey-patch this for testing.'
        return os.listdir(os.path.join(self.basedir, 'locales'))


def compare_web_app(basedir, locales, other_observer=None):
    '''Compare gaia-style web app.

    Optional arguments are:
    - other_observer. A object implementing
        notify(category, _file, data)
      The return values of that callback are ignored.
    '''
    comparer = ContentComparer()
    if other_observer is not None:
        comparer.add_observer(other_observer)
    webapp_comp = WebAppCompare(basedir)
    webapp_comp.setWatcher(comparer)
    webapp_comp.compare(locales)
    return comparer.observer
