"""Compile locale/<lang>/LC_MESSAGES/django.po into the .mo Django reads.

    python3 tools/compile_messages.py [locale_dir]

This exists because neither the build container nor the cPanel host has
GNU gettext installed, so `django-admin compilemessages` (which shells out to
msgfmt) cannot run. The .mo container format is simple and stable, so it is
written here directly - no dependency, and the same script runs in both places.

Format, for anyone who has to touch this later:

    magic 0x950412de, revision, count, offset of the msgid table,
    offset of the msgstr table, hash table size, hash table offset,
    then two tables of (length, offset) pairs, then the string data.

The hash table is optional; Python's gettext module reads every entry into a
dict regardless, so it is left empty. Entries are sorted by msgid because the
format says they must be, not because anything here depends on it.
"""

import os
import re
import struct
import sys

MAGIC = 0x950412DE


def unescape(s):
    out, i = [], 0
    simple = {'n': '\n', 't': '\t', 'r': '\r', '"': '"', '\\': '\\', 'a': '\a', 'b': '\b', 'f': '\f', 'v': '\v'}
    while i < len(s):
        c = s[i]
        if c == '\\' and i + 1 < len(s):
            nxt = s[i + 1]
            if nxt in simple:
                out.append(simple[nxt]); i += 2; continue
        out.append(c); i += 1
    return ''.join(out)


STR_RE = re.compile(r'"((?:[^"\\]|\\.)*)"')


def parse_po(path):
    """Return {msgid: msgstr}. Plurals become msgid\\x00msgid_plural keys."""
    entries, cur, key = {}, {}, None
    def flush():
        if not cur:
            return
        msgid = cur.get('msgid', '')
        plural = cur.get('msgid_plural')
        if plural is not None:
            forms = [cur[k] for k in sorted(cur) if k.startswith('msgstr[')]
            if any(forms):
                entries[msgid + '\x00' + plural] = '\x00'.join(forms)
        else:
            # An empty msgstr means "not translated" - gettext falls back to
            # the source string, which is exactly what we want while a
            # translation is still in progress. Storing "" would blank the page.
            if cur.get('msgstr'):
                entries[msgid] = cur['msgstr']
        cur.clear()

    with open(path, encoding='utf-8') as fh:
        for raw in fh:
            line = raw.strip()
            if not line:
                flush(); key = None; continue
            if line.startswith('#'):
                continue
            m = re.match(r'^(msgid_plural|msgid|msgstr\[\d+\]|msgstr)\s+(.*)$', line)
            if m:
                key = m.group(1)
                sm = STR_RE.match(m.group(2))
                cur[key] = unescape(sm.group(1)) if sm else ''
                continue
            sm = STR_RE.match(line)
            if sm and key:
                cur[key] += unescape(sm.group(1))
    flush()
    return entries


def write_mo(entries, path):
    items = sorted(entries.items())
    ids = b''
    strs = b''
    id_tab, str_tab = [], []
    for k, v in items:
        kb, vb = k.encode('utf-8'), v.encode('utf-8')
        id_tab.append((len(kb), len(ids)))
        ids += kb + b'\x00'
        str_tab.append((len(vb), len(strs)))
        strs += vb + b'\x00'

    n = len(items)
    keystart = 7 * 4 + 16 * n            # header + both tables
    valuestart = keystart + len(ids)
    out = struct.pack('<Iiiiiii', MAGIC, 0, n, 7 * 4, 7 * 4 + 8 * n, 0, 0)
    out += b''.join(struct.pack('<ii', l, o + keystart) for l, o in id_tab)
    out += b''.join(struct.pack('<ii', l, o + valuestart) for l, o in str_tab)
    out += ids + strs
    with open(path, 'wb') as fh:
        fh.write(out)
    return n


def main(base):
    total = 0
    for lang in sorted(os.listdir(base)):
        d = os.path.join(base, lang, 'LC_MESSAGES')
        po = os.path.join(d, 'django.po')
        if not os.path.isfile(po):
            continue
        entries = parse_po(po)
        if '' not in entries:
            raise SystemExit('%s has no metadata entry (msgid "") - Django needs '
                             'its charset header' % po)
        n = write_mo(entries, os.path.join(d, 'django.mo'))
        translated = sum(1 for k, v in entries.items() if k and v)
        print('%s: %d entries, %d translated -> django.mo' % (lang, n, translated))
        total += translated
    print('total translated strings: %d' % total)


if __name__ == '__main__':
    main(sys.argv[1] if len(sys.argv) > 1 else
         os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'locale'))
