/*  This file is part of the KDE libraries    Copyright (C) 2007 Chusslove Illich <caslav.ilic@gmx.net>

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

    This library 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
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.
*/

#include <ktranscript_p.h>
#include <common_helpers_p.h>

#include <config.h>

#include <kdecore_export.h>
#include <kglobal.h>

//#include <unistd.h>

#include <kjs/value.h>
#include <kjs/object.h>
#include <kjs/lookup.h>
#include <kjs/function.h>
#include <kjs/interpreter.h>
#include <kjs/string_object.h>
#include <kjs/error_object.h>

#include <QVariant>
#include <QStringList>
#include <QList>
#include <QHash>
#include <QFile>
#include <QIODevice>
#include <QTextStream>
#include <QRegExp>
#include <qendian.h>

using namespace KJS;

class KTranscriptImp;
class Scriptface;

typedef QHash<QString, QString> TsConfigGroup;
typedef QHash<QString, TsConfigGroup> TsConfig;

// Transcript implementation (used as singleton).
class KTranscriptImp : public KTranscript
{
    public:

    KTranscriptImp ();
    ~KTranscriptImp ();

    QString eval (const QList<QVariant> &argv,
                  const QString &lang,
                  const QString &lscr,
                  const QString &msgctxt,
                  const QHash<QString, QString> &dynctxt,
                  const QString &msgid,
                  const QStringList &subs,
                  const QList<QVariant> &vals,
                  const QString &final,
                  QList<QStringList> &mods,
                  QString &error,
                  bool &fallback);

    QStringList postCalls (const QString &lang);

    // Lexical path of the module for the executing code.
    QString currentModulePath;

    private:

    void loadModules (const QList<QStringList> &mods, QString &error);
    void setupInterpreter (const QString &lang);

    TsConfig config;

    QHash<QString, Scriptface*> m_sface;
};

// Script-side transcript interface.
class Scriptface : public JSObject
{
    public:
    Scriptface (ExecState *exec, const TsConfigGroup &config);
    ~Scriptface ();

    // Interface functions.
    JSValue *loadf (ExecState *exec, const List &fnames);
    JSValue *setcallf (ExecState *exec, JSValue *name,
                       JSValue *func, JSValue *fval);
    JSValue *hascallf (ExecState *exec, JSValue *name);
    JSValue *acallf (ExecState *exec, const List &argv);
    JSValue *setcallForallf (ExecState *exec, JSValue *name,
                             JSValue *func, JSValue *fval);
    JSValue *fallbackf (ExecState *exec);
    JSValue *nsubsf (ExecState *exec);
    JSValue *subsf (ExecState *exec, JSValue *index);
    JSValue *valsf (ExecState *exec, JSValue *index);
    JSValue *msgctxtf (ExecState *exec);
    JSValue *dynctxtf (ExecState *exec, JSValue *key);
    JSValue *msgidf (ExecState *exec);
    JSValue *msgkeyf (ExecState *exec);
    JSValue *msgstrff (ExecState *exec);
    JSValue *dbgputsf (ExecState *exec, JSValue *str);
    JSValue *lscrf (ExecState *exec);
    JSValue *normKeyf (ExecState *exec, JSValue *phrase);
    JSValue *loadPropsf (ExecState *exec, const List &fnames);
    JSValue *getPropf (ExecState *exec, JSValue *phrase, JSValue *prop);
    JSValue *setPropf (ExecState *exec, JSValue *phrase, JSValue *prop, JSValue *value);
    JSValue *toUpperFirstf (ExecState *exec, JSValue *str, JSValue *nalt);
    JSValue *toLowerFirstf (ExecState *exec, JSValue *str, JSValue *nalt);
    JSValue *getConfStringf (ExecState *exec, JSValue *key, JSValue *dval);
    JSValue *getConfBoolf (ExecState *exec, JSValue *key, JSValue *dval);
    JSValue *getConfNumberf (ExecState *exec, JSValue *key, JSValue *dval);

    enum {
        Load,
        Setcall,
        Hascall,
        Acall,
        SetcallForall,
        Fallback,
        Nsubs,
        Subs,
        Vals,
        Msgctxt,
        Dynctxt,
        Msgid,
        Msgkey,
        Msgstrf,
        Dbgputs,
        Lscr,
        NormKey,
        LoadProps,
        GetProp,
        SetProp,
        ToUpperFirst,
        ToLowerFirst,
        GetConfString,
        GetConfBool,
        GetConfNumber
    };

    // Helper methods to interface functions.
    QString loadProps_bin (const QString &fpath);
    QString loadProps_text (const QString &fpath);

    // Virtual implementations.
    bool getOwnPropertySlot (ExecState *exec, const Identifier& propertyName, PropertySlot& slot);
    JSValue *getValueProperty (ExecState *exec, int token) const;
    void put (ExecState *exec, const Identifier &propertyName, JSValue *value, int attr);
    void putValueProperty (ExecState *exec, int token, JSValue *value, int attr);
    const ClassInfo* classInfo() const { return &info; }

    static const ClassInfo info;

    // Link to its interpreter.
    // FIXME: Probably accessible without the explicit link.
    Interpreter *jsi;

    // Current message data.
    const QString *msgctxt;
    const QHash<QString, QString> *dynctxt;
    const QString *msgid;
    const QStringList *subs;
    const QList<QVariant> *vals;
    const QString *final;
    const QString *lscr;

    // Fallback request handle.
    bool *fallback;

    // Function register.
    QHash<QString, JSObject*> funcs;
    QHash<QString, JSValue*> fvals;
    QHash<QString, QString> fpaths;

    // Ordering of those functions which execute for all messages.
    QList<QString> nameForalls;

    // Property values per phrase (used by *Prop interface calls).
    // Not QStrings, in order to avoid conversion from UTF-8 when
    // loading compiled maps (less latency on startup).
    QHash<QByteArray, QHash<QByteArray, QByteArray> > phraseProps;

    // User config.
    TsConfigGroup config;
};

// ----------------------------------------------------------------------
// Custom debug output (kdebug not available)
#define DBGP "KTranscript: "
void dbgout (const QString &str) {
    #ifndef NDEBUG
    fprintf(stderr, DBGP"%s\n", str.toLocal8Bit().data());
    #else
    Q_UNUSED(str);
    #endif
}
template <typename T1>
void dbgout (const QString &str, const T1 &a1) {
    #ifndef NDEBUG
    fprintf(stderr, DBGP"%s\n", str.arg(a1).toLocal8Bit().data());
    #else
    Q_UNUSED(str); Q_UNUSED(a1);
    #endif
}
template <typename T1, typename T2>
void dbgout (const QString &str, const T1 &a1, const T2 &a2) {
    #ifndef NDEBUG
    fprintf(stderr, DBGP"%s\n", str.arg(a1).arg(a2).toLocal8Bit().data());
    #else
    Q_UNUSED(str); Q_UNUSED(a1); Q_UNUSED(a2);
    #endif
}
template <typename T1, typename T2, typename T3>
void dbgout (const QString &str, const T1 &a1, const T2 &a2, const T3 &a3) {
    #ifndef NDEBUG
    fprintf(stderr, DBGP"%s\n", str.arg(a1).arg(a2).arg(a3).toLocal8Bit().data());
    #else
    Q_UNUSED(str); Q_UNUSED(a1); Q_UNUSED(a2); Q_UNUSED(a3);
    #endif
}

// ----------------------------------------------------------------------
// Conversions between QString and KJS UString.
// Taken from kate.
UString::UString(const QString &d)
{
    unsigned int len = d.length();
    UChar *dat = static_cast<UChar*>(fastMalloc(sizeof(UChar) * len));
    memcpy(dat, d.unicode(), len * sizeof(UChar));
    m_rep = UString::Rep::create(dat, len);
}
QString UString::qstring() const
{
    return QString((QChar*) data(), size());
}

// ----------------------------------------------------------------------
// Produces a string out of a KJS exception.
QString expt2str (ExecState *exec)
{
    JSValue *expt = exec->exception();
    if (   expt->isObject()
        && expt->getObject()->hasProperty(exec, "message"))
    {
        JSValue *msg = expt->getObject()->get(exec, "message");
        return QString("Error: %1").arg(msg->getString().qstring());
    }
    else
    {
        QString strexpt = exec->exception()->toString(exec).qstring();
        return QString("Caught exception: %1").arg(strexpt);
    }
}

// ----------------------------------------------------------------------
// Count number of lines in the string,
// up to and excluding the requested position.
int countLines (const QString &s, int p)
{
    int n = 1;
    int len = s.length();
    for (int i = 0; i < p && i < len; ++i) {
        if (s[i] == '\n') {
            ++n;
        }
    }
    return n;
}

// ----------------------------------------------------------------------
// Normalize string key for hash lookups,
QByteArray normKeystr (const QString &raw)
{
    // NOTE: Regexes should not be used here for performance reasons.
    // This function may potentially be called thousands of times
    // on application startup.

    QString key = raw;

    // Strip all whitespace.
    int len = key.length();
    QString nkey;
    for (int i = 0; i < len; ++i) {
        QChar c = key[i];
        if (!c.isSpace()) {
            nkey.append(c);
        }
    }
    key = nkey;

    // Strip accelerator marker.
    key = removeAcceleratorMarker(key);

    // Convert to lower case.
    key = key.toLower();

    return key.toUtf8();
}

// ----------------------------------------------------------------------
// Trim multiline string in a "smart" way:
// Remove leading and trailing whitespace up to and including first
// newline from that side, if there is one; otherwise, don't touch.
QString trimSmart (const QString &raw)
{
    // NOTE: This could be done by a single regex, but is not due to
    // performance reasons.
    // This function may potentially be called thousands of times
    // on application startup.

    int len = raw.length();

    int is = 0;
    while (is < len && raw[is].isSpace() && raw[is] != '\n') {
        ++is;
    }
    if (is >= len || raw[is] != '\n') {
        is = -1;
    }

    int ie = len - 1;
    while (ie >= 0 && raw[ie].isSpace() && raw[ie] != '\n') {
        --ie;
    }
    if (ie < 0 || raw[ie] != '\n') {
        ie = len;
    }

    return raw.mid(is + 1, ie - is - 1);
}

// ----------------------------------------------------------------------
// Produce a JavaScript object out of Qt variant.
JSValue *variantToJsValue (const QVariant &val)
{
    QVariant::Type vtype = val.type();
    if (vtype == QVariant::String)
        return jsString(val.toString());
    else if (   vtype == QVariant::Double \
             || vtype == QVariant::Int || vtype == QVariant::UInt \
             || vtype == QVariant::LongLong || vtype == QVariant::ULongLong)
        return jsNumber(val.toDouble());
    else
        return jsUndefined();
}

// ----------------------------------------------------------------------
// Parse ini-style config file,
// returning content as hash of hashes by group and key.
// Parsing is not fussy, it will read what it can.
TsConfig readConfig (const QString &fname)
{
    TsConfig config;
    // Add empty group.
    TsConfig::iterator configGroup;
    configGroup = config.insert(QString(), TsConfigGroup());

    QFile file(fname);
    if (!file.open(QIODevice::ReadOnly)) {
        return config;
    }
    QTextStream stream(&file);
    stream.setCodec("UTF-8");
    while (!stream.atEnd()) {
        QString line = stream.readLine();
        int p1, p2;

        // Remove comment from the line.
        p1 = line.indexOf('#');
        if (p1 >= 0) {
            line = line.left(p1);
        }
        line = line.trimmed();
        if (line.isEmpty()) {
            continue;
        }

        if (line[0] == '[') {
            // Group switch.
            p1 = 0;
            p2 = line.indexOf(']', p1 + 1);
            if (p2 < 0) {
                continue;
            }
            QString group = line.mid(p1 + 1, p2 - p1 - 1).trimmed();
            configGroup = config.find(group);
            if (configGroup == config.end()) {
                // Add new group.
                configGroup = config.insert(group, TsConfigGroup());
            }
        } else {
            // Field.
            p1 = line.indexOf('=');
            if (p1 < 0) {
                continue;
            }
            QString field = line.left(p1).trimmed();
            QString value = line.mid(p1 + 1).trimmed();
            if (!field.isEmpty()) {
                (*configGroup)[field] = value;
            }
        }
    }
    file.close();

    return config;
}

// ----------------------------------------------------------------------
// Dynamic loading.
K_GLOBAL_STATIC(KTranscriptImp, globalKTI)
extern "C"
{
    KDE_EXPORT KTranscript *load_transcript ()
    {
        return globalKTI;
    }
}

// ----------------------------------------------------------------------
// KTranscript definitions.

KTranscriptImp::KTranscriptImp ()
{
    // Load user configuration.
    QString homeDir = qgetenv("HOME");
    QString tsConfigFile = ".transcriptrc";
    QString tsConfigPath = homeDir + '/' + tsConfigFile;
    config = readConfig(tsConfigPath);
}

KTranscriptImp::~KTranscriptImp ()
{
    // FIXME: vallgrind shows an afwul lot of "invalid read" in WTF:: stuff
    // when deref is called... Are we leaking somewhere?
    //foreach (Scriptface *sface, m_sface.values())
    //    sface->jsi->deref();
}

QString KTranscriptImp::eval (const QList<QVariant> &argv,
                              const QString &lang,
                              const QString &lscr,
                              const QString &msgctxt,
                              const QHash<QString, QString> &dynctxt,
                              const QString &msgid,
                              const QStringList &subs,
                              const QList<QVariant> &vals,
                              const QString &final,
                              QList<QStringList> &mods,
                              QString &error,
                              bool &fallback)
{
    //error = "debug"; return QString();

    error.clear(); // empty error message means successful evaluation
    fallback = false; // fallback not requested

    #if 0
    // FIXME: Maybe not needed, as KJS has no native outside access?
    // Unportable (needs unistd.h)?

    // If effective user id is root and real user id is not root.
    if (geteuid() == 0 && getuid() != 0)
    {
        // Since scripts are user input, and the program is running with
        // root permissions while real user is not root, do not invoke
        // scripting at all, to prevent exploits.
        error = "Security block: trying to execute a script in suid environment.";
        return QString();
    }
    #endif

    // Load any new modules and clear the list.
    if (!mods.isEmpty())
    {
        loadModules(mods, error);
        mods.clear();
        if (!error.isEmpty())
            return QString();
    }

    // Add interpreters for new languages.
    // (though it should never happen here, but earlier when loading modules;
    // this also means there are no calls set, so the unregistered call error
    // below will be reported).
    if (!m_sface.contains(lang))
        setupInterpreter(lang);

    // Shortcuts.
    Scriptface *sface = m_sface[lang];
    ExecState *exec = sface->jsi->globalExec();
    JSObject *gobj = sface->jsi->globalObject();

    // Link current message data for script-side interface.
    sface->msgctxt = &msgctxt;
    sface->dynctxt = &dynctxt;
    sface->msgid = &msgid;
    sface->subs = &subs;
    sface->vals = &vals;
    sface->final = &final;
    sface->fallback = &fallback;
    sface->lscr = &lscr;

    // Find corresponding JS function.
    int argc = argv.size();
    if (argc < 1)
    {
        //error = "At least the call name must be supplied.";
        // Empty interpolation is OK, possibly used just to initialize
        // at a given point (e.g. for Ts.setForall() to start having effect).
        return QString();
    }
    QString funcName = argv[0].toString();
    if (!sface->funcs.contains(funcName))
    {
        error = QString("Unregistered call to '%1'.").arg(funcName);
        return QString();
    }
    JSObject *func = sface->funcs[funcName];
    JSValue *fval = sface->fvals[funcName];

    // Recover module path from the time of definition of this call,
    // for possible load calls.
    currentModulePath = sface->fpaths[funcName];

    // Execute function.
    List arglist;
    for (int i = 1; i < argc; ++i)
        arglist.append(variantToJsValue(argv[i]));
    JSValue *val;
    if (fval->isObject())
        val = func->callAsFunction(exec, fval->getObject(), arglist);
    else // no object associated to this function, use global
        val = func->callAsFunction(exec, gobj, arglist);

    if (fallback)
    // Fallback to ordinary translation requested.
    {
        // Possibly clear exception state.
        if (exec->hadException())
            exec->clearException();

        return QString();
    }
    else if (!exec->hadException())
    // Evaluation successful.
    {
        if (val->isString())
        // Good to go.
        {
            return val->getString().qstring();
        }
        else
        // Accept only strings.
        {
            QString strval = val->toString(exec).qstring();
            error = QString("Non-string return value: %1").arg(strval);
            return QString();
        }
    }
    else
    // Exception raised.
    {
        error = expt2str(exec);

        exec->clearException();

        return QString();
    }
}

QStringList KTranscriptImp::postCalls (const QString &lang)
{
    // Return no calls if scripting was not already set up for this language.
    // NOTE: This shouldn't happen, as postCalls cannot be called in such case.
    if (!m_sface.contains(lang))
        return QStringList();

    // Shortcuts.
    Scriptface *sface = m_sface[lang];

    return sface->nameForalls;
}

void KTranscriptImp::loadModules (const QList<QStringList> &mods,
                                  QString &error)
{
    QList<QString> modErrors;

    foreach (const QStringList &mod, mods)
    {
        QString mpath = mod[0];
        QString mlang = mod[1];

        // Add interpreters for new languages.
        if (!m_sface.contains(mlang))
            setupInterpreter(mlang);

        // Setup current module path for loading submodules.
        // (sort of closure over invocations of loadf)
        int posls = mpath.lastIndexOf('/');
        if (posls < 1)
        {
            modErrors.append(QString("Funny module path '%1', skipping.")
                                    .arg(mpath));
            continue;
        }
        currentModulePath = mpath.left(posls);
        QString fname = mpath.mid(posls + 1);
        // Scriptface::loadf() wants no extension on the filename
        fname = fname.left(fname.lastIndexOf('.'));

        // Load the module.
        ExecState *exec = m_sface[mlang]->jsi->globalExec();
        List alist;
        alist.append(jsString(fname));

        m_sface[mlang]->loadf(exec, alist);

        // Handle any exception.
        if (exec->hadException())
        {
            modErrors.append(expt2str(exec));
            exec->clearException();
        }
    }

    // Unset module path.
    currentModulePath.clear();

    foreach (const QString &merr, modErrors)
        error.append(merr + '\n');
}

KJS_QT_UNICODE_IMPL

#define SFNAME "Ts"
void KTranscriptImp::setupInterpreter (const QString &lang)
{
    // Create new interpreter.
    Interpreter *jsi = new Interpreter;
    KJS_QT_UNICODE_SET;
    jsi->initGlobalObject();
    jsi->ref();

    // Add scripting interface into the interpreter.
    // NOTE: Config may not contain an entry for the language, in which case
    // it is automatically constructed as an empty hash. This is intended.
    Scriptface *sface = new Scriptface(jsi->globalExec(), config[lang]);
    jsi->globalObject()->put(jsi->globalExec(), SFNAME, sface,
                             DontDelete|ReadOnly);

    // Store scriptface and link to its interpreter.
    sface->jsi = jsi;
    m_sface[lang] = sface;

    //dbgout("=====> Created interpreter for '%1'", lang);
}

// ----------------------------------------------------------------------
// Scriptface internal mechanics.
#include "ktranscript.lut.h"

/* Source for ScriptfaceProtoTable.
@begin ScriptfaceProtoTable 2
    load            Scriptface::Load            DontDelete|ReadOnly|Function 0
    setcall         Scriptface::Setcall         DontDelete|ReadOnly|Function 3
    hascall         Scriptface::Hascall         DontDelete|ReadOnly|Function 1
    acall           Scriptface::Acall           DontDelete|ReadOnly|Function 0
    setcallForall   Scriptface::SetcallForall   DontDelete|ReadOnly|Function 3
    fallback        Scriptface::Fallback        DontDelete|ReadOnly|Function 0
    nsubs           Scriptface::Nsubs           DontDelete|ReadOnly|Function 0
    subs            Scriptface::Subs            DontDelete|ReadOnly|Function 1
    vals            Scriptface::Vals            DontDelete|ReadOnly|Function 1
    msgctxt         Scriptface::Msgctxt         DontDelete|ReadOnly|Function 0
    dynctxt         Scriptface::Dynctxt         DontDelete|ReadOnly|Function 1
    msgid           Scriptface::Msgid           DontDelete|ReadOnly|Function 0
    msgkey          Scriptface::Msgkey          DontDelete|ReadOnly|Function 0
    msgstrf         Scriptface::Msgstrf         DontDelete|ReadOnly|Function 0
    dbgputs         Scriptface::Dbgputs         DontDelete|ReadOnly|Function 1
    lscr            Scriptface::Lscr            DontDelete|ReadOnly|Function 0
    normKey         Scriptface::NormKey         DontDelete|ReadOnly|Function 1
    loadProps       Scriptface::LoadProps       DontDelete|ReadOnly|Function 0
    getProp         Scriptface::GetProp         DontDelete|ReadOnly|Function 2
    setProp         Scriptface::SetProp         DontDelete|ReadOnly|Function 3
    toUpperFirst    Scriptface::ToUpperFirst    DontDelete|ReadOnly|Function 2
    toLowerFirst    Scriptface::ToLowerFirst    DontDelete|ReadOnly|Function 2
    getConfString   Scriptface::GetConfString   DontDelete|ReadOnly|Function 2
    getConfBool     Scriptface::GetConfBool     DontDelete|ReadOnly|Function 2
    getConfNumber   Scriptface::GetConfNumber   DontDelete|ReadOnly|Function 2
@end
*/
/* Source for ScriptfaceTable.
@begin ScriptfaceTable 0
@end
*/

KJS_DEFINE_PROTOTYPE(ScriptfaceProto)
KJS_IMPLEMENT_PROTOFUNC(ScriptfaceProtoFunc)
KJS_IMPLEMENT_PROTOTYPE("Scriptface", ScriptfaceProto, ScriptfaceProtoFunc, ObjectPrototype)

const ClassInfo Scriptface::info = {"Scriptface", 0, &ScriptfaceTable, 0};

Scriptface::Scriptface (ExecState *exec, const TsConfigGroup &config_)
: JSObject(ScriptfaceProto::self(exec)), fallback(NULL), config(config_)
{}

Scriptface::~Scriptface ()
{}

bool Scriptface::getOwnPropertySlot (ExecState *exec, const Identifier& propertyName, PropertySlot& slot)
{
    return getStaticValueSlot<Scriptface, JSObject>(exec, &ScriptfaceTable, this, propertyName, slot);
}

JSValue *Scriptface::getValueProperty (ExecState * /*exec*/, int token) const
{
    switch (token) {
        default:
            dbgout("Scriptface::getValueProperty: Unknown property id %1", token);
    }
    return jsUndefined();
}

void Scriptface::put (ExecState *exec, const Identifier &propertyName, JSValue *value, int attr)
{
    lookupPut<Scriptface, JSObject>(exec, propertyName, value, attr, &ScriptfaceTable, this);
}

void Scriptface::putValueProperty (ExecState * /*exec*/, int token, JSValue * /*value*/, int /*attr*/)
{
    switch(token) {
        default:
            dbgout("Scriptface::putValueProperty: Unknown property id %1", token);
    }
}

#define CALLARG(i) (args.size() > i ? args[i] : jsNull())
JSValue *ScriptfaceProtoFunc::callAsFunction (ExecState *exec, JSObject *thisObj, const List &args)
{
    if (!thisObj->inherits(&Scriptface::info)) {
        return throwError(exec, TypeError);
    }
    Scriptface *obj = static_cast<Scriptface*>(thisObj);
    switch (id) {
        case Scriptface::Load:
            return obj->loadf(exec, args);
        case Scriptface::Setcall:
            return obj->setcallf(exec, CALLARG(0), CALLARG(1), CALLARG(2));
        case Scriptface::Hascall:
            return obj->hascallf(exec, CALLARG(0));
        case Scriptface::Acall:
            return obj->acallf(exec, args);
        case Scriptface::SetcallForall:
            return obj->setcallForallf(exec, CALLARG(0), CALLARG(1), CALLARG(2));
        case Scriptface::Fallback:
            return obj->fallbackf(exec);
        case Scriptface::Nsubs:
            return obj->nsubsf(exec);
        case Scriptface::Subs:
            return obj->subsf(exec, CALLARG(0));
        case Scriptface::Vals:
            return obj->valsf(exec, CALLARG(0));
        case Scriptface::Msgctxt:
            return obj->msgctxtf(exec);
        case Scriptface::Dynctxt:
            return obj->dynctxtf(exec, CALLARG(0));
        case Scriptface::Msgid:
            return obj->msgidf(exec);
        case Scriptface::Msgkey:
            return obj->msgkeyf(exec);
        case Scriptface::Msgstrf:
            return obj->msgstrff(exec);
        case Scriptface::Dbgputs:
            return obj->dbgputsf(exec, CALLARG(0));
        case Scriptface::Lscr:
            return obj->lscrf(exec);
        case Scriptface::NormKey:
            return obj->normKeyf(exec, CALLARG(0));
        case Scriptface::LoadProps:
            return obj->loadPropsf(exec, args);
        case Scriptface::GetProp:
            return obj->getPropf(exec, CALLARG(0), CALLARG(1));
        case Scriptface::SetProp:
            return obj->setPropf(exec, CALLARG(0), CALLARG(1), CALLARG(2));
        case Scriptface::ToUpperFirst:
            return obj->toUpperFirstf(exec, CALLARG(0), CALLARG(1));
        case Scriptface::ToLowerFirst:
            return obj->toLowerFirstf(exec, CALLARG(0), CALLARG(1));
        case Scriptface::GetConfString:
            return obj->getConfStringf(exec, CALLARG(0), CALLARG(1));
        case Scriptface::GetConfBool:
            return obj->getConfBoolf(exec, CALLARG(0), CALLARG(1));
        case Scriptface::GetConfNumber:
            return obj->getConfNumberf(exec, CALLARG(0), CALLARG(1));
        default:
            return jsUndefined();
    }
}

// ----------------------------------------------------------------------
// Scriptface interface functions.
#define SPREF SFNAME"."

JSValue *Scriptface::loadf (ExecState *exec, const List &fnames)
{
    if (globalKTI->currentModulePath.isEmpty())
        return throwError(exec, GeneralError,
                          SPREF"load: no current module path, aiiie...");

    for (int i = 0; i < fnames.size(); ++i)
        if (!fnames[i]->isString())
            return throwError(exec, TypeError,
                              SPREF"load: expected string as file name");

    for (int i = 0; i < fnames.size(); ++i)
    {
        QString qfname = fnames[i]->getString().qstring();
        QString qfpath = globalKTI->currentModulePath + '/' + qfname + ".js";

        QFile file(qfpath);
        if (!file.open(QIODevice::ReadOnly))
            return throwError(exec, GeneralError,
                              QString(SPREF"load: cannot read file '%1'")\
                                     .arg(qfpath));

        QTextStream stream(&file);
        stream.setCodec("UTF-8");
        QString source = stream.readAll();
        file.close();

        Completion comp = jsi->evaluate(qfpath, 0, source);

        if (comp.complType() == Throw)
        {
            JSValue *exval = comp.value();
            ExecState *exec = jsi->globalExec();
            QString msg = exval->toString(exec).qstring();

            QString line;
            if (exval->type() == ObjectType)
            {
                JSValue *lval = exval->getObject()->get(exec, "line");
                if (lval->type() == NumberType)
                    line = QString::number(lval->toInt32(exec));
            }

            return throwError(exec, TypeError,
                              QString("at %1:%2: %3")
                                     .arg(qfpath, line, msg));
        }
        dbgout("Loaded module: %1", qfpath);
    }

    return jsUndefined();
}

JSValue *Scriptface::setcallf (ExecState *exec, JSValue *name,
                               JSValue *func, JSValue *fval)
{
    if (!name->isString())
        return throwError(exec, TypeError,
                          SPREF"setcall: expected string as first argument");
    if (   !func->isObject()
        || !func->getObject()->implementsCall())
        return throwError(exec, TypeError,
                          SPREF"setcall: expected function as second argument");
    if (!(fval->isObject() || fval->isNull()))
        return throwError(exec, TypeError,
                          SPREF"setcall: expected object or null as third argument");

    QString qname = name->toString(exec).qstring();
    funcs[qname] = func->getObject();
    fvals[qname] = fval;

    // Register values to keep GC from collecting them. Is this needed?
    put(exec, Identifier(QString("#:f<%1>").arg(qname)), func, Internal);
    put(exec, Identifier(QString("#:o<%1>").arg(qname)), fval, Internal);

    // Set current module path as module path for this call,
    // in case it contains load subcalls.
    fpaths[qname] = globalKTI->currentModulePath;

    return jsUndefined();
}

JSValue *Scriptface::hascallf (ExecState *exec, JSValue *name)
{
    if (!name->isString())
        return throwError(exec, TypeError,
                          SPREF"hascall: expected string as first argument");

    QString qname = name->toString(exec).qstring();
    return jsBoolean(funcs.contains(qname));
}

JSValue *Scriptface::acallf (ExecState *exec, const List &argv)
{
    if (argv.size() < 1) {
        return throwError(exec, SyntaxError,
                          SPREF"acall: expected at least one argument (call name)");
    }
    if (!argv[0]->isString()) {
        return throwError(exec, SyntaxError,
                          SPREF"acall: expected string as first argument (call name)");
    }

    // Get the function and its context object.
    QString callname = argv[0]->getString().qstring();
    if (!funcs.contains(callname)) {
        return throwError(exec, EvalError,
                          QString(SPREF"acall: unregistered call to '%1'").arg(callname));
    }
    JSObject *func = funcs[callname];
    JSValue *fval = fvals[callname];

    // Recover module path from the time of definition of this call,
    // for possible load calls.
    globalKTI->currentModulePath = fpaths[callname];

    // Execute function.
    List arglist;
    for (int i = 1; i < argv.size(); ++i)
        arglist.append(argv[i]);
    JSValue *val;
    if (fval->isObject()) {
        // Call function with the context object.
        val = func->callAsFunction(exec, fval->getObject(), arglist);
    }
    else {
        // No context object associated to this function, use global.
        val = func->callAsFunction(exec, jsi->globalObject(), arglist);
    }
    return val;
}

JSValue *Scriptface::setcallForallf (ExecState *exec, JSValue *name,
                                     JSValue *func, JSValue *fval)
{
    if (!name->isString())
        return throwError(exec, TypeError,
                          SPREF"setcallForall: expected string as first argument");
    if (   !func->isObject()
        || !func->getObject()->implementsCall())
        return throwError(exec, TypeError,
                          SPREF"setcallForall: expected function as second argument");
    if (!(fval->isObject() || fval->isNull()))
        return throwError(exec, TypeError,
                          SPREF"setcallForall: expected object or null as third argument");

    QString qname = name->toString(exec).qstring();
    funcs[qname] = func->getObject();
    fvals[qname] = fval;

    // Register values to keep GC from collecting them. Is this needed?
    put(exec, Identifier(QString("#:fall<%1>").arg(qname)), func, Internal);
    put(exec, Identifier(QString("#:oall<%1>").arg(qname)), fval, Internal);

    // Set current module path as module path for this call,
    // in case it contains load subcalls.
    fpaths[qname] = globalKTI->currentModulePath;

    // Put in the queue order for execution on all messages.
    nameForalls.append(qname);

    return jsUndefined();
}

JSValue *Scriptface::fallbackf (ExecState *exec)
{
    Q_UNUSED(exec);
    if (fallback != NULL)
        *fallback = true;
    return jsUndefined();
}

JSValue *Scriptface::nsubsf (ExecState *exec)
{
    Q_UNUSED(exec);
    return jsNumber(subs->size());
}

JSValue *Scriptface::subsf (ExecState *exec, JSValue *index)
{
    if (!index->isNumber())
        return throwError(exec, TypeError,
                          SPREF"subs: expected number as first argument");

    int i = qRound(index->getNumber());
    if (i < 0 || i >= subs->size())
        return throwError(exec, RangeError,
                          SPREF"subs: index out of range");

    return jsString(subs->at(i));
}

JSValue *Scriptface::valsf (ExecState *exec, JSValue *index)
{
    if (!index->isNumber())
        return throwError(exec, TypeError,
                          SPREF"vals: expected number as first argument");

    int i = qRound(index->getNumber());
    if (i < 0 || i >= vals->size())
        return throwError(exec, RangeError,
                          SPREF"vals: index out of range");

    return variantToJsValue(vals->at(i));
}

JSValue *Scriptface::msgctxtf (ExecState *exec)
{
    Q_UNUSED(exec);
    return jsString(*msgctxt);
}

JSValue *Scriptface::dynctxtf (ExecState *exec, JSValue *key)
{
    if (!key->isString())
        return throwError(exec, TypeError,
                          SPREF"dynctxt: expected string as first argument");

    QString qkey = key->getString().qstring();
    if (dynctxt->contains(qkey)) {
        return jsString(dynctxt->value(qkey));
    }
    return jsUndefined();
}

JSValue *Scriptface::msgidf (ExecState *exec)
{
    Q_UNUSED(exec);
    return jsString(*msgid);
}

JSValue *Scriptface::msgkeyf (ExecState *exec)
{
    Q_UNUSED(exec);
    return jsString(*msgctxt + '|' + *msgid);
}

JSValue *Scriptface::msgstrff (ExecState *exec)
{
    Q_UNUSED(exec);
    return jsString(*final);
}

JSValue *Scriptface::dbgputsf (ExecState *exec, JSValue *str)
{
    if (!str->isString())
        return throwError(exec, TypeError,
                          SPREF"dbgputs: expected string as first argument");

    QString qstr = str->getString().qstring();

    dbgout("(JS) " + qstr);

    return jsUndefined();
}

JSValue *Scriptface::lscrf (ExecState *exec)
{
    Q_UNUSED(exec);
    return jsString(*lscr);
}

JSValue *Scriptface::normKeyf (ExecState *exec, JSValue *phrase)
{
    if (!phrase->isString()) {
        return throwError(exec, TypeError,
                          SPREF"normKey: expected string as argument");
    }

    QByteArray nqphrase = normKeystr(phrase->toString(exec).qstring());
    return jsString(QString::fromUtf8(nqphrase));
}

JSValue *Scriptface::loadPropsf (ExecState *exec, const List &fnames)
{
    if (globalKTI->currentModulePath.isEmpty()) {
        return throwError(exec, GeneralError,
                          SPREF"loadProps: no current module path, aiiie...");
    }

    for (int i = 0; i < fnames.size(); ++i) {
        if (!fnames[i]->isString()) {
            return throwError(exec, TypeError,
                              SPREF"loadProps: expected string as file name");
        }
    }

    for (int i = 0; i < fnames.size(); ++i)
    {
        QString qfname = fnames[i]->getString().qstring();
        QString qfpath_base = globalKTI->currentModulePath + '/' + qfname;

        // Determine which kind of map is available.
        // Give preference to compiled map.
        QString qfpath = qfpath_base + ".pmapc";
        bool haveCompiled = true;
        QFile file_check(qfpath);
        if (!file_check.open(QIODevice::ReadOnly)) {
            haveCompiled = false;
            qfpath = qfpath_base + ".pmap";
            QFile file_check(qfpath);
            if (!file_check.open(QIODevice::ReadOnly)) {
                return throwError(exec, GeneralError,
                              QString(SPREF"loadProps: cannot read map '%1'")
                                     .arg(qfpath_base));
            }
        }
        file_check.close();

        // Load from appropriate type of map.
        QString errorString;
        if (haveCompiled) {
            errorString = loadProps_bin(qfpath);
        }
        else {
            errorString = loadProps_text(qfpath);
        }
        if (!errorString.isEmpty()) {
            return throwError(exec, SyntaxError, errorString);
        }
        dbgout("Loaded property map: %1", qfpath);
    }

    return jsUndefined();
}

JSValue *Scriptface::getPropf (ExecState *exec, JSValue *phrase, JSValue *prop)
{
    if (!phrase->isString()) {
        return throwError(exec, TypeError,
                          SPREF"getProp: expected string as first argument");
    }
    if (!prop->isString()) {
        return throwError(exec, TypeError,
                          SPREF"getProp: expected string as second argument");
    }

    QByteArray qphrase = normKeystr(phrase->toString(exec).qstring());
    QHash<QByteArray, QByteArray> props = phraseProps.value(qphrase);
    if (!props.isEmpty()) {
        QByteArray qprop = normKeystr(prop->toString(exec).qstring());
        QByteArray qval = props.value(qprop);
        if (!qval.isEmpty()) {
            return jsString(QString::fromUtf8(qval));
        }
    }
    return jsUndefined();
}

JSValue *Scriptface::setPropf (ExecState *exec, JSValue *phrase, JSValue *prop, JSValue *value)
{
    if (!phrase->isString()) {
        return throwError(exec, TypeError,
                          SPREF"setProp: expected string as first argument");
    }
    if (!prop->isString()) {
        return throwError(exec, TypeError,
                          SPREF"setProp: expected string as second argument");
    }
    if (!value->isString()) {
        return throwError(exec, TypeError,
                          SPREF"setProp: expected string as third argument");
    }

    QByteArray qphrase = normKeystr(phrase->toString(exec).qstring());
    QByteArray qprop = normKeystr(prop->toString(exec).qstring());
    QByteArray qvalue = value->toString(exec).qstring().toUtf8();
    // Any non-existent key in first or second-level hash will be created.
    phraseProps[qphrase][qprop] = qvalue;
    return jsUndefined();
}

static QString toCaseFirst (const QString &qstr, int qnalt, bool toupper)
{
    static QString head("~@");
    static int hlen = head.length();

    // If the first letter is found within an alternatives directive,
    // change case of the first letter in each of the alternatives.
    QString qstrcc = qstr;
    int len = qstr.length();
    QChar altSep;
    int remainingAlts = 0;
    bool checkCase = true;
    int numChcased = 0;
    int i = 0;
    while (i < len) {
        QChar c = qstr[i];

        if (qnalt && !remainingAlts && qstr.mid(i, hlen) == head) {
            // An alternatives directive is just starting.
            i += 2;
            if (i >= len) break; // malformed directive, bail out
            // Record alternatives separator, set number of remaining
            // alternatives, reactivate case checking.
            altSep = qstrcc[i];
            remainingAlts = qnalt;
            checkCase = true;
        }
        else if (remainingAlts && c == altSep) {
            // Alternative separator found, reduce number of remaining
            // alternatives and reactivate case checking.
            --remainingAlts;
            checkCase = true;
        }
        else if (checkCase && c.isLetter()) {
            // Case check is active and the character is a letter; change case.
            if (toupper) {
                qstrcc[i] = c.toUpper();
            } else {
                qstrcc[i] = c.toLower();
            }
            ++numChcased;
            // No more case checks until next alternatives separator.
            checkCase = false;
        }

        // If any letter has been changed, and there are no more alternatives
        // to be processed, we're done.
        if (numChcased > 0 && remainingAlts == 0) {
            break;
        }

        // Go to next character.
        ++i;
    }

    return qstrcc;
}

JSValue *Scriptface::toUpperFirstf (ExecState *exec,
                                    JSValue *str, JSValue *nalt)
{
    if (!str->isString()) {
        return throwError(exec, TypeError,
                          SPREF"toUpperFirst: expected string as first argument");
    }
    if (!(nalt->isNumber() || nalt->isNull())) {
        return throwError(exec, TypeError,
                          SPREF"toUpperFirst: expected number as second argument");
    }

    QString qstr = str->toString(exec).qstring();
    int qnalt = nalt->isNull() ? 0 : nalt->toInteger(exec);

    QString qstruc = toCaseFirst(qstr, qnalt, true);

    return jsString(qstruc);
}

JSValue *Scriptface::toLowerFirstf (ExecState *exec,
                                    JSValue *str, JSValue *nalt)
{
    if (!str->isString()) {
        return throwError(exec, TypeError,
                          SPREF"toLowerFirst: expected string as first argument");
    }
    if (!(nalt->isNumber() || nalt->isNull())) {
        return throwError(exec, TypeError,
                          SPREF"toLowerFirst: expected number as second argument");
    }

    QString qstr = str->toString(exec).qstring();
    int qnalt = nalt->isNull() ? 0 : nalt->toInteger(exec);

    QString qstrlc = toCaseFirst(qstr, qnalt, false);

    return jsString(qstrlc);
}

JSValue *Scriptface::getConfStringf (ExecState *exec,
                                     JSValue *key, JSValue *dval)
{
    if (!key->isString()) {
        return throwError(exec, TypeError,
                          SPREF"getConfString: expected string "
                          "as first argument");
    }
    if (!(dval->isString() || dval->isNull())) {
        return throwError(exec, TypeError,
                          SPREF"getConfString: expected string "
                          "as second argument (when given)");
    }

    if (dval->isNull()) {
        dval = jsUndefined();
    }

    QString qkey = key->getString().qstring();
    if (config.contains(qkey)) {
        return jsString(config.value(qkey));
    }

    return dval;
}

JSValue *Scriptface::getConfBoolf (ExecState *exec,
                                   JSValue *key, JSValue *dval)
{
    if (!key->isString()) {
        return throwError(exec, TypeError,
                          SPREF"getConfBool: expected string as "
                          "first argument");
    }
    if (!(dval->isBoolean() || dval->isNull())) {
        return throwError(exec, TypeError,
                          SPREF"getConfBool: expected boolean "
                          "as second argument (when given)");
    }

    static QStringList falsities;
    if (falsities.isEmpty()) {
        falsities.append(QString('0'));
        falsities.append(QString("no"));
        falsities.append(QString("false"));
    }

    if (dval->isNull()) {
        dval = jsUndefined();
    }

    QString qkey = key->getString().qstring();
    if (config.contains(qkey)) {
        QString qval = config.value(qkey).toLower();
        return jsBoolean(!falsities.contains(qval));
    }

    return dval;
}

JSValue *Scriptface::getConfNumberf (ExecState *exec,
                                     JSValue *key, JSValue *dval)
{
    if (!key->isString()) {
        return throwError(exec, TypeError,
                          SPREF"getConfNumber: expected string "
                          "as first argument");
    }
    if (!(dval->isNumber() || dval->isNull())) {
        return throwError(exec, TypeError,
                          SPREF"getConfNumber: expected number "
                          "as second argument (when given)");
    }

    if (dval->isNull()) {
        dval = jsUndefined();
    }

    QString qkey = key->getString().qstring();
    if (config.contains(qkey)) {
        QString qval = config.value(qkey);
        bool convOk;
        double qnum = qval.toDouble(&convOk);
        if (convOk) {
            return jsNumber(qnum);
        }
    }

    return dval;
}

// ----------------------------------------------------------------------
// Scriptface helpers to interface functions.

QString Scriptface::loadProps_text (const QString &fpath)
{
    QFile file(fpath);
    if (!file.open(QIODevice::ReadOnly)) {
        return QString(SPREF"loadProps_text: cannot read file '%1'")
                      .arg(fpath);
    }
    QTextStream stream(&file);
    stream.setCodec("UTF-8");
    QString s = stream.readAll();
    file.close();

    // Parse the map.
    // Should care about performance: possibly executed on each KDE
    // app startup and reading houndreds of thousands of characters.
    enum {s_nextEntry, s_nextKey, s_nextValue};
    QList<QByteArray> ekeys; // holds keys for current entry
    QHash<QByteArray, QByteArray> props; // holds properties for current entry
    int slen = s.length();
    int state = s_nextEntry;
    QByteArray pkey;
    QChar prop_sep, key_sep;
    int i = 0;
    while (1) {
        int i_checkpoint = i;

        if (state == s_nextEntry) {
            while (s[i].isSpace()) {
                ++i;
                if (i >= slen) goto END_PROP_PARSE;
            }
            if (i + 1 >= slen) {
                return QString(SPREF"loadProps_text: unexpected end "
                               "of file in %1").arg(fpath);
            }
            if (s[i] != '#') {
                // Separator characters for this entry.
                key_sep = s[i];
                prop_sep = s[i + 1];
                if (key_sep.isLetter() || prop_sep.isLetter()) {
                    return  QString(SPREF"loadProps_text: separator "
                                    "characters must not be letters at %1:%2")
                                   .arg(fpath).arg(countLines(s, i));
                }

                // Reset all data for current entry.
                ekeys.clear();
                props.clear();
                pkey.clear();

                i += 2;
                state = s_nextKey;
            }
            else {
                // This is a comment, skip to EOL, don't change state.
                while (s[i] != '\n') {
                    ++i;
                    if (i >= slen) goto END_PROP_PARSE;
                }
            }
        }
        else if (state == s_nextKey) {
            int ip = i;
            // Proceed up to next key or property separator.
            while (s[i] != key_sep && s[i] != prop_sep) {
                ++i;
                if (i >= slen) goto END_PROP_PARSE;
            }
            if (s[i] == key_sep) {
                // This is a property key,
                // record for when the value gets parsed.
                pkey = normKeystr(s.mid(ip, i - ip));

                i += 1;
                state = s_nextValue;
            }
            else { // if (s[i] == prop_sep) {
                // This is an entry key, or end of entry.
                QByteArray ekey = normKeystr(s.mid(ip, i - ip));
                if (!ekey.isEmpty()) {
                    // An entry key.
                    ekeys.append(ekey);

                    i += 1;
                    state = s_nextKey;
                }
                else {
                    // End of entry.
                    if (ekeys.size() < 1) {
                        return QString(SPREF"loadProps_text: no entry key "
                                       "for entry ending at %1:%2")
                                       .arg(fpath).arg(countLines(s, i));
                    }

                    // Add collected entry into global store,
                    // once for each entry key (QHash implicitly shared).
                    foreach (const QByteArray &ekey, ekeys) {
                        phraseProps[ekey] = props;
                    }

                    i += 1;
                    state = s_nextEntry;
                }
            }
        }
        else if (state == s_nextValue) {
            int ip = i;
            // Proceed up to next property separator.
            while (s[i] != prop_sep) {
                ++i;
                if (i >= slen) goto END_PROP_PARSE;
                if (s[i] == key_sep) {
                    return QString(SPREF"loadProps_text: property separator "
                                   "inside property value at %1:%2")
                                  .arg(fpath).arg(countLines(s, i));
                }
            }
            // Extract the property value and store the property.
            QByteArray pval = trimSmart(s.mid(ip, i - ip)).toUtf8();
            props[pkey] = pval;

            i += 1;
            state = s_nextKey;
        }
        else {
            return QString(SPREF"loadProps: internal error 10 at %1:%2")
                          .arg(fpath).arg(countLines(s, i));
        }

        // To avoid infinite looping and stepping out.
        if (i == i_checkpoint || i >= slen) {
            return QString(SPREF"loadProps: internal error 20 at %1:%2")
                          .arg(fpath).arg(countLines(s, i));
        }
    }

    END_PROP_PARSE:

    if (state != s_nextEntry) {
        return QString(SPREF"loadProps: unexpected end of file in %1")
                      .arg(fpath);
    }

    return QString();
}

static int bin_read_int (const char *fc, qlonglong len, qlonglong &pos)
{
    // Binary format is big-endian, 32-bit integer.
    static const int nbytes = 4;
    if (pos + nbytes > len) {
        pos = -1;
        return 0;
    }
    int num = qFromBigEndian<quint32>((uchar*) fc + pos);
    pos += nbytes;
    return num;
}

static QByteArray bin_read_string (const char *fc, qlonglong len, qlonglong &pos)
{
    // Binary format stores strings as length followed by byte sequence.
    // No null-termination.
    int nbytes = bin_read_int(fc, len, pos);
    if (pos < 0) {
        return QByteArray();
    }
    if (nbytes < 0 || pos + nbytes > len) {
        pos = -1;
        return QByteArray();
    }
    QByteArray s(fc + pos, nbytes);
    pos += nbytes;
    return s;
}

QString Scriptface::loadProps_bin (const QString &fpath)
{
    QFile file(fpath);
    if (!file.open(QIODevice::ReadOnly)) {
        return QString(SPREF"loadProps_bin: cannot read file '%1'")
                      .arg(fpath);
    }
    QByteArray fctmp = file.readAll();
    file.close();
    const char *fc = fctmp.data();
    const int fclen = fctmp.size();

    // Indicates stream state.
    qlonglong pos = 0;

    // Match header.
    QByteArray head(fc, 8);
    pos += 8;
    if (head != "TSPMAP00") goto END_PROP_PARSE;

    // Read total number of entries.
    int nentries;
    nentries = bin_read_int(fc, fclen, pos);
    if (pos < 0) goto END_PROP_PARSE;

    // Read all entries.
    for (int i = 0; i < nentries; ++i) {

        // Read number of entry keys and all entry keys.
        QList<QByteArray> ekeys;
        int nekeys = bin_read_int(fc, fclen, pos);
        if (pos < 0) goto END_PROP_PARSE;
        for (int j = 0; j < nekeys; ++j) {
            QByteArray ekey = bin_read_string(fc, fclen, pos);
            if (pos < 0) goto END_PROP_PARSE;
            ekeys.append(ekey);
        }
        //dbgout("--------> ekey[0]={%1}", QString::fromUtf8(ekeys[0]));

        // Read number of properties and all properties.
        QHash<QByteArray, QByteArray> props;
        int nprops = bin_read_int(fc, fclen, pos);
        if (pos < 0) goto END_PROP_PARSE;
        for (int j = 0; j < nprops; ++j) {
            QByteArray pkey = bin_read_string(fc, fclen, pos);
            if (pos < 0) goto END_PROP_PARSE;
            QByteArray pval = bin_read_string(fc, fclen, pos);
            if (pos < 0) goto END_PROP_PARSE;
            props[pkey] = pval;
        }

        // Add collected entry into global store,
        // once for each entry key (QHash implicitly shared).
        foreach (const QByteArray &ekey, ekeys) {
            phraseProps[ekey] = props;
        }
    }

    END_PROP_PARSE:

    if (pos < 0) {
        return QString(SPREF"loadProps_bin: corrupt compiled map %1")
                      .arg(fpath);
    }

    return QString();
}
