# This file is part of Indico.
# Copyright (C) 2002 - 2021 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

import json
import os
import re
import subprocess
import sys
from distutils.dist import Distribution
from functools import wraps
from itertools import chain
from pkgutil import walk_packages

import click
from babel.messages import frontend
from babel.messages.pofile import read_po
from flask.helpers import get_root_path

from indico.util.console import cformat


@click.group()
def cli():
    os.chdir(os.path.join(get_root_path('indico'), '..'))


TRANSLATIONS_DIR = 'indico/translations'
MESSAGES_POT = os.path.join(TRANSLATIONS_DIR, 'messages.pot')
MESSAGES_JS_POT = os.path.join(TRANSLATIONS_DIR, 'messages-js.pot')
MESSAGES_REACT_POT = os.path.join(TRANSLATIONS_DIR, 'messages-react.pot')

DEFAULT_OPTIONS = {
    'init_catalog': {
        'output_dir': TRANSLATIONS_DIR
    },
    'extract_messages': {
        'keywords': 'N_:1,2',
        'width': '120',
        'output_file': MESSAGES_POT,
        'mapping_file': 'babel.cfg'
    },
    'compile_catalog': {
        'domain': 'messages',
        'directory': TRANSLATIONS_DIR
    },
    'update_catalog': {
        'input_file': MESSAGES_POT,
        'output_dir': TRANSLATIONS_DIR,
        'domain': 'messages'
    },

    # JavaScript
    'init_catalog_js': {
        'output_dir': TRANSLATIONS_DIR,
        'domain': 'messages-js'
    },
    'extract_messages_js': {
        'keywords': ['$T', 'gettext', 'ngettext:1,2', 'pgettext:1c,2', 'npgettext:1c,2,3'],
        'width': '120',
        'output_file': MESSAGES_JS_POT,
        'mapping_file': 'babel-js.cfg',
        'no_default_keywords': '1'
    },
    'update_catalog_js': {
        'input_file': MESSAGES_JS_POT,
        'output_dir': TRANSLATIONS_DIR,
        'domain': 'messages-js'
    },

    # JavaScript / React
    'init_catalog_react': {
        'output_dir': TRANSLATIONS_DIR,
        'domain': 'messages-react'
    },
    'update_catalog_react': {
        'input_file': MESSAGES_REACT_POT,
        'output_dir': TRANSLATIONS_DIR,
        'domain': 'messages-react'
    },
}


def find_packages(path, prefix=""):
    yield prefix
    prefix = prefix + "."
    for _, name, ispkg in walk_packages(path, prefix):
        if ispkg:
            yield name


def wrap_distutils_command(command_class):
    @wraps(command_class)
    def _wrapper(**kwargs):

        import indico

        command = command_class(Distribution({
            'name': 'indico',
            'version': indico.__version__,
            'packages': find_packages(indico.__path__, indico.__name__)
        }))

        for key, val in kwargs.items():
            setattr(command, key, val)

        command.finalize_options()
        command.run()

    return _wrapper


def _make_command(cmd_name):
    cmd_class = getattr(frontend, re.sub(r'_(js|react)$', '', cmd_name))
    cmd = click.command(cmd_name.replace('_', '-'))(wrap_distutils_command(cmd_class))
    for opt, short_opt, description in cmd_class.user_options:
        long_opt_name = opt.rstrip('=')
        var_name = long_opt_name.replace('-', '_')
        opts = ['--' + long_opt_name]

        if short_opt:
            opts.append('-' + short_opt)

        default = DEFAULT_OPTIONS.get(cmd_name, {}).get(var_name)
        is_flag = not opt.endswith('=')
        cmd = click.option(*(opts + [var_name]), is_flag=is_flag, default=default, help=description)(cmd)
    return cmd


cmd_list = ['init_catalog', 'extract_messages', 'update_catalog', 'compile_catalog',
            'init_catalog_js', 'extract_messages_js', 'update_catalog_js',
            'init_catalog_react', 'update_catalog_react']


for cmd_name in cmd_list:
    cli.add_command(_make_command(cmd_name))


@cli.command()
def extract_messages_react():
    output = subprocess.check_output(['npx', 'react-jsx-i18n', 'extract', 'indico/web/client/', 'indico/modules/'],
                                     env=dict(os.environ, FORCE_COLOR='1'))
    with open(MESSAGES_REACT_POT, 'wb') as f:
        f.write(output)


@cli.command()
def compile_catalog_react():
    for locale in os.listdir(TRANSLATIONS_DIR):
        po_file = os.path.join(TRANSLATIONS_DIR, locale, 'LC_MESSAGES', 'messages-react.po')
        json_file = os.path.join(TRANSLATIONS_DIR, locale, 'LC_MESSAGES', 'messages-react.json')
        if not os.path.exists(po_file):
            continue
        output = subprocess.check_output(['npx', 'react-jsx-i18n', 'compile', po_file])
        json.loads(output)  # just to be sure the JSON is valid
        with open(json_file, 'wb') as f:
            f.write(output)


@cli.command()
def check_format_strings():
    """Check whether format strings match.

    This helps finding cases where e.g. the original string uses
    ``{error}`` but the translation uses ``{erro}``, resulting
    in errors when using the translated string.
    """
    root_path = 'indico/translations'
    paths = set()
    for root, dirs, files in os.walk(root_path):
        for file in files:
            if file.endswith('.po'):
                paths.add(os.path.join(root, file))
    all_valid = True
    for path in paths:
        invalid = _get_invalid_po_format_strings(path)
        if invalid:
            all_valid = False
            click.echo(f'Found invalid format strings in {os.path.relpath(path, root_path)}')
            for item in invalid:
                click.echo(cformat('%{yellow}{}%{reset} | %{yellow!}{}%{reset}\n%{red}{}%{reset} != %{red!}{}%{reset}')
                           .format(item['orig'], item['trans'],
                                   list(item['orig_placeholders']), list(item['trans_placeholders'])))
            click.echo()
    sys.exit(0 if all_valid else 1)


def _extract_placeholders(string):
    return set(re.findall(r'(\{[^}]+\})', string))


def _get_invalid_po_format_strings(path):
    with open(path, 'rb') as f:
        po_data = read_po(f)
    invalid = []
    for msg in po_data:
        all_orig = msg.id if isinstance(msg.id, tuple) else (msg.id,)
        all_trans = msg.string if isinstance(msg.string, tuple) else (msg.string,)
        if not any(all_trans):  # not translated
            continue
        for orig, trans in zip(all_orig, all_trans):
            # brace format only; python-format (%s etc) is too vague
            # since there are many strings containing e.g. just `%`
            # which are never used for formatting, and babel's
            # `_validate_format` checker fails on those too
            orig_placeholders = _extract_placeholders(orig)
            # in some cases the english singular doesn't use the placeholder but rather e.g. "One".
            # but depending on the language (usually with nplurals=1) the singular version MUST include
            # the placeholder, so we need to consider those as well
            orig_plural_placeholders = set(chain.from_iterable(map(_extract_placeholders, all_orig[1:])))
            trans_placeholders = _extract_placeholders(trans)
            if trans_placeholders not in (orig_placeholders, orig_plural_placeholders):
                invalid.append({
                    'orig': orig,
                    'trans': trans,
                    'orig_placeholders': orig_placeholders,
                    'trans_placeholders': trans_placeholders
                })
    return invalid
