#!/usr/bin/env python3
"""Extract, synchronize, compile and validate bundled WordPress translations."""

import argparse
from collections import Counter
from html.parser import HTMLParser
from pathlib import Path
import re
import subprocess
import sys
import tempfile

import polib

ROOT = Path(__file__).resolve().parents[1]
DOMAIN = 'sumup-payment-gateway-for-woocommerce'
LANGUAGES = ROOT / 'languages'
PLURALS = {
    'bg': 'nplurals=2; plural=(n != 1);',
    'cs': 'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
    'da': 'nplurals=2; plural=(n != 1);',
    'de': 'nplurals=2; plural=(n != 1);',
    'el': 'nplurals=2; plural=(n != 1);',
    'es': 'nplurals=2; plural=(n != 1);',
    'et': 'nplurals=2; plural=(n != 1);',
    'fi': 'nplurals=2; plural=(n != 1);',
    'fr': 'nplurals=2; plural=(n > 1);',
    'hr': 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);',
    'hu': 'nplurals=2; plural=(n != 1);',
    'it': 'nplurals=2; plural=(n != 1);',
    'lt': 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);',
    'lv': 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);',
    'nb': 'nplurals=2; plural=(n != 1);',
    'nl': 'nplurals=2; plural=(n != 1);',
    'pl': 'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);',
    'pt': 'nplurals=2; plural=(n != 1);',
    'pt_BR': 'nplurals=2; plural=(n > 1);',
    'ro': 'nplurals=3; plural=(n==1 ? 0 : n==0 || (n%100>0 && n%100<20) ? 1 : 2);',
    'sk': 'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
    'sl': 'nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);',
    'sv': 'nplurals=2; plural=(n != 1);',
}


def metadata(locale=''):
    result = {
        'Project-Id-Version': 'SumUp Payment Gateway For WooCommerce',
        'Report-Msgid-Bugs-To': 'https://wordpress.org/support/plugin/sumup-payment-gateway-for-woocommerce/',
        'Language': locale,
        'MIME-Version': '1.0',
        'Content-Type': 'text/plain; charset=UTF-8',
        'Content-Transfer-Encoding': '8bit',
        'X-Domain': DOMAIN,
    }
    if locale:
        result['Plural-Forms'] = PLURALS.get(locale, PLURALS[locale.split('_')[0]])
    return result


def extract():
    sources = sorted([ROOT / f'{DOMAIN}.php', *(ROOT / 'includes').rglob('*.php'), *(ROOT / 'templates').rglob('*.php')])
    keywords = ['__:1', '_e:1', '_x:1,2c', '_ex:1,2c', '_n:1,2', '_nx:1,2,4c',
                'esc_html__:1', 'esc_html_e:1', 'esc_attr__:1', 'esc_attr_e:1',
                'esc_html_x:1,2c', 'esc_attr_x:1,2c', '_n_noop:1,2', '_nx_noop:1,2,3c']
    with tempfile.TemporaryDirectory(prefix='sumup-pot-') as directory:
        output = Path(directory) / 'messages.pot'
        subprocess.run(['xgettext', '--language=PHP', '--from-code=UTF-8', '--no-wrap',
                        '--add-comments=translators:', '--keyword=',
                        *[f'--keyword={keyword}' for keyword in keywords],
                        f'--output={output}', *[str(p.relative_to(ROOT)) for p in sources]], cwd=ROOT, check=True)
        catalog = polib.pofile(str(output), wrapwidth=0)
    # Plugin headers are translated by WordPress but are not PHP function calls.
    header = (ROOT / f'{DOMAIN}.php').read_text(encoding='utf-8').split('*/', 1)[0]
    for field in ['Plugin Name', 'Plugin URI', 'Description', 'Author', 'Author URI']:
        match = re.search(r'^\s*\*?\s*' + field + r':\s*(.+)$', header, re.MULTILINE)
        if match and not catalog.find(match[1].strip()):
            catalog.append(polib.POEntry(msgid=match[1].strip(), comment=f'{field} of the plugin'))
    catalog.metadata = metadata()
    catalog.header = 'Generated by scripts/i18n.py. Edit PHP sources, then run i18n.py update.'
    catalog.sort(key=lambda entry: (entry.msgctxt or '', entry.msgid))
    return catalog


def synchronize(catalog, template, locale):
    catalog.merge(template)
    catalog[:] = [entry for entry in catalog if not entry.obsolete]
    catalog.sort(key=lambda entry: (entry.msgctxt or '', entry.msgid))
    catalog.metadata = metadata(locale)
    catalog.header = 'Translations for the SumUp WooCommerce plugin.\nEdit this PO file, then run scripts/i18n.py update to rebuild its MO file.'
    catalog.wrapwidth = 0
    return catalog


class Markup(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.tags = Counter()
        self.stack = []
        self.valid = True

    def handle_starttag(self, tag, attrs):
        self.tags[(tag, tuple(sorted(attrs)))] += 1
        if tag not in {'br', 'hr', 'img', 'input', 'wbr'}:
            self.stack.append(tag)

    def handle_endtag(self, tag):
        if not self.stack or self.stack.pop() != tag:
            self.valid = False


def validate(catalog):
    errors = []
    for entry in catalog:
        if not entry.translated():
            errors.append(f'Missing or fuzzy translation: {entry.msgid}')
            continue
        for value in list(entry.msgstr_plural.values()) if entry.msgid_plural else [entry.msgstr]:
            source, translated = Markup(), Markup()
            source.feed(entry.msgid)
            translated.feed(value)
            if source.tags != translated.tags or not translated.valid or translated.stack:
                errors.append(f'HTML differs: {entry.msgid}')
    return errors


def run(mode, locales):
    template = extract()
    errors = []
    pot = LANGUAGES / f'{DOMAIN}.pot'
    if mode == 'update':
        template.save(str(pot))
        for locale in locales:
            path = LANGUAGES / f'{DOMAIN}-{locale}.po'
            if not path.exists():
                polib.POFile().save(str(path))
    elif not pot.exists() or pot.read_text(encoding='utf-8') != str(template):
        errors.append('POT is stale. Run scripts/i18n.py update.')
    for path in sorted(LANGUAGES.glob(f'{DOMAIN}-*.po')):
        locale = path.stem[len(DOMAIN) + 1:]
        catalog = polib.pofile(str(path), wrapwidth=0)
        if mode == 'update':
            synchronize(catalog, template, locale).save(str(path))
            catalog.save_as_mofile(str(path.with_suffix('.mo')))
        else:
            # Completeness and generated-file freshness are independent checks.
            errors.extend(f'{locale}: {error}' for error in validate(catalog))
            expected = synchronize(polib.pofile(str(path), wrapwidth=0), template, locale)
            if str(expected) != path.read_text(encoding='utf-8'):
                errors.append(f'{locale}: PO is stale. Run scripts/i18n.py update.')
            mo = path.with_suffix('.mo')
            if not mo.exists() or mo.read_bytes() != expected.to_binary():
                errors.append(f'{locale}: MO is stale. Run scripts/i18n.py update.')
            result = subprocess.run(['msgfmt', '--check', '--check-format', '-o', '/dev/null', str(path)], capture_output=True, text=True)
            if result.returncode:
                errors.append(f'{locale}: {result.stderr.strip()}')
    if errors:
        print('\n'.join(errors), file=sys.stderr)
        return 1
    print(f'Translations {mode}: {len(template)} messages, {len(list(LANGUAGES.glob("*.po")))} locales.')
    return 0


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('mode', choices=['update', 'check'])
    parser.add_argument('--add-locale', action='append', default=[], choices=sorted(PLURALS))
    args = parser.parse_args()
    try:
        sys.exit(run(args.mode, args.add_locale))
    except (OSError, subprocess.CalledProcessError, KeyError) as error:
        parser.exit(1, f'{error}\nInstall GNU gettext and scripts/i18n-requirements.txt before running this command.\n')
