"""
Borealis, a Nintendo Switch UI Library
Copyright (C) 2020  natinusala

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
# Run with Python 3

import argparse
import json
import re
from jsonpointer import resolve_pointer, JsonPointerException
from pathlib import Path

# All locales supported by HOS
_SUPPORTED_LOCALES = [
    "ja",
    "en-US",
    "en-GB",
    "fr",
    "fr-CA",
    "de",
    "it",
    "es",
    "zh-CN",
    "zh-Hans",
    "zh-Hant",
    "zh-TW",
    "ko",
    "nl",
    "pt",
    "pt-BR",
    "ru",
    "es-419",
]

# The default locale used by brls
_DEFAULT_LOCALE = "en-US"

# All illegal chars in the string keys
# Mostly jsonpointer reserved chars
_ILLEGAL_KEYS_CHARS = [
    "/",
    "~",
    " ",
    "#",
    "$",
]

# All locales and their strings, filled by _check_locales
# {locale -> {key -> string}}
_LOCALES_CACHE = {}


def _folder_exists(path: Path, errors: list, warnings: list) -> tuple:
    """Checks that the i18n folder exists and is a folder"""
    if not path.exists():
        errors.append((1, f"Cannot continue with the checks: folder \"{path}\" doesn't exist"))
    elif not path.is_dir():
        errors.append((2, f"Cannot continue with the checks: file \"{path}\" is not a folder"))


def _ensure_default_locale(path: Path, errors: list, warnings: list) -> tuple:
    """Ensures that the default locale exists"""
    defaultlocalefile = path / f"{_DEFAULT_LOCALE}"
    if not defaultlocalefile.exists() or not defaultlocalefile.is_dir():
        warnings.append((6, f"Default locale {_DEFAULT_LOCALE} is missing from the i18n folder"))


def _check_locales(path: Path, errors: list, warnings: list) -> tuple:
    """
    Checks that the i18n only contains known locales
    and loads them for subsequent checks
    """
    for f in path.iterdir():
        # Load locale
        if f.is_dir():
            # Known locale
            if f.name not in _SUPPORTED_LOCALES:
                warnings.append((3, f"Unknown locale for folder \"{f.name}\""))
                continue
            #Load all JSON files inside
            for ff in f.iterdir():
                # Directory
                if ff.is_dir():
                    warnings.append((1, f"{f.name} folder contains stray folder \"{ff.name}\""))
                # Known format
                elif not ff.name.endswith(".json"):
                    warnings.append((2, f"{f.name} folder contains stray file \"{ff.name}\""))
                # Load it
                else:
                    with open(ff, "r") as jsonf:
                        _LOCALES_CACHE.setdefault(f.name, {})

                        try:
                            _LOCALES_CACHE[f.name][ff.name[:-5]] = json.loads(jsonf.read())
                        except json.JSONDecodeError as e:
                            errors.append((5, f"Cannot parse JSON file \"{f.name}/{ff.name}\": {e}"))
                            return  # don't bother continuing
        # File
        else:
            warnings.append((2, f"i18n folder contains stray file \"{f.name}\""))

    if _DEFAULT_LOCALE not in _LOCALES_CACHE:
        _LOCALES_CACHE[_DEFAULT_LOCALE] = {}

def _check_types(path: Path, errors: list, warnings: list) -> tuple:
    """Checks that locales only contain valid data"""
    def _check_node(breadcrumb: str, key: str, value: dict, locale: str):
        # Illegal chars in key
        for char in _ILLEGAL_KEYS_CHARS:
            if char in key:
                errors.append((6, f"String \"{breadcrumb}\" of {locale} locale contains illegal character \"{char}\" in its name"))

        # Dict
        if isinstance(value, dict):
            for nested_key in value:
                new_breadcrumb = f"{breadcrumb}/{nested_key}" if breadcrumb else nested_key
                _check_node(new_breadcrumb, nested_key, value[nested_key], locale)
        # Not strings
        elif not isinstance(value, str):
            errors.append((7, f"String \"{breadcrumb}\" of {locale} locale contains data \"{str(value)}\" of invalid type \"{type(value).__name__}\""))

    for locale in _LOCALES_CACHE:
        _check_node("", "", _LOCALES_CACHE[locale], locale)


def _check_untranslated_strings(path: Path, errors: list, warnings: list) -> tuple:
    """
    Ensure there are no untranslated strings
    """
    def _check_node(breadcrumb: str, value: dict):
        for nested_key in value:
            if breadcrumb:
                base = f"{breadcrumb}/{nested_key}"
            else:
                base = nested_key

            # Dict
            if isinstance(value[nested_key], dict):
                _check_node(base, value[nested_key])

            # Str
            else:
                for locale in _LOCALES_CACHE:
                    if locale == _DEFAULT_LOCALE:
                        continue

                    pointer = f"/{base}"
                    try:
                        resolve_pointer(_LOCALES_CACHE[locale], pointer)
                    except JsonPointerException:
                        warnings.append((4, f"Locale {locale} is missing string \"{base}\" (untranslated from {_DEFAULT_LOCALE})"))

    _check_node("", _LOCALES_CACHE[_DEFAULT_LOCALE])

def _check_unknown_translations(path: Path, errors: list, warnings: list) -> tuple:
    """Ensures all strings from all translations are in default locale"""
    default_locale = _LOCALES_CACHE[_DEFAULT_LOCALE]

    def _check_node(locale: str, breadcrumb: str, value: dict):
        # Dict
        if isinstance(value, dict):
            for nested_key in value:
                if breadcrumb:
                    base = f"{breadcrumb}/{nested_key}"
                else:
                    base = nested_key

                _check_node(locale, base, value[nested_key])

        # Str
        elif isinstance(value, str):
            pointer = f"/{breadcrumb}"

            try:
                resolve_pointer(default_locale, pointer)
            except JsonPointerException:
                warnings.append((5, f"String \"{breadcrumb}\" is translated in locale {locale} but is missing from default locale {_DEFAULT_LOCALE} (translation of unknown string)"))

    for locale in _LOCALES_CACHE:
        if locale == _DEFAULT_LOCALE:
            continue
        _check_node(locale, "", _LOCALES_CACHE[locale])

if __name__ == "__main__":
    # Arguments parsing
    parser = argparse.ArgumentParser(description="Check integrity of i18n strings")

    parser.add_argument(
        dest="path",
        action="store",
        help="The path to the i18n folder to check",
    )

    args = parser.parse_args()

    path = Path(args.path)

    print(f"Checking i18n folder {path}...\n")

    # Validation
    checks = [
        _folder_exists,
        _ensure_default_locale,
        _check_locales,
        _check_types,
        _check_untranslated_strings,
        _check_unknown_translations,
    ]

    errors = []
    warnings = []

    for check in checks:
        check(path, errors, warnings)

        if errors:
            break  # errors are fatal

    if warnings:
        print(f"{len(warnings)} warning(s):")

        for code, warning in warnings:
            print(f"     - W{code:02}: {warning}")

        print("\nWarnings are not fatal but should be fixed to avoid missing / broken translations in the app.")

    if errors:
        print(f"{len(errors)} error(s):")

        for code, error in errors:
            print(f"     - E{code:02}: {error}")

        print("\nPlease fix them and run the script again.")

    if not errors and not warnings:
        print("No errors or warnings detected, your i18n folder is good to go!")
