#include <string.h>
#include <ctype.h>
#include <new>
#include <mk4.h>

#include "jim.h"
#include "jimautoconf.h"
#include "jim-subcmd.h"

extern "C" { /* The whole file is essentially C */

#define MK_PROPERTY_BINARY  'B'
#define MK_PROPERTY_INT     'I'
#define MK_PROPERTY_LONG    'L'
#define MK_PROPERTY_FLOAT   'F'
#define MK_PROPERTY_DOUBLE  'D'
#define MK_PROPERTY_STRING  'S'
#define MK_PROPERTY_VIEW    'V'

#define MK_MODE_ORIGINAL    -1
#define MK_MODE_READONLY    0
#define MK_MODE_READWRITE   1
#define MK_MODE_EXTEND      2

#define MK_CMD_LEN 32
#define JIM_CURSOR_SPACE (35+JIM_REFERENCE_TAGLEN + 1 + 20)
#define JIM_POSITION_SPACE 32
#define MK_VERSION_SPACE 16
#define JIM_MK_DESCR_LEN 64 /* Default, will be reallocated if needed */

#define isnamech(c) ( (c) && !strchr(":,[^]!", (c)) )

#ifndef max
#define max(x, y) ((x) >= (y) ? (x) : (y))
#endif

/* utilities */
static int JimCheckMkName(Jim_Interp *interp, Jim_Obj *name, const char *type);
static const char *JimMkTypeName(char type);
static Jim_Obj *JimFromMkDescription(Jim_Interp *interp, const char *descr, const char **endPtr);
static int JimToMkDescription(Jim_Interp *interp, Jim_Obj *obj, char **descrPtr);
static Jim_Obj *JimGetMkValue(Jim_Interp *interp, c4_Cursor cur, const c4_Property &prop);
static int JimSetMkValue(Jim_Interp *interp, c4_Cursor cur, const c4_Property &prop, Jim_Obj *obj);

static int JimPipelineBoundary(int argc, Jim_Obj *const *argv);

/* property object */
static Jim_Obj *JimNewPropertyObj (Jim_Interp *interp, c4_Property prop);
static int JimGetProperty (Jim_Interp *interp, Jim_Obj *obj,
    c4_View view, const char *what, const c4_Property **propPtr);
static int JimGetPropertyTyped (Jim_Interp *interp, Jim_Obj *obj,
    char type, const c4_Property **propPtr);
static int JimGetNewProperty (Jim_Interp *interp, Jim_Obj *obj,
    c4_View view, char type, const c4_Property **propPtr);
static int JimGetProperties (Jim_Interp *interp, int objc, Jim_Obj *const *objv,
    c4_View view, c4_View *propsPtr);
static Jim_Obj *JimViewPropertiesList (Jim_Interp *interp, c4_View view);

/* cursor object */
static int JimGetPosition (Jim_Interp *interp, Jim_Obj *obj, c4_View view, int *indexPtr);
static int JimGetCursor (Jim_Interp *interp, Jim_Obj *obj, c4_Cursor *curPtr);
static int JimGetCursorView (Jim_Interp *interp, Jim_Obj *obj,
    Jim_Obj **viewObjPtr);
static int JimCursorPos (Jim_Interp *interp, Jim_Obj *obj, Jim_Obj **posObjPtr);
static int JimIncrCursor (Jim_Interp *interp, Jim_Obj *obj, int offset);
static int JimSeekCursor (Jim_Interp *interp, Jim_Obj *obj, Jim_Obj *posObj);

/* Also accepts JIM_ERRMSG */
#define JIM_CURSOR_GET      (1 << JIM_PRIV_FLAG_SHIFT)
#define JIM_CURSOR_SET      (2 << JIM_PRIV_FLAG_SHIFT)
#define JIM_CURSOR_INSERT   (4 << JIM_PRIV_FLAG_SHIFT)

static int JimCheckCursor (Jim_Interp *interp, Jim_Obj *curObj, int flags);

/* view handle */
static Jim_Obj *JimNewViewObj (Jim_Interp *interp, c4_View view);
static int JimGetView (Jim_Interp *interp, Jim_Obj *obj, c4_View *viewPtr);
static void JimPinView (Jim_Interp *interp, Jim_Obj *obj);

/* -------------------------------------------------------------------------
 * Utilities
 * ------------------------------------------------------------------------- */

static int JimCheckMkName(Jim_Interp *interp, Jim_Obj *name, const char *type)
{
    const char *s;
    int i, len;

    s = Jim_GetString(name, &len);

    if (len > 0 && s[0] == '-')
        goto err;
    for (i = 0; i < len; i++) {
        if (!isnamech(s[i]))
            goto err;
    }

    return JIM_OK;

  err:
    Jim_SetResultFormatted(interp, "expected %s name but got \"%#s\"", type ? type : "property", name);
    return JIM_ERR;
}

static const char *const jim_mktype_options[] = {
    "-integer",
    "-long",
    "-float",
    "-double",
    "-string",
    "-subview",
    /* FIXME "-binary", */
    0
};

static const char *const jim_mktype_names[] = {
    "integer",
    "long",
    "float",
    "double",
    "string",
    "subview",
    /* FIXME "binary", */
    0
};

static const char jim_mktype_types[] = {
    MK_PROPERTY_INT,
    MK_PROPERTY_LONG,
    MK_PROPERTY_FLOAT,
    MK_PROPERTY_DOUBLE,
    MK_PROPERTY_STRING,
    MK_PROPERTY_VIEW,
    /* MK_PROPERTY_BINARY, */
};

#define JIM_MKTYPES ((int)(sizeof(jim_mktype_types) / sizeof(jim_mktype_types[0])))

static const char *JimMkTypeName(char type)
{
    int i;

    for (i = 0; i < JIM_MKTYPES; i++) {
        if (type == jim_mktype_types[i])
            return jim_mktype_names[i];
    }
    return "(unknown type)";
}

static Jim_Obj *JimFromMkDescription(Jim_Interp *interp, const char *descr, const char **endPtr)
{
    Jim_Obj *result;
    const char *delim;

    result = Jim_NewListObj(interp, NULL, 0);
    for (;;) {
        if (*descr == ']') {
            descr++;
            break;
        }
        else if (*descr == '\0')
            break;
        else if (*descr == ',')
            descr++;

        delim = strpbrk(descr, ",:[]");
        /* JimPanic((!delim, "Invalid Metakit description string")); */

        Jim_ListAppendElement(interp, result,
            Jim_NewStringObj(interp, descr, delim - descr));

        if (delim[0] == '[') {
            Jim_ListAppendElement(interp, result,
                JimFromMkDescription(interp, delim + 1, &descr));
        }
        else if (delim[0] == ':') {
            Jim_ListAppendElement(interp, result,
                Jim_NewStringObj(interp, JimMkTypeName(delim[1]), -1));
            descr = delim + 2;
        }
        else {
            /* Seems that Metakit never generates descriptions without type
             * tags, but let's handle this just to be safe
             */

            Jim_ListAppendElement(interp, result,
                Jim_NewStringObj(interp, JimMkTypeName(MK_PROPERTY_STRING), -1));
        }
    }

    if (endPtr)
        *endPtr = descr;
    return result;
}

/* This allocates the buffer once per user call and stores it in a static
 * variable. Recursive calls are distinguished by descrPtr == NULL.
 */
static int JimToMkDescription(Jim_Interp *interp, Jim_Obj *descrObj, char **descrPtr)
{
    static char *descr, *outPtr;
    static int bufSize;

    #define ENLARGE(size) do {                                   \
        if ((descr - outPtr) + (size) > bufSize) {               \
            bufSize = max(2*bufSize, (descr - outPtr) + (size)); \
            descr = (char *)Jim_Realloc(descr, bufSize);         \
        }                                                        \
    } while(0)

    int i, count;
    Jim_Obj *name, *struc;

    const char *rep;
    int len;

    count = Jim_ListLength(interp, descrObj);
    if (count % 2) {
        Jim_SetResultString(interp,
            "view description must have an even number of elements", -1);
        return JIM_ERR;
    }

    if (descrPtr) {
        descr = (char *)Jim_Alloc(bufSize = JIM_MK_DESCR_LEN);
        outPtr = descr;
    }

    for (i = 0; i < count; i += 2) {
        Jim_ListIndex(interp, descrObj, i, &name, 0);
        Jim_ListIndex(interp, descrObj, i + 1, &struc, 0);

        if (JimCheckMkName(interp, name, NULL) != JIM_OK)
            goto err;

        rep = Jim_GetString(name, &len);
        ENLARGE(len + 3); /* At least :T, or [], */
        memcpy(outPtr, rep, len);
        outPtr += len;

        if (Jim_ListLength(interp, struc) == 1) {
            int idx;

            if (Jim_GetEnum(interp, struc, jim_mktype_names, &idx,
                    "property type", JIM_ERRMSG | JIM_ENUM_ABBREV) != JIM_OK)
                goto err;

            *outPtr++ = ':';
            *outPtr++ = jim_mktype_types[idx];
        }
        else {
            *outPtr++ = '[';

            if (JimToMkDescription(interp, struc, NULL) != JIM_OK)
                goto err;

            ENLARGE(2); /* bracket, comma */
            *outPtr++ = ']';
        }

        *outPtr++ = ',';
    }
    *(--outPtr) = '\0';

    #undef ENLARGE

    if (descrPtr) {
        *descrPtr = (char *)Jim_Realloc(descr, strlen(descr) + 1);
        descr = NULL; /* Safety measure */
    }

    return JIM_OK;

  err:

    if (descrPtr)
        Jim_Free(descr);

    return JIM_ERR;
}

static Jim_Obj *JimGetMkValue(Jim_Interp *interp, c4_Cursor cur, const c4_Property &prop)
{
    switch (prop.Type()) {
        case MK_PROPERTY_INT:
            return Jim_NewIntObj(interp, ((c4_IntProp &)prop).Get(*cur));
        case MK_PROPERTY_LONG:
            return Jim_NewIntObj(interp, ((c4_LongProp &)prop).Get(*cur));
        case MK_PROPERTY_FLOAT:
            return Jim_NewDoubleObj(interp, ((c4_FloatProp &)prop).Get(*cur));
        case MK_PROPERTY_DOUBLE:
            return Jim_NewDoubleObj(interp, ((c4_DoubleProp &)prop).Get(*cur));
        case MK_PROPERTY_STRING:
            return Jim_NewStringObj(interp, ((c4_StringProp &)prop).Get(*cur), -1);
        case MK_PROPERTY_VIEW:
            return JimNewViewObj(interp, ((c4_ViewProp &)prop).Get(*cur));

        case MK_PROPERTY_BINARY:
            /* FIXME */
        default:
            /* FIXME Something more meaningful here? */
            return Jim_NewEmptyStringObj(interp);
    }
}

static int JimSetMkValue(Jim_Interp *interp, c4_Cursor cur, const c4_Property &prop, Jim_Obj *obj)
{
    switch (prop.Type()) {
        case MK_PROPERTY_INT: {
            jim_wide value;

            if (Jim_GetWide(interp, obj, &value) != JIM_OK)
                return JIM_ERR;

            ((c4_IntProp &)prop).Set(*cur, value);
            return JIM_OK;
        }
        case MK_PROPERTY_LONG: {
            jim_wide value;

            if (Jim_GetWide(interp, obj, &value) != JIM_OK)
                return JIM_ERR;

            ((c4_LongProp &)prop).Set(*cur, value);
            return JIM_OK;
        }
        case MK_PROPERTY_FLOAT: {
            double value;

            if (Jim_GetDouble(interp, obj, &value) != JIM_OK)
                return JIM_ERR;

            ((c4_FloatProp &)prop).Set(*cur, value);
            return JIM_OK;
        }
        case MK_PROPERTY_DOUBLE: {
            double value;

            if (Jim_GetDouble(interp, obj, &value) != JIM_OK)
                return JIM_ERR;

            ((c4_DoubleProp &)prop).Set(*cur, value);
            return JIM_OK;
        }
        case MK_PROPERTY_STRING: {
            int len;
            const char *rep;

            rep = Jim_GetString(obj, &len);
            if (len != (int)strlen(rep)) {
                Jim_SetResultString(interp, "null characters are not allowed in Metakit strings", -1);
                return JIM_ERR;
            }

            ((c4_StringProp &)prop).Set(*cur, rep);
            return JIM_OK;
        }
        case MK_PROPERTY_VIEW: {
            c4_View value;

            if (JimGetView(interp, obj, &value) != JIM_OK)
                return JIM_ERR;

            ((c4_ViewProp &)prop).Set(*cur, value);
        }
        case MK_PROPERTY_BINARY:
            /* FIXME */
        default:
            Jim_SetResultString(interp, "unsupported Metakit type", -1);
            return JIM_ERR;
    }
}

static int JimPipelineBoundary(int argc, Jim_Obj *const *argv) {
    const char *rep;
    int pipe, len;

    for (pipe = 0; pipe < argc; pipe++) {
        rep = Jim_GetString(argv[pipe], &len);
        if (len == 1 && rep[0] == '|')
            break;
    }
    return pipe;
}

/* -------------------------------------------------------------------------
 * Property object
 * ------------------------------------------------------------------------- */

#define JimPropertyValue(o) ((c4_Property *)((o)->internalRep.ptr))

static void FreePropertyInternalRep(Jim_Interp *interp, Jim_Obj *obj)
{
    delete JimPropertyValue(obj);
}

static void DupPropertyInternalRep(Jim_Interp *interp, Jim_Obj *oldObj, Jim_Obj *newObj)
{
    newObj->internalRep.ptr = new c4_Property(*JimPropertyValue(oldObj));
    newObj->typePtr = oldObj->typePtr;
}

static void UpdateStringOfProperty(Jim_Obj* obj)
{
    const char *name = JimPropertyValue(obj)->Name();
    int len = strlen(name);

    obj->bytes = (char *) Jim_Alloc(len + 1);
    memcpy(obj->bytes, name, len + 1);
    obj->length = len;
}

static Jim_ObjType propertyObjType = {
    "mk.property",
    FreePropertyInternalRep,
    DupPropertyInternalRep,
    UpdateStringOfProperty,
    JIM_TYPE_NONE
};

static int JimGetProperty(Jim_Interp *interp, Jim_Obj *obj, c4_View view, const char *name, const c4_Property **propPtr)
{
    int index;

    if (obj->typePtr == &propertyObjType) {
        index = view.FindProperty(JimPropertyValue(obj)->GetId());
    }
    else {
        if (JimCheckMkName(interp, obj, name) != JIM_OK)
            return JIM_ERR;
        index = view.FindPropIndexByName(Jim_String(obj));
    }

    if (index != -1) {
        *propPtr = &view.NthProperty(index);
        return JIM_OK;
    }
    else {
        Jim_SetResultFormatted(interp, "%s \"%#s\" does not exist",
            name ? name : "property", obj);
        return JIM_ERR;
    }
}

static int JimGetPropertyTyped(Jim_Interp *interp, Jim_Obj *obj, char type, const c4_Property **propPtr)
{
    c4_Property *prop;

    if (obj->typePtr == &propertyObjType) {
        if (JimPropertyValue(obj)->Type() != type) {
            /* coerce the property type */

            prop = new c4_Property(type, JimPropertyValue(obj)->Name());
            delete JimPropertyValue(obj);
            obj->internalRep.ptr = prop;
        }
    }
    else {
        if (JimCheckMkName(interp, obj, NULL) != JIM_OK)
            return JIM_ERR;

        prop = new c4_Property(type, Jim_String(obj));

        Jim_FreeIntRep(interp, obj);
        obj->typePtr = &propertyObjType;
        obj->internalRep.ptr = (void *)prop;
    }

    *propPtr = JimPropertyValue(obj);
    return JIM_OK;
}

static int JimGetNewProperty(Jim_Interp *interp, Jim_Obj *obj, c4_View view, char type, const c4_Property **propPtr)
{
    const c4_Property *newp, *prop;

    if (JimGetPropertyTyped(interp, obj, type, &newp) != JIM_OK)
        return JIM_ERR;

    prop = &view.NthProperty(view.AddProperty(*newp));

    if (prop->Type() != newp->Type()) {
        Jim_SetResultFormatted(interp, "property \"%#s\" is %s, not %s",
            obj, JimMkTypeName(prop->Type()), JimMkTypeName(newp->Type()));
        return JIM_ERR;
    }

    *propPtr = prop;
    return JIM_OK;
}

static int JimGetProperties(Jim_Interp *interp, int objc, Jim_Obj *const *objv, c4_View view, c4_View *propsPtr)
{
    int i;
    const c4_Property *prop;
    c4_View props;

    for (i = 0; i < objc; i++) {
        if (JimGetProperty(interp, objv[i], view, NULL, &prop) != JIM_OK)
            return JIM_ERR;

        props.AddProperty(*prop);
    }

    *propsPtr = props;
    return JIM_OK;
}

static Jim_Obj *JimNewPropertyObj(Jim_Interp *interp, c4_Property prop)
{
    Jim_Obj *obj;

    obj = Jim_NewObj(interp);
    obj->typePtr = &propertyObjType;
    obj->bytes = NULL;
    obj->internalRep.ptr = new c4_Property(prop);
    return obj;
}

/* -------------------------------------------------------------------------
 * Cursor object
 * ------------------------------------------------------------------------- */

/* Position ---------------------------------------------------------------- */

/* A normal position if endFlag == 0; otherwise an offset from end+1 (!) */
typedef struct MkPosition {
    int index;
    int endFlag;
} MkPosition;

/* This is mostly the same as SetIndexFromAny, but preserves more information
 * and allows multiple [+-]integer parts.
 */
static int GetPosition(Jim_Interp *interp, Jim_Obj *obj, MkPosition *posPtr)
{
    MkPosition pos;
    const char *rep;
    char *end;
    int sign, offset;

    rep = Jim_String(obj);

    if (strncmp(rep, "end", 3) == 0) {
        pos.endFlag = 1;
        pos.index = -1;

        rep += 3;
    }
    else {
        pos.endFlag = 0;
        pos.index = strtol(rep, &end, 10);
        if (end == rep)
            goto err;

        rep = end;
    }

    while ((rep[0] == '+') || (rep[0] == '-')) {
        sign = (rep[0] == '+' ? 1 : -1);
        rep++;

        offset = strtol(rep, &end, 10);
        if (end == rep)
            goto err;

        pos.index += sign * offset;
        rep = end;
    }

    while (isspace(UCHAR(*rep)))
        rep++;
    if (*rep != '\0')
        goto err;

    *posPtr = pos;
    return JIM_OK;

  err:
    Jim_SetResultFormatted(interp, "expected cursor position but got \"%#s\"", obj);
    return JIM_ERR;
}

static int PositionIndex(const MkPosition *posPtr, c4_View view)
{
    if (posPtr->endFlag)
        return view.GetSize() + posPtr->index;
    else
        return posPtr->index;
}

static int JimGetPosition(Jim_Interp *interp, Jim_Obj *obj, c4_View view, int *indexPtr)
{
    MkPosition pos;

    if (GetPosition(interp, obj, &pos) != JIM_OK)
        return JIM_ERR;

    *indexPtr = PositionIndex(&pos, view);
    return JIM_OK;
}

/* Cursor type ------------------------------------------------------------- */

typedef struct MkCursor {
    MkPosition pos;
    Jim_Obj *viewObj;
} MkCursor;

#define JimCursorValue(obj) ((MkCursor *)(obj->internalRep.ptr))
static void FreeCursorInternalRep(Jim_Interp *interp, Jim_Obj *obj)
{
    Jim_DecrRefCount(interp, JimCursorValue(obj)->viewObj);
    Jim_Free(obj->internalRep.ptr);
}

static void DupCursorInternalRep(Jim_Interp *interp, Jim_Obj *oldObj, Jim_Obj *newObj)
{
    newObj->internalRep.ptr = Jim_Alloc(sizeof(MkCursor));
    *JimCursorValue(newObj) = *JimCursorValue(oldObj);
    Jim_IncrRefCount(JimCursorValue(oldObj)->viewObj);

    newObj->typePtr = oldObj->typePtr;
}

static void UpdateStringOfCursor(Jim_Obj *obj)
{
    char buf[JIM_CURSOR_SPACE + 1];
    MkCursor *curPtr = JimCursorValue(obj);
    int idx, len;

    len = snprintf(buf, JIM_CURSOR_SPACE + 1, "%s!", Jim_String(curPtr->viewObj));

    if (curPtr->pos.endFlag) {
        idx = curPtr->pos.index + 1;
        if (idx == 0)
            len += snprintf(buf + len, JIM_CURSOR_SPACE + 1 - len, "end");
        else
            len += snprintf(buf + len, JIM_CURSOR_SPACE + 1 - len, "end%+d", idx);
    }
    else {
        len += snprintf(buf + len, JIM_CURSOR_SPACE + 1 - len, "%d",
            curPtr->pos.index);
    }

    obj->bytes = (char *)Jim_Alloc(len + 1);
    memcpy(obj->bytes, buf, len + 1);
    obj->length = len;
}

static Jim_ObjType cursorObjType = {
    "mk.cursor",
    FreeCursorInternalRep,
    DupCursorInternalRep,
    UpdateStringOfCursor,
    JIM_TYPE_REFERENCES
};

static int SetCursorFromAny(Jim_Interp *interp, Jim_Obj *obj)
{
    const char *rep, *delim;
    int len;
    Jim_Obj *posObj;
    MkCursor cur;

    rep = Jim_GetString(obj, &len);
    delim = strrchr(rep, '!');

    if (!delim) {
        Jim_SetResultFormatted(interp, "expected cursor but got \"%#s\"", obj);
        return JIM_ERR;
    }

    cur.viewObj = Jim_NewStringObj(interp, rep, delim - rep);
    posObj = Jim_NewStringObj(interp, delim + 1, len - (delim - rep) - 1);

    if (GetPosition(interp, posObj, &cur.pos) != JIM_OK) {
        Jim_FreeNewObj(interp, posObj);
        Jim_FreeNewObj(interp, cur.viewObj);
        return JIM_ERR;
    }

    Jim_FreeIntRep(interp, obj);
    Jim_FreeNewObj(interp, posObj);
    Jim_IncrRefCount(cur.viewObj);

    obj->typePtr = &cursorObjType;
    obj->internalRep.ptr = Jim_Alloc(sizeof(MkCursor));
    *JimCursorValue(obj) = cur;

    return JIM_OK;
}

/* Functions --------------------------------------------------------------- */

static int JimCursorPos(Jim_Interp *interp, Jim_Obj *obj, Jim_Obj **posObjPtr)
{
    if (obj->typePtr != &cursorObjType && SetCursorFromAny(interp, obj) != JIM_OK)
        return JIM_ERR;

    *posObjPtr = Jim_NewStringObj(interp, strrchr(Jim_String(obj), '!') + 1, -1);
    return JIM_OK;
}

static int JimGetCursorView(Jim_Interp *interp, Jim_Obj *obj, Jim_Obj **viewObjPtr)
{
    if (obj->typePtr != &cursorObjType && SetCursorFromAny(interp, obj) != JIM_OK)
        return JIM_ERR;

    *viewObjPtr = JimCursorValue(obj)->viewObj;
    return JIM_OK;
}

static int JimGetCursor(Jim_Interp *interp, Jim_Obj *obj, c4_Cursor *curPtr)
{
    c4_View view;

    if (obj->typePtr != &cursorObjType && SetCursorFromAny(interp, obj) != JIM_OK)
        return JIM_ERR;
    if (JimGetView(interp, JimCursorValue(obj)->viewObj, &view) != JIM_OK)
        return JIM_ERR;

    if (curPtr)
        *curPtr = &view[PositionIndex(&JimCursorValue(obj)->pos, view)];
    return JIM_OK;
}

static int JimIncrCursor(Jim_Interp *interp, Jim_Obj *obj, int offset)
{
    /* JimPanic((Jim_IsShared(obj), "JimIncrCursor called with shared object")) */

    if (obj->typePtr != &cursorObjType && SetCursorFromAny(interp, obj) != JIM_OK)
        return JIM_ERR;

    Jim_InvalidateStringRep(obj);
    JimCursorValue(obj)->pos.index += offset;
    return JIM_OK;
}

static int JimSeekCursor(Jim_Interp *interp, Jim_Obj *obj, Jim_Obj *posObj)
{
    /* JimPanic((Jim_IsShared(obj), "JimSeekCursor called with shared object")) */

    if (obj->typePtr != &cursorObjType && SetCursorFromAny(interp, obj) != JIM_OK)
        return JIM_ERR;

    Jim_InvalidateStringRep(obj);
    return GetPosition(interp, posObj, &JimCursorValue(obj)->pos);
}

static int JimCheckCursor(Jim_Interp *interp, Jim_Obj *curObj, int flags)
{
    static c4_View nullView;

    c4_Cursor cur = &nullView[0];
    int size;

    if (JimGetCursor(interp, curObj, &cur) != JIM_OK)
        return JIM_ERR;
    size = (*cur).Container().GetSize();

    if ((flags & JIM_CURSOR_GET) && (cur._index < 0 || cur._index >= size)) {
        if (flags & JIM_ERRMSG) {
            Jim_SetResultFormatted(interp,
                "cursor \"%#s\" does not point to an existing row", curObj);
        }
        return JIM_ERR;
    }
    else if ((flags & JIM_CURSOR_SET) && cur._index < 0) {
        if (flags & JIM_ERRMSG) {
            Jim_SetResultFormatted(interp,
                "cursor \"%#s\" points before start of view", curObj);
        }
        return JIM_ERR;
    }
    else if ((flags & JIM_CURSOR_INSERT) && (cur._index < 0 || cur._index > size)) {
        if (flags & JIM_ERRMSG) {
            Jim_SetResultFormatted(interp,
                "cursor \"%#s\" does not point to a valid insert position", curObj);
        }
        return JIM_ERR;
    }

    return JIM_OK;
}

/* Records ----------------------------------------------------------------- */

static int cursor_cmd_get(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    c4_View view;
    c4_Cursor cur = &view[0];

    if (JimGetCursor(interp, argv[0], &cur) != JIM_OK)
        return JIM_ERR;
    if (JimCheckCursor(interp, argv[0], JIM_ERRMSG | JIM_CURSOR_GET) != JIM_OK)
        return JIM_ERR;

    view = (*cur).Container();

    if (argc == 1) { /* Return all properties */
        int i, count;
        Jim_Obj *result;

        result = Jim_NewListObj(interp, NULL, 0);
        count = view.NumProperties();

        for (i = 0; i < count; i++) {
            c4_Property prop = view.NthProperty(i);

            Jim_ListAppendElement(interp, result, JimNewPropertyObj(interp, prop));
            Jim_ListAppendElement(interp, result, JimGetMkValue(interp, cur, prop));
        }

        Jim_SetResult(interp, result);
        return JIM_OK;
    }
    else { /* Return a single property */
        const c4_Property *propPtr;
        int pipe;

        pipe = JimPipelineBoundary(argc, argv);
        if (pipe == 2) {
            /* No type annotation, existing property */
            if (JimGetProperty(interp, argv[1], view, NULL, &propPtr) != JIM_OK)
                return JIM_ERR;
        }
        else if (pipe == 3) {
            /* Explicit type annotation; the property may be new */
            int idx;

            if (Jim_GetEnum(interp, argv[1], jim_mktype_options, &idx,
                    "property type", JIM_ERRMSG | JIM_ENUM_ABBREV) != JIM_OK)
                return JIM_ERR;
            if (JimGetNewProperty(interp, argv[2], view, jim_mktype_types[idx], &propPtr) != JIM_OK)
                return JIM_ERR;
        }
        else {
            Jim_WrongNumArgs(interp, 0, NULL, "cursor get ?-type? ?prop?");
            return JIM_ERR;
        }

        Jim_SetResult(interp, JimGetMkValue(interp, cur, *propPtr));

        if (pipe == argc)
            return JIM_OK;
        else
            return Jim_EvalObjPrefix(interp, Jim_GetResult(interp), argc - pipe - 1, argv + pipe + 1);
    }
}

static int cursor_cmd_set(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    c4_View view;
    c4_Cursor cur = &view[0];
    const c4_Property *propPtr;
    int i, oldSize;

    if (JimGetCursor(interp, argv[0], &cur) != JIM_OK)
        return JIM_ERR;
    if (JimCheckCursor(interp, argv[0], JIM_ERRMSG | JIM_CURSOR_SET) != JIM_OK)
        return JIM_ERR;

    view = (*cur).Container();
    oldSize = view.GetSize();

    if (cur._index >= oldSize)
        view.SetSize(cur._index + 1);

    if (argc == 2) {
        /* Update everything except subviews from a dictionary in argv[1].
         * No new properties are permitted.
         */

        int objc;
        Jim_Obj **objv;

        if (Jim_DictPairs(interp, argv[1], &objv, &objc) != JIM_OK)
            goto err;

        for (i = 0; i < objc; i += 2) {
            if (JimGetProperty(interp, objv[i], view, NULL, &propPtr) != JIM_OK ||
                JimSetMkValue(interp, cur, *propPtr, objv[i+1]) != JIM_OK)
            {
                Jim_Free(objv);
                goto err;
            }
        }
    }
    else {
        /* Update everything from argv[1..]. New properties are permitted if
         * explicitly typed.
         */

        for (i = 1; i < argc; i += 2) {
            if (Jim_String(argv[i])[0] == '-') {
                int idx;

                if (i + 2 >= argc) {
                    Jim_WrongNumArgs(interp, 2, argv, "?-type? prop value ?...?");
                    goto err;
                }

                if (Jim_GetEnum(interp, argv[i], jim_mktype_options, &idx,
                        "property type", JIM_ERRMSG | JIM_ENUM_ABBREV) != JIM_OK)
                    goto err;
                if (JimGetNewProperty(interp, argv[i+1], view, jim_mktype_types[idx], &propPtr) != JIM_OK)
                    goto err;
                i++;
            }
            else {
                if (i + 1 >= argc) {
                    Jim_WrongNumArgs(interp, 2, argv, "?-type? prop value ?...?");
                    goto err;
                }

                if (JimGetProperty(interp, argv[i], view, NULL, &propPtr) != JIM_OK)
                    goto err;
            }

            if (JimSetMkValue(interp, cur, *propPtr, argv[i+1]) != JIM_OK)
                goto err;
        }
    }

    return JIM_OK;

  err:
    view.SetSize(oldSize);
    return JIM_ERR;
}

static int cursor_cmd_insert(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    c4_View view;
    c4_Cursor cur = &view[0];
    jim_wide count;

    if (JimGetCursor(interp, argv[0], &cur) != JIM_OK)
        return JIM_ERR;
    if (JimCheckCursor(interp, argv[0], JIM_ERRMSG | JIM_CURSOR_INSERT) != JIM_OK)
        return JIM_ERR;

    view = (*cur).Container();

    if (argc == 1)
        count = 1;
    else {
        if (Jim_GetWide(interp, argv[1], &count) != JIM_OK)
            return JIM_ERR;
    }

    if (count > 0) {
        c4_Row empty;
        view.InsertAt(cur._index, empty, (int)count);
    }

    Jim_SetEmptyResult(interp);
    return JIM_OK;
}

static int cursor_cmd_remove(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    c4_View view;
    c4_Cursor cur = &view[0];
    int pos;
    jim_wide count;

    if (JimGetCursor(interp, argv[0], &cur) != JIM_OK)
        return JIM_ERR;
    if (JimCheckCursor(interp, argv[0], JIM_ERRMSG | JIM_CURSOR_SET) != JIM_OK)
        return JIM_ERR;

    view = (*cur).Container();
    pos = cur._index;

    if (argc == 1)
        count = 1;
    else {
        if (Jim_GetWide(interp, argv[1], &count) != JIM_OK)
            return JIM_ERR;
    }

    if (pos + count < view.GetSize())
        count = view.GetSize() - pos;

    if (pos < view.GetSize())
        view.RemoveAt(pos, (int)count);

    return JIM_OK;
}

/* Attributes -------------------------------------------------------------- */

static int cursor_cmd_view(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    Jim_Obj *viewObj;

    if (JimGetCursorView(interp, argv[0], &viewObj) != JIM_OK)
        return JIM_ERR;

    JimPinView(interp, viewObj);
    Jim_SetResult(interp, viewObj);
    return JIM_OK;
}

/* Positioning ------------------------------------------------------------- */

static int cursor_cmd_tell(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    if (argc == 1) {
        Jim_Obj *result;

        if (JimCursorPos(interp, argv[0], &result) != JIM_OK)
            return JIM_ERR;
        Jim_SetResult(interp, result);
    }
    else {
        static c4_View nullView;
        c4_Cursor cur = &nullView[0];

        if (!Jim_CompareStringImmediate(interp, argv[0], "-absolute")) {
            Jim_SetResultFormatted(interp,
                "bad option \"%#s\": must be -absolute", argv[0]);
            return JIM_ERR;
        }

        if (JimGetCursor(interp, argv[1], &cur) != JIM_OK)
            return JIM_ERR;

        Jim_SetResultInt(interp, cur._index);
    }

    return JIM_OK;
}

static int cursor_cmd_validfor(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    static const char *options[] = {
        "get", "set", "insert", "remove", 0
    };
    static int optflags[] = {
        JIM_CURSOR_GET,
        JIM_CURSOR_SET,
        JIM_CURSOR_INSERT,
        JIM_CURSOR_SET
    };

    int idx;

    if (argc == 1)
        idx = 0;
    else {
        if (Jim_GetEnum(interp, argv[0], options, &idx, NULL,
                JIM_ERRMSG | JIM_ENUM_ABBREV) != JIM_OK)
            return JIM_ERR;
    }

    if (JimGetCursor(interp, argv[argc-1], NULL) != JIM_OK)
        return JIM_ERR;

    Jim_SetResultBool(interp, JimCheckCursor(interp, argv[argc-1], optflags[idx]) == JIM_OK);
    return JIM_OK;
}

static int cursor_cmd_seek(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    Jim_Obj *curObj;

    curObj = Jim_GetVariable(interp, argv[0], JIM_ERRMSG | JIM_UNSHARED);
    if (curObj == NULL)
        return JIM_ERR;

    if (JimSeekCursor(interp, curObj, argv[1]) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, curObj);
    return JIM_OK;
}

static int cursor_cmd_incr(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    Jim_Obj *curObj;
    jim_wide offset;

    if (argc == 1)
        offset = 1;
    else {
        if (Jim_GetWide(interp, argv[1], &offset) != JIM_OK)
            return JIM_ERR;
    }

    curObj = Jim_GetVariable(interp, argv[0], JIM_ERRMSG | JIM_UNSHARED);
    if (curObj == NULL)
        return JIM_ERR;

    if (JimIncrCursor(interp, curObj, (int)offset) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, curObj);
    return JIM_OK;
}

/* Command table ----------------------------------------------------------- */

static const jim_subcmd_type cursor_command_table[] = {

    /* Records */

    {   "get", "cur ?-type? ?prop?",
        cursor_cmd_get,
        1, -1,
        0,
        /*"Get the whole record or a specific property at the cursor"*/
    },
    {   "set", "cur [dict | ?-type? field value ?...?]",
        cursor_cmd_set,
        1, -1,
        0,
        /*"Update the record at the cursor"*/
    },
    {   "insert", "cur ?count?",
        cursor_cmd_insert,
        1, 2,
        0,
        /*"Insert a specified number of empty rows at the cursor (default 1)"*/
    },
    {   "remove", "cur ?count?",
        cursor_cmd_remove,
        1, 2,
        0,
        /*"Remove a specified number of rows at the cursor (default 1)"*/
    },

    /* Attributes */

    {   "view", "cur",
        cursor_cmd_view,
        1, 1,
        0,
        /*"Get the view the cursor points into"*/
    },

    /* Positioning */

    {   "tell", "?-absolute? cur",
        cursor_cmd_tell,
        1, 2,
        0,
        /*"Get the position of the cursor"*/
    },
    {   "validfor", "?command? cur",
        cursor_cmd_validfor,
        1, 2,
        0,
        /*"Checks if the cursor is valid for get (default), set or insert commands"*/
    },
    {   "seek", "curVar index",
        cursor_cmd_seek,
        2, 2,
        0,
        /*"Seek to the specified index in the view"*/
    },
    {   "incr", "curVar ?offset?",
        cursor_cmd_incr,
        1, 2,
        0,
        /*"Move the cursor offset records from its current position (default 1)"*/
    },

    { 0 }
};

static int JimCursorCommand(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    Jim_Obj *cmdObj;

    if (argc < 2) {
        Jim_WrongNumArgs(interp, 1, argv, "command ...");
        return JIM_ERR;
    }

    cmdObj = Jim_NewStringObj(interp, "cursor ", -1);
    Jim_AppendObj(interp, cmdObj, argv[1]);

    if (Jim_GetCommand(interp, cmdObj, 0) != NULL)
        return Jim_EvalObjPrefix(interp, cmdObj, argc - 2, argv + 2);
    else {
        Jim_FreeNewObj(interp, cmdObj);
        return Jim_CallSubCmd(interp,
            Jim_ParseSubCmd(interp, cursor_command_table, argc, argv), argc, argv);
    }
}

/* -------------------------------------------------------------------------
 * View handle
 * ------------------------------------------------------------------------- */

/* Views aren't really Jim objects; instead, they are Tk-style commands with
 * oo.tcl-like lifetime management. Additionally, all views are initially
 * created as one-shot, meaning that they die after one command. Call
 * JimPinView to make a view object persistent.
 *
 * It is valid to rename a view in the Tcl land, but by doing this you take
 * the responsibility of destroying the object when it's no longer needed.
 * Any cursors that pointed into the view become invalid.
 */

static int JimViewSubCmdProc(Jim_Interp *interp, int argc, Jim_Obj *const *argv);
static int JimOneShotViewSubCmdProc(Jim_Interp *interp, int argc, Jim_Obj *const *argv);

/* Unary operations -------------------------------------------------------- */

#define UNOP(name, Method) \
static int view_cmd_##name(Jim_Interp *interp, int argc, Jim_Obj *const *argv)  \
{   \
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);          \
    \
    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->Method()));            \
    return JIM_OK;                                                              \
}

UNOP(copy, Duplicate)
UNOP(clone, Clone)
UNOP(unique, Unique)

#undef UNOP

static int view_cmd_blocked(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);

    if (viewPtr->GetSize() != 1 ||
        strcmp(viewPtr->NthProperty(0).Name(), "_B") != 0 ||
        viewPtr->NthProperty(0).Type() != MK_PROPERTY_VIEW)
    {
        Jim_SetResultString(interp,
            "blocked view must have exactly one subview property called _B", -1);
        return JIM_ERR;
    }

    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->Blocked()));
    return JIM_OK;
}

/* Binary operations ------------------------------------------------------- */

#define BINOP(name, Method) \
static int view_cmd_##name(Jim_Interp *interp, int argc, Jim_Obj *const *argv)  \
{   \
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);          \
    c4_View otherView;                                                          \
    \
    if (JimGetView(interp, argv[0], &otherView) != JIM_OK)                      \
        return JIM_ERR;                                                         \
    \
    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->Method(otherView)));   \
    return JIM_OK;                                                              \
}

BINOP(pair, Pair)
BINOP(concat, Concat)
BINOP(product, Product)

BINOP(union, Union)
BINOP(intersect, Intersect)
BINOP(minus, Minus)
BINOP(different, Different)

#undef BINOP

/* Projections ------------------------------------------------------------- */

static int view_cmd_project(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    c4_View props;

    if (JimGetProperties(interp, argc, argv, *viewPtr, &props) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->Project(props)));
    return JIM_OK;
}

static int view_cmd_without(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    c4_View props;

    if (JimGetProperties(interp, argc, argv, *viewPtr, &props) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->ProjectWithout(props)));
    return JIM_OK;
}

static int view_cmd_range(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    int start, end;
    jim_wide step;

    if (JimGetPosition(interp, argv[0], *viewPtr, &start) != JIM_OK ||
        JimGetPosition(interp, argv[1], *viewPtr, &end) != JIM_OK)
    {
        return JIM_ERR;
    }

    if (argc == 2)
        step = 1;
    else if (Jim_GetWide(interp, argv[2], &step) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->Slice(start, end + 1, (int)step)));
    return JIM_OK;
}

/* Ordering ---------------------------------------------------------------- */

static int view_cmd_sort(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    c4_View sortProps, revProps;
    const c4_Property *propPtr;
    int i, len;

    const char *rep;
    int reverse;
    Jim_Obj *propObj;

    /* Special case: property names may be preceded with a dash. Use
     * a temporary object in this case.
     */

    for (i = 0; i < argc; i++) {
        propObj = argv[i];

        rep = Jim_GetString(argv[i], &len);
        reverse = (len > 0 && rep[0] == '-');

        if (reverse)
            propObj = Jim_NewStringObj(interp, rep + 1, len - 1);

        if (JimGetProperty(interp, propObj, *viewPtr, NULL, &propPtr) != JIM_OK) {
            if (reverse)
                Jim_FreeNewObj(interp, propObj);
            return JIM_ERR;
        }

        sortProps.AddProperty(*propPtr);
        if (reverse) {
            revProps.AddProperty(*propPtr);
            Jim_FreeNewObj(interp, propObj);
        }
    }

    if (sortProps.GetSize() == 0)
        Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->Sort()));
    else if (revProps.GetSize() == 0)
        Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->SortOn(sortProps)));
    else
        Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->SortOnReverse(sortProps, revProps)));

    return JIM_OK;
}

/* Metakit core seems to be doing something similar for SortOn, but neither
 * Ordered nor Hash use it, for unknown reason.
 */

static int BubbleProperties(Jim_Interp *interp, c4_View orig, int objc, Jim_Obj *const *objv, c4_View *projPtr)
{
    c4_View proj;
    const c4_Property *propPtr;
    int i, count;

    for (i = 0; i < objc; i++) {
        if (JimGetProperty(interp, objv[i], orig, NULL, &propPtr) != JIM_OK)
            return JIM_ERR;
        proj.AddProperty(*propPtr);
    }

    count = orig.NumProperties();
    for (i = 0; i < count; i++)
        proj.AddProperty(orig.NthProperty(i));

    *projPtr = proj;
    return JIM_OK;
}

static int view_cmd_ordered(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    c4_View proj;

    if (BubbleProperties(interp, *viewPtr, argc, argv, &proj) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, JimNewViewObj(interp, proj.Ordered(argc)));
    return JIM_OK;
}

static int view_cmd_hash(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    c4_View hash, proj;

    if (JimGetView(interp, argv[0], &hash) != JIM_OK)
        return JIM_ERR;

    if (hash.GetSize() != 2 ||
        strcmp(hash.NthProperty(0).Name(), "_H") != 0 ||
        hash.NthProperty(0).Type() != MK_PROPERTY_INT ||
        strcmp(hash.NthProperty(1).Name(), "_R") != 0 ||
        hash.NthProperty(1).Type() != MK_PROPERTY_INT) /* Ouch. */
    {
        Jim_SetResultString(interp,
            "hash view must be laid out as {_H integer _R integer}", -1);
        return JIM_ERR;
    }

    if (BubbleProperties(interp, *viewPtr, argc - 1, argv + 1, &proj) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, JimNewViewObj(interp, proj.Hash(hash, argc - 1)));
    return JIM_OK;
}

/* Relational operations --------------------------------------------------- */

static int view_cmd_join(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    c4_View other, props;
    int outer, off;

    if (JimGetView(interp, argv[0], &other) != JIM_OK)
        return JIM_ERR;

    off = 1; outer = 0;
    if (Jim_CompareStringImmediate(interp, argv[1], "-outer")) {
        off++; outer = 1;
    }

    if (JimGetProperties(interp, argc - off, argv + off, *viewPtr, &props) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->Join(props, other, outer)));
    return JIM_OK;
}

static int view_cmd_group(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    const c4_Property *subviewPtr;
    c4_View props;

    if (JimGetPropertyTyped(interp, argv[0], MK_PROPERTY_VIEW, &subviewPtr) != JIM_OK)
        return JIM_ERR;

    if (JimGetProperties(interp, argc - 1, argv + 1, *viewPtr, &props) != JIM_OK)
        return JIM_ERR;

    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->GroupBy(props, *(c4_ViewProp *)subviewPtr)));
    return JIM_OK;
}

static int view_cmd_flatten(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);
    const c4_Property *subviewPtr;

    if (JimGetProperty(interp, argv[0], *viewPtr, NULL, &subviewPtr) != JIM_OK)
        return JIM_ERR;

    if (subviewPtr->Type() != MK_PROPERTY_VIEW) {
        Jim_SetResultFormatted(interp, "expected a subview property but got %s one",
            JimMkTypeName(subviewPtr->Type()));
        return JIM_ERR;
    }

    Jim_SetResult(interp, JimNewViewObj(interp, viewPtr->JoinProp(*(c4_ViewProp *)subviewPtr)));
    return JIM_OK;
}

/* View queries ------------------------------------------------------------ */

static int view_cmd_properties(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *) Jim_CmdPrivData(interp);
    Jim_SetResult(interp, JimViewPropertiesList(interp, *viewPtr));
    return JIM_OK;
}

static int view_cmd_size(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *) Jim_CmdPrivData(interp);
    Jim_SetResultInt(interp, viewPtr->GetSize());
    return JIM_OK;
}

static int view_cmd_resize(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    c4_View *view = (c4_View *) Jim_CmdPrivData(interp);
    jim_wide size;

    if (Jim_GetWide(interp, argv[0], &size) != JIM_OK)
        return JIM_ERR;
    if (size < 0 || size > INT_MAX) {
        Jim_SetResultFormatted(interp,
            "view size \"%#s\" is out of range", argv[0]);
        return JIM_ERR;
    }

    view->SetSize((int)size);
    Jim_SetResult(interp, argv[0]);
    return JIM_OK;
}

static int view_cmd_type(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    const c4_View *viewPtr = (const c4_View *)Jim_CmdPrivData(interp);

    if (argc == 1) {
        const c4_Property *propPtr;

        if (JimGetProperty(interp, argv[0], *viewPtr, NULL, &propPtr) != JIM_OK)
            return JIM_ERR;

        Jim_SetResultString(interp, JimMkTypeName(propPtr->Type()), -1);
    }
    else {
        Jim_Obj *result;
        int i, count;

        result = Jim_NewListObj(interp, NULL, 0);
        count = viewPtr->NumProperties();

        for (i = 0; i < count; i++) {
            c4_Property prop = viewPtr->NthProperty(i);
            Jim_ListAppendElement(interp, result, JimNewPropertyObj(interp, prop));
            Jim_ListAppendElement(interp, result,
                Jim_NewStringObj(interp, JimMkTypeName(prop.Type()), -1));
        }

        Jim_SetResult(interp, result);
    }

    return JIM_OK;
}

/* View lifetime ----------------------------------------------------------- */

static int view_cmd_pin(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    JimPinView(interp, argv[0]);
    Jim_SetResult(interp, argv[0]);
    return JIM_OK;
}

static int view_cmd_as(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    JimPinView(interp, argv[0]);
    Jim_SetVariable(interp, argv[2], argv[0]);
    Jim_SetResult(interp, argv[0]);
    return JIM_OK;
}

static int view_cmd_destroy(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    Jim_DeleteCommand(interp, Jim_String(argv[0]));
    return JIM_OK;
}

/* Command table ----------------------------------------------------------- */

static const jim_subcmd_type view_command_table[] = {

    /* Unary operations */

    {   "copy", "",
        view_cmd_copy,
        0, 0,
        0,
        /*"Create a copy of the view with exactly the same data"*/
    },
    {   "clone", "",
        view_cmd_clone,
        0, 0,
        0,
        /*"Create an empty view with the same properties as this one"*/
    },
    {   "unique", "",
        view_cmd_unique,
        0, 0,
        0,
        /*"Derived view without any duplicate rows (read-only, no change notifications)"*/
    },
    {   "blocked", "",
        view_cmd_blocked,
        0, 0,
        0,
        /*"Build a scalable \"blocked\" out of a view with a single subview property called _B"*/
    },

    /* Binary operations */

    #define BINOP(name, descr)  \
    {   #name, "otherView",     \
        view_cmd_##name,        \
        1, 1, 0,                \
    }

    BINOP(pair, "Pairwise concatenation of two views"),
    BINOP(concat, "Concatenation of two views; unlike union, doesn't remove duplicates"),
    BINOP(product, "Cartesian product of two views, i.e. every row in view paired with every row in otherView"),

    /* Set operations */

    #define SETOP(name, descr) BINOP(name, descr "; works only if all the rows are unique")

    SETOP(union, "Set union of two views (read-only, no change notifications)"),
    SETOP(intersect, "Set intersection of two views"),
    SETOP(different, "Symmetric difference of two views"),
    SETOP(minus, "Set minus, i.e. all rows from view not in otherView"),

    #undef SETOP

    #undef BINOP

    /* Projections and selections */

    {   "project", "prop ?prop ...?",
        view_cmd_project,
        1, -1,
        0,
        /*"View projection: only the specified properties, in the specified order"*/
    },
    {   "without", "prop ?prop ...?",
        view_cmd_without,
        1, -1,
        0,
        /*"View projection: remove the specified properties"*/
    },
    {   "range", "first last ?step?",
        view_cmd_range,
        2, 3,
        0,
        /*"Range or slice of the view (read-write, no change notifications)"*/
    },

    /* Ordering */

    {   "sort", "?[prop|-prop] ...?",
        view_cmd_sort,
        0, -1,
        0,
        /*"Derived view sorted on the specified properties (in order), or on all properties"*/
    },
    {   "ordered", "prop ?prop ...?",
        view_cmd_ordered,
        1, -1,
        0,
        /*"Consider the underlying view ordered on the specified properties"*/
    },
    {   "hash", "hashView prop ?prop ...?",
        view_cmd_hash,
        2, -1,
        0,
        /*"Mapped view maintaining a hash table on the key consisting of the specified properties"*/
    },

    /* Relational operations */

    {   "join", "view ?-outer? prop ?prop ...?",
        view_cmd_join,
        2, -1,
        0,
        /*"Relational join with view on the specified properties"*/
    },
    {   "group", "subviewName prop ?prop ...?",
        view_cmd_group,
        1, -1,
        0,
        /*"Group rows with equal specified properties, move all other properties into subview"*/
    },
    {   "flatten", "subviewProp",
        view_cmd_flatten,
        1, 1,
        0,
        /*"Flatten the specified subview; the inverse of group"*/
    },

    /* Attributes */

    {   "properties", "",
        view_cmd_properties,
        0, 0,
        0,
        /*"List the properties in this view"*/
    },
    {   "size", "",
        view_cmd_size,
        0, 0,
        0,
        /*"Return the number of records in the view"*/
    },
    {   "resize", "newSize",
        view_cmd_resize,
        1, 1,
        0,
        /*"Set the number of records in the view"*/
    },
    {   "type", "?prop?",
        view_cmd_type,
        0, 1,
        0,
        /*"Return the type of an existing property, or of all properties"*/
    },

    /* Lifetime management */

    {   "pin", "",
        view_cmd_pin,
        0, 0,
        JIM_MODFLAG_FULLARGV,
        /*"Marks the view as persistent"*/
    },
    {   "as", "varName",
        view_cmd_as,
        1, 1,
        JIM_MODFLAG_FULLARGV,
        /*"Marks the view as persistent and assigns it to the given variable"*/
    },
    {   "destroy", "",
        view_cmd_destroy,
        0, 0,
        JIM_MODFLAG_FULLARGV,
        /*"Destroys the view explicitly"*/
    },

    { 0 }
};

static void JimViewDelProc(Jim_Interp *interp, void *privData)
{
    delete (c4_View *)privData;
}

static int JimViewSubCmdProc(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int pipe, result;
    Jim_Obj *cmdObj;

    pipe = JimPipelineBoundary(argc, argv);

    if (pipe < 1) {
        Jim_WrongNumArgs(interp, 1, argv, "command ...");
        return JIM_ERR;
    }

    /* Check for a Tcl command first, and try builtins afterwards.
     * We have to do it in this order so that Jim_ParseSubCmd isn't too greedy
     * about abbreviations, and still it can't now detect ambigous abbrevs
     * properly :( Tcl commands cannot be abbreviated at all.
     */

    cmdObj = Jim_NewStringObj(interp, "mk.view ", -1);
    Jim_AppendObj(interp, cmdObj, argv[1]);

    /* The command will be cached even though we discard the result */
    if (Jim_GetCommand(interp, cmdObj, 0) != NULL) {
        /* Shuffle the arguments: $view cmd args... => {mk.view cmd} $view args... */

        Jim_Obj **objv = (Jim_Obj **)Jim_Alloc(pipe * sizeof(Jim_Obj *));
        objv[0] = cmdObj;
        objv[1] = argv[0];
        memcpy(objv + 2, argv + 2, (pipe - 2) * sizeof(Jim_Obj *));

        result = Jim_EvalObjVector(interp, pipe, objv);

        Jim_Free(objv);
    } else {
        Jim_FreeNewObj(interp, cmdObj);
        result = Jim_CallSubCmd(interp, Jim_ParseSubCmd(interp, view_command_table, pipe, argv), pipe, argv);
    }

    if (result != JIM_OK || pipe == argc)
        return result;
    else
        return Jim_EvalObjPrefix(interp, Jim_GetResult(interp), argc - pipe - 1, argv + pipe + 1);
}

static int JimOneShotViewSubCmdProc(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int result;
    Jim_Cmd *cmd;

    result = JimViewSubCmdProc(interp, argc, argv);

    cmd = Jim_GetCommand(interp, argv[0], 0);
    if (cmd && !cmd->isproc && cmd->u.native.cmdProc == JimOneShotViewSubCmdProc)
        Jim_DeleteCommand(interp, Jim_String(argv[0]));

    return result;
}

static int JimViewFinalizerProc(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    /* We won't succeed here if the user renamed the command, and this is right */
    Jim_DeleteCommand(interp, Jim_String(argv[1]));
    return JIM_OK;
}

static Jim_Obj *JimNewViewObj(Jim_Interp *interp, c4_View view) {
    Jim_Obj *tag, *ref;

    tag = Jim_NewStringObj(interp, "mk.view", -1);
    ref = Jim_NewReference(interp, tag, tag, Jim_NewStringObj(interp, "mk.view.finalizer", -1));
    Jim_CreateCommand(interp, Jim_String(ref),
        JimOneShotViewSubCmdProc, new c4_View(view), JimViewDelProc);

    return ref;
}

static int JimGetView(Jim_Interp *interp, Jim_Obj *obj, c4_View *viewPtr)
{
    Jim_Cmd *cmd = Jim_GetCommand(interp, obj, 0);

    if (cmd == NULL || cmd->isproc || cmd->u.native.delProc != JimViewDelProc) {
        Jim_SetResultFormatted(interp, "invalid view object \"%#s\"", obj);
        return JIM_ERR;
    }

    *viewPtr = *(c4_View *)cmd->u.native.privData;
    return JIM_OK;
}

/* Only call this against known view objects. */
static void JimPinView(Jim_Interp *interp, Jim_Obj *obj)
{
    Jim_Cmd *cmd = Jim_GetCommand(interp, obj, 0);
    /* JimPanic((cmd == NULL, "JimPinView called against non-view"))
       JimPanic((cmd->u.native.delProc != JimViewDelProc, "JimPinView called against non-view")) */
    cmd->u.native.cmdProc = JimViewSubCmdProc;
}

static Jim_Obj *JimViewPropertiesList(Jim_Interp *interp, c4_View view)
{
    int i, count;
    Jim_Obj *result;

    result = Jim_NewListObj(interp, NULL, 0);
    count = view.NumProperties();

    for (i = 0; i < count; i++) {
        Jim_ListAppendElement(interp, result, Jim_NewStringObj(interp,
            view.NthProperty(i).Name(), -1));
    }

    return result;
}

/* ----------------------------------------------------------------------------
 * Storage handle
 * ---------------------------------------------------------------------------- */

/* These are also commands, like views, but must be managed explicitly by the
 * user. Quite like file handles, actually.
 */

typedef struct MkStorage {
    unsigned flags;
    Jim_Obj *filename;
    c4_Storage storage;
    c4_Cursor content;
} MkStorage;

#define JIM_MKFLAG_INMEMORY     0x0001
#define JIM_MKFLAG_READONLY     0x0002
#define JIM_MKFLAG_EXTEND       0x0004
#define JIM_MKFLAG_AUTOCOMMIT   0x0008

/* Attributes -------------------------------------------------------------- */

static int storage_cmd_autocommit(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk = (MkStorage *)Jim_CmdPrivData(interp);

    if (argc == 1) {
        jim_wide flag;

        if (Jim_GetWide(interp, argv[0], &flag) != JIM_OK)
            return JIM_ERR;

        if (flag)
            mk->flags |= JIM_MKFLAG_AUTOCOMMIT;
        else
            mk->flags &= ~JIM_MKFLAG_AUTOCOMMIT;
        mk->storage.AutoCommit(flag);
    }

    Jim_SetResultBool(interp, (mk->flags & JIM_MKFLAG_AUTOCOMMIT) != 0);
    return JIM_OK;
}

static int storage_cmd_readonly(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk = (MkStorage *)Jim_CmdPrivData(interp);

    Jim_SetResultBool(interp, (mk->flags & JIM_MKFLAG_READONLY) != 0);
    return JIM_OK;
}

/* Views ------------------------------------------------------------------- */

static int storage_cmd_views(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk = (MkStorage *)Jim_CmdPrivData(interp);

    Jim_SetResult(interp, JimViewPropertiesList(interp, mk->storage));
    return JIM_OK;
}

static int storage_cmd_view(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk = (MkStorage *)Jim_CmdPrivData(interp);
    const c4_Property *propPtr;

    if (JimGetProperty(interp, argv[0], mk->storage, "view", &propPtr) != JIM_OK)
        return JIM_ERR;
    Jim_SetResult(interp, JimGetMkValue(interp, mk->content, *propPtr));

    if (argc == 1)
        return JIM_OK;
    else {
        if (!Jim_CompareStringImmediate(interp, argv[1], "|")) {
            Jim_SetResultFormatted(interp,
                "expected start of a pipeline but got \"%#s\"", argv[1]);
            return JIM_ERR;
        }
        return Jim_EvalObjPrefix(interp, Jim_GetResult(interp), argc - 2, argv + 2);
    }
}

static int storage_cmd_structure(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk = (MkStorage *)Jim_CmdPrivData(interp);

    if (argc < 2) { /* Query */
        const char *name;

        if (argc == 0)
            name = NULL;
        else {
            const c4_Property *propPtr;

            if (JimGetProperty(interp, argv[0], mk->storage, "view", &propPtr) != JIM_OK)
                return JIM_ERR;
            name = propPtr->Name();
        }

        Jim_SetResult(interp, JimFromMkDescription(interp,
            mk->storage.Description(name), NULL));
    }
    else { /* Modify */
        char *descr;
        const char *name;
        int len, dlen;

        if (JimCheckMkName(interp, argv[0], "view") != JIM_OK)
            return JIM_ERR;
        name = Jim_GetString(argv[0], &len);

        if (JimToMkDescription(interp, argv[1], &descr) != JIM_OK)
            return JIM_ERR;
        dlen = strlen(descr);

        descr = (char *)Jim_Realloc(descr, dlen + len + 2);
        memmove(descr + len + 1, descr, dlen);
        memcpy(descr, name, len);
        descr[len] = '[';
        descr[len + 1 + dlen] = ']';
        descr[len + 1 + dlen + 1] = '\0';

        mk->storage.GetAs(descr);

        Jim_Free(descr);
    }

    return JIM_OK;
}

/* Store operations -------------------------------------------------------- */

static int storage_cmd_commit(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk = (MkStorage *)Jim_CmdPrivData(interp);

    if (mk->flags & JIM_MKFLAG_INMEMORY) {
        Jim_SetResultString(interp, "cannot commit an in-memory storage", -1);
        return JIM_ERR;
    }
    else if (mk->flags & JIM_MKFLAG_READONLY) {
        Jim_SetResultString(interp, "cannot commit a read-only storage", -1);
        return JIM_ERR;
    }

    if (mk->storage.Commit(0)) {
        Jim_SetEmptyResult(interp);
        return JIM_OK;
    }
    else {
        Jim_SetResultString(interp, "I/O error during commit", -1);
        return JIM_ERR;
    }
}

static int storage_cmd_rollback(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk = (MkStorage *)Jim_CmdPrivData(interp);

    if (mk->flags & JIM_MKFLAG_INMEMORY) {
        Jim_SetResultString(interp, "cannot rollback an in-memory storage", -1);
        return JIM_ERR;
    }

    if (mk->storage.Rollback(0)) {
        Jim_SetEmptyResult(interp);
        return JIM_OK;
    }
    else {
        Jim_SetResultString(interp, "I/O error during rollback", -1);
        return JIM_ERR;
    }
}

static int storage_cmd_close(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    return Jim_DeleteCommand(interp, Jim_String(argv[0]));
}

/* Command table ----------------------------------------------------------- */

static const jim_subcmd_type storage_command_table[] = {

    /* Options */

    {   "autocommit", "?value?",
        storage_cmd_autocommit,
        0, 1,
        0,
        /*"Query or modify the auto-commit option of this storage"*/
    },
    {   "readonly", "",
        storage_cmd_readonly,
        0, 0,
        0,
        /*"Returns the read-only status of this storage"*/
    },

    /* Views */

    {   "views", "",
        storage_cmd_views,
        0, 0,
        0,
        /*"Returns the list of views stored here"*/
    },
    {   "view", "viewName",
        storage_cmd_view,
        1, -1,
        0,
        /*"Retrieve the view specified by viewName"*/
    },
    {   "structure", "?viewName? ?description?",
        storage_cmd_structure,
        0, 2,
        0,
        /*"Query or modify the structure of this storage"*/
    },

    /* Store operations */

    {   "commit", "",
        storage_cmd_commit,
        0, 0,
        0,
        /*"Commit the changes to disk"*/
    },
    {   "rollback", "",
        storage_cmd_rollback,
        0, 0,
        0,
        /*"Revert to the saved state"*/
    },
    {   "close", "",
        storage_cmd_close,
        0, 0,
        JIM_MODFLAG_FULLARGV,
        /*"Close this storage"*/
    },

    { 0 }
};

static void JimStorageDelProc(Jim_Interp *interp, void *privData)
{
    MkStorage *mk = (MkStorage *)privData;

    mk->storage.~c4_Storage();
    mk->content.~c4_Cursor();
    Jim_DecrRefCount(interp, mk->filename);
    Jim_Free(mk);
}

static int JimStorageSubCmdProc(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    Jim_Obj *cmdObj;

    cmdObj = Jim_NewStringObj(interp, "mk.storage ", -1);
    Jim_AppendObj(interp, cmdObj, argv[1]);

    if (Jim_GetCommand(interp, cmdObj, 0) != NULL) {
        int result;

        Jim_Obj **objv = (Jim_Obj **)Jim_Alloc(argc * sizeof(Jim_Obj *));
        objv[0] = cmdObj;
        objv[1] = argv[0];
        memcpy(objv + 2, argv + 2, (argc - 2) * sizeof(Jim_Obj *));

        result = Jim_EvalObjVector(interp, argc, objv);

        Jim_Free(objv);
        return result;
    } else {
        Jim_FreeNewObj(interp, cmdObj);
        return Jim_CallSubCmd(interp, Jim_ParseSubCmd(interp,
            storage_command_table, argc, argv), argc, argv);
    }
}

/* -------------------------------------------------------------------------
 * storage ?options? ?filename?
 *
 * Creates a new metakit storage object, optionally backed by a file.
 *
 * Options apply only when filename is given; these include:
 *
 *   -readonly   Open the file in read-only mode
 *   -original   Open the file in read-only mode, discarding possible extends
 *   -extend     Open the file in extend mode
 *   -nocommit   Do not commit the changes when the storage is closed
 * ------------------------------------------------------------------------- */

static int JimStorageCommand(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    MkStorage *mk;
    char buf[MK_CMD_LEN];
    int i, mode;

    static const char *const options[] = {
        "-readonly",
        "-original",
        "-extend",
        "-nocommit",
        0
    };
    enum {
        OPT_READONLY,
        OPT_ORIGINAL,
        OPT_EXTEND,
        OPT_NOCOMMIT
    };
    int option;

    mk = (MkStorage *)Jim_Alloc(sizeof(MkStorage));
    mk->flags = JIM_MKFLAG_AUTOCOMMIT;
    mode = MK_MODE_READWRITE;
    for (i = 1; i < argc - 1; i++ ) {
        if (Jim_GetEnum(interp, argv[i], options, &option, NULL, JIM_ERRMSG) != JIM_OK) {
            Jim_Free(mk);
            return JIM_ERR;
        }

        switch (option) {
            case OPT_READONLY:
                if (mode != MK_MODE_READWRITE)
                    goto modeconflict;

                mode = MK_MODE_READONLY;
                mk->flags |= JIM_MKFLAG_READONLY;
                break;

            case OPT_ORIGINAL:
                if (mode != MK_MODE_READWRITE)
                    goto modeconflict;

                mode = MK_MODE_ORIGINAL;
                mk->flags |= JIM_MKFLAG_READONLY;
                break;

            case OPT_EXTEND:
                if (mode != MK_MODE_READWRITE)
                    goto modeconflict;

                mode = MK_MODE_EXTEND;
                mk->flags |= JIM_MKFLAG_EXTEND;
                break;

            case OPT_NOCOMMIT:
                mk->flags &= ~JIM_MKFLAG_AUTOCOMMIT;
                break;
        }
    }

    if (argc > 1) {
        new(&mk->storage) c4_Storage(Jim_String(argv[argc-1]), mode);

        if (!mk->storage.Strategy().IsValid()) {
            mk->storage.~c4_Storage();
            Jim_Free(mk);
            Jim_SetResultFormatted(interp, "could not open storage \"%#s\"", argv[argc-1]);
            return JIM_ERR;
        }

        mk->filename = argv[argc-1];

        if ((mk->flags & JIM_MKFLAG_AUTOCOMMIT) && !(mk->flags & JIM_MKFLAG_READONLY))
            mk->storage.AutoCommit(1);
    }
    else {
        mk->flags |= JIM_MKFLAG_INMEMORY;

        new(&mk->storage) c4_Storage();
        mk->filename = Jim_NewEmptyStringObj(interp);
    }
    new(&mk->content) c4_Cursor(&mk->storage[0]);
    Jim_IncrRefCount(mk->filename);

    snprintf(buf, sizeof(buf), "mk.handle%ld", Jim_GetId(interp));
    Jim_CreateCommand(interp, buf, JimStorageSubCmdProc, mk, JimStorageDelProc);
    Jim_SetResultString(interp, buf, -1);
    return JIM_OK;

  modeconflict:
    Jim_Free(mk);
    Jim_SetResultString(interp, "only one of -readonly, -original and -extend may be specified", -1);
    return JIM_ERR;
}

/* -------------------------------------------------------------------------
 * Initialization code
 * ------------------------------------------------------------------------- */

int Jim_mkInit(Jim_Interp *interp)
{
    char version[MK_VERSION_SPACE];

    snprintf(version, MK_VERSION_SPACE, "%d.%d.%d",
        d4_MetakitLibraryVersion / 100,
        d4_MetakitLibraryVersion % 100 / 10,
        d4_MetakitLibraryVersion % 10);

    if (Jim_PackageProvide(interp, "mk", version, JIM_ERRMSG))
        return JIM_ERR;

    Jim_CreateCommand(interp, "storage", JimStorageCommand, NULL, NULL);
    Jim_CreateCommand(interp, "cursor", JimCursorCommand, NULL, NULL);
    Jim_CreateCommand(interp, "mk.view.finalizer", JimViewFinalizerProc, NULL, NULL);

    return JIM_OK;
}

} /* extern "C" */
