#!/usr/bin/env python3
"""Translate a word using Wikipedia's cross-wiki links"""

# pylint: disable=line-too-long


__version__ = "3.0.0"


from argparse import ArgumentParser
import gettext
import json
import locale
import os
import re
import sys
import urllib.parse
import urllib.request


# set up gettext for i18n
_t = gettext.translation('wptr',
                         localedir=os.path.join(
                             os.path.dirname(os.path.dirname(__file__)),
                             "locale"),
                         fallback=True,
                         languages=[locale.getdefaultlocale()[0]])
_ = _t.gettext


# different possible output formats
formats = {
    "plain": {
        "start": _("Translations for \u201C{term}\u201D ({lang})\n\n"),
        "end": _("\nSource: {url}\n"),
        "line": _("{lang} ({nlang}, {iso}):{filler} {item}\n"),
        "error": _("Couldn\u2019t find a translation for \u201C{term}\u201D ({lang})\n"),
    },
    "console": {
        "start": _("\u001B[33mTranslations for \u201C\u001B[32m{term}\u001B[33m\u201D ({lang})\u001B[0m\n\n"),
        "end": _("\n\u001B[38;5;241mSource: {url}\u001B[0m\n"),
        "line": _("{lang} \u001B[33m({nlang}, {iso})\u001B[0m:{filler} \u001B[32m{item}\u001B[0m\n"),
        "error": _("\u001B[31mCouldn\u2019t find a translation for \u201C{term}\u201D ({lang})\u001B[0m\n"),
    },
    "csv": {
        "start": ('"' +
                  _('Language') + '","' +
                  _('ISO code') + '","' +
                  _('Translation') + '"\n"' +
                  _('source') + '","{lang}","{term}"\n'),
        "end": '',
        "line": '"{lang}","{iso}","{item}"\n',
        "error": _("Couldn\u2019t find a translation for \u201C{term}\u201D ({lang}),\n"),
    },
    "short": {
        "start": "",
        "end": "({url})\n",
        "line": "{iso} {item}\n",
        "error": "{code}\n",
    },
    "html": {
        "start": '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' +
                 _('Translations of {term}')+'</title></head><body><h1>' +
                 _('Translations of {term}')+'</h1><p>' +
                 (_('Source: %s') % '<a href="{url}">{url}</a>') + '</p><dl>',
        "end": "</dl></body></html>",
        "line": '<dt>{lang} ({nlang}, {iso})</dt><dd><a href="{url}">{item}</a></dd>',
        "error": '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' +
                 _('Translations of {term}')+'</title></head><body><p>' +
                 _("Couldn\u2019t find a translation for \u201C{term}\u201D ({lang}),\n") +
                "</p></body></html>",
    },
    "json": {
        "start": '{{"src":"{url}","term":"{term}","lang":"{lang}","translations":{{',
        "end": '}}}}',
        "line": '"{iso}":"{item}",',
        "last-line": '"{iso}":"{item}"',
        "error": 'null',
    },
}


def get_sparql(term, lang, to):
    """get the SPARQL query that is used to fetch the data"""

    to_filter = ''
    if to:
        to_langs = ','.join([f"'{to_lang}'" for to_lang in to])
        to_filter = f'\n  FILTER(?lang in ({to_langs})) .'

    return f'''
SELECT DISTINCT ?lang ?langItemLabel ?langNativeLabel ?name WHERE {{

  ?sourceArticle schema:about ?item;
                 schema:isPartOf [ wikibase:wikiGroup "wikipedia" ];
                 schema:name "{term}"@{lang}.

  ?targetArticle schema:about ?item;
                 schema:inLanguage ?lang;
                 schema:name ?name;
                 schema:isPartOf [ wikibase:wikiGroup "wikipedia" ].

  FILTER(?lang not in ('{lang}')) .{to_filter}
  FILTER (!CONTAINS(?name, ':')) .

  ?langItem wdt:P31 wd:Q34770 ; wdt:P424 ?lang ; wdt:P1705 ?langNativeLabel .

  SERVICE wikibase:label {{ bd:serviceParam wikibase:language "{lang}". }}
}}
ORDER BY ?lang
                       '''


def get_json(term, lang, to):
    """Fetch the information from Wikipedia and return the plain markup"""

    headers = {
        "User-Agent": "Mozilla/5.0 wptr like Gecko, Firefox"
    }
    req = urllib.request.Request('https://query.wikidata.org/sparql?format=json&query=' +
                                 urllib.parse.quote(get_sparql(term, lang, to)),
                                 headers=headers)
    response = urllib.request.urlopen(req)

    if response.status != 200:
        raise ValueError(response.status)

    return json.loads(response.read().decode('UTF-8'))['results']['bindings']


def get_url(term, lang="en"):
    """return Wikipedia URL for term"""
    return f'https://{lang}.wikipedia.org/wiki/{urllib.parse.quote(term)}'


def get_translations(term, lang, to):
    """get a list of translations for a term"""

    try:
        data = get_json(term, lang, to)
    except (KeyError, ValueError) as err:
        return err

    ret = {}
    for item in data:
        iso = item["lang"]["value"]
        title = item["name"]["value"]
        url = f'https://{iso}.wikipedia.org/wiki/{urllib.parse.quote(title)}'
        ret[iso] = {
            "iso": iso,
            "item": title,
            "nlang": item["langItemLabel"]["value"],
            "url": url,
            "lang": item["langNativeLabel"]["value"],
        }

    return ret.values()


def format_translations(term, data, fmt="plain", lang="en"):
    """format the translation data"""

    if fmt not in formats:
        # double-check. For CLI this is controlled by Optparse already
        raise KeyError(f"Format {fmt} is unknown")

    if isinstance(data, (KeyError, ValueError)):
        ret = formats[fmt]["error"].format(code=data.message, term=term, lang=lang)

    else:
        ret = formats[fmt]["start"].format(term=term, lang=lang, url=get_url(term, lang))

        # determine the label length for console output
        maxlabel = 0
        for trans in data:
            length = len(trans["iso"]) + len(trans["lang"]) + len(trans["nlang"])
            if length > maxlabel:
                maxlabel = length

        # main data loop
        len_data = len(data) - 1
        for i, trans in enumerate(data):
            length = len(trans["iso"]) + len(trans["lang"]) + len(trans["nlang"])
            trans["filler"] = " " * (maxlabel - length)
            trans["i"] = i+1
            cur = formats[fmt]["line"]
            if i == 0 and "first-line" in formats[fmt]:
                cur = formats[fmt]["first-line"]
            elif i == len_data and "last-line" in formats[fmt]:
                cur = formats[fmt]["last-line"]
            ret += cur.format(**trans)

        ret += formats[fmt]["end"].format(term=term, lang=lang, url=get_url(term, lang))

    return ret


def cli():
    """CLI interface"""

    fmt = "plain"
    if sys.stdout.isatty():
        fmt = "console"

    lang = re.sub('[^a-zA-Z].*', '', locale.getdefaultlocale()[0]).lower()
    if not lang or lang == "C":
        lang = "en"

    parser = ArgumentParser(
        usage=_("%(prog)s [options] term"),
        description=_("Query Wikipedia for translations of a term.")
    )
    parser.add_argument(
        '--version',
        action='version',
        version=f"%(prog)s {__version__}"
    )
    parser.add_argument(
        "-l", "--language", "--from",
        action="store",
        dest="lang",
        default=lang,
        metavar=_("LANGUAGE"),
        help=_("set language the terms are in [default: %(default)s]. You can also prefix the search term with the language, e. g., \u2018en:Mirror\u2019"),
    )
    parser.add_argument(
        '-t', '--to',
        default=[],
        action='append',
        help=_('translate to this language. Can be used more than once or with a comma-separated list, e.g. --to de,fr,es'))
    parser.add_argument(
        "-f", "--format",
        action="store",
        default=fmt,
        dest="fmt",
        choices=list(formats.keys()),
        help=_("format output (possible values are: %s)") % _(", ").join(formats.keys()),
    )
    parser.set_defaults(verbose=True)
    parser.add_argument(
        "-q", "--quiet",
        action="store_false",
        dest="verbose",
        help=_("be quiet (short for --format=short)"),
    )
    parser.add_argument(
        "--color",
        default="auto",
        dest="color",
        choices=("auto", "always", "never"),
        metavar=_("WHEN"),
        help=_("colorize the output.  WHEN defaults to \u2018auto\u2019 or can be \u2018never\u2019 or \u2018always\u2019"),
    )
    parser.set_defaults(show_query=False)
    parser.add_argument(
        "--show-query",
        dest="show_query",
        action="store_true",
        help=_("print the SPARQL query that would be used and exit"),
    )
    parser.add_argument(
        'term',
        nargs='*',
        help=_("the term to translate")
    )

    opts = parser.parse_args()

    if not re.search(r'^[a-z]{2,3}$', opts.lang, re.I):
        parser.error(_("-l option requires a valid language identifier (like \u2018en\u2019 or \u2018de\u2019)."))

    if opts.fmt == "console" and opts.color == "never":
        opts.fmt = "plain"
    elif opts.fmt == "plain" and opts.color == "always":
        opts.fmt = "console"
    if not opts.verbose:
        opts.fmt = "short"

    if len(opts.term) < 1:
        parser.error(_("no argument given for translation."))

    to = set()
    if opts.to:
        for item in opts.to:
            to = to.union([toLang.strip() for toLang in item.split(',')])
        # safe-guard against translating to original language
        to.discard(opts.lang)

    if opts.show_query:
        sys.stdout.write(get_sparql(' '.join(opts.term), opts.lang, to)+'\n')
        sys.exit(0)

    sys.stdout.write(
        format_translations(
            ' '.join(opts.term),
            get_translations(' '.join(opts.term), opts.lang, to),
            opts.fmt,
            opts.lang
        ))


if __name__ == '__main__':
    cli()
