/* Disposable LMDB 0.9.33 page-reuse experiment. No existing environment accepted. */
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "lmdb.h"
#define KEYS 8
#define VALUE_BYTES 16384
#define UPDATES 256
#define FOLLOWUP 64
static void check(int rc) {
    if (rc) { fprintf(stderr, "%s (%d)\n", mdb_strerror(rc), rc); exit(1); }
}
static int put(MDB_txn *txn, MDB_dbi db, int key, unsigned char value) {
    unsigned char payload[VALUE_BYTES];
    memset(payload, value, sizeof(payload));
    MDB_val k = {sizeof(key), &key}, v = {sizeof(payload), payload};
    return mdb_put(txn, db, &k, &v, 0);
}
static void verify(MDB_txn *txn, MDB_dbi db, const unsigned char expected[KEYS]) {
    MDB_stat stat;
    check(mdb_stat(txn, db, &stat));
    if (stat.ms_entries != KEYS) exit(2);
    for (int key = 0; key < KEYS; key++) {
        MDB_val k = {sizeof(key), &key}, v;
        check(mdb_get(txn, db, &k, &v));
        if (v.mv_size != VALUE_BYTES) exit(2);
        for (size_t i = 0; i < v.mv_size; i++)
            if (((unsigned char *)v.mv_data)[i] != expected[key]) exit(2);
    }
}
static size_t high_water(MDB_env *env, unsigned page_size) {
    MDB_envinfo info;
    check(mdb_env_info(env, &info));
    return (info.me_last_pgno + 1) * page_size;
}
static int update(MDB_env *env, MDB_dbi db, int key, unsigned char value,
                  const char **stage) {
    MDB_txn *txn;
    *stage = "begin";
    int rc = mdb_txn_begin(env, NULL, 0, &txn);
    if (rc) return rc;
    *stage = "put";
    rc = put(txn, db, key, value);
    if (rc) { mdb_txn_abort(txn); return rc; }
    *stage = "commit";
    return mdb_txn_commit(txn); /* Commit consumes the handle, including on error. */
}
int main(int argc, char **argv) {
    if (argc != 3) return 2;
    const char *mode = argv[1];
    if (strcmp(mode, "released") && strcmp(mode, "pinned") && strcmp(mode, "reset"))
        return 2;
    size_t map_bytes = (size_t)strtoull(argv[2], NULL, 10);
    if (map_bytes != 1048576 && map_bytes != 2097152) return 2;
    char path[] = "/tmp/hesela-lmdb-XXXXXX";
    if (!mkdtemp(path)) return 1;
    MDB_env *env;
    MDB_txn *txn, *reader;
    MDB_dbi db;
    MDB_stat stat;
    unsigned char original[KEYS], expected[KEYS];
    check(mdb_env_create(&env));
    check(mdb_env_set_mapsize(env, map_bytes));
    /* NOTLS allows the read handle and the writer on this one test thread. */
    check(mdb_env_open(env, path, MDB_NOTLS, 0600));
    check(mdb_txn_begin(env, NULL, 0, &txn));
    check(mdb_dbi_open(txn, NULL, 0, &db));
    for (int k = 0; k < KEYS; k++) {
        original[k] = expected[k] = (unsigned char)k;
        check(put(txn, db, k, original[k]));
    }
    check(mdb_txn_commit(txn));
    check(mdb_env_stat(env, &stat));
    check(mdb_txn_begin(env, NULL, MDB_RDONLY, &reader));
    verify(reader, db, original);
    if (!strcmp(mode, "released")) { mdb_txn_abort(reader); reader = NULL; }
    else if (!strcmp(mode, "reset")) mdb_txn_reset(reader);
    size_t initial = high_water(env, stat.ms_psize);
    printf("{\"mode\":\"%s\",\"map_bytes\":%zu,\"page_size\":%u,"
           "\"logical_value_bytes\":%d,\"initial_high_water_bytes\":%zu,"
           "\"trace\":[", mode, map_bytes, stat.ms_psize, KEYS * VALUE_BYTES, initial);
    int completed = 0, rc = 0;
    const char *stage = "none";
    for (int i = 0; i < UPDATES; i++) {
        int key = i % KEYS;
        unsigned char value = (unsigned char)((i + 20) % 251);
        rc = update(env, db, key, value, &stage);
        if (rc) break;
        expected[key] = value;
        completed++;
        printf("%s[%d,%zu]", i ? "," : "", completed, high_water(env, stat.ms_psize));
    }
    if (rc && rc != MDB_MAP_FULL) check(rc);
    int dead = -1;
    check(mdb_reader_check(env, &dead));
    if (dead != 0) exit(2);
    if (!strcmp(mode, "pinned")) verify(reader, db, original);
    check(mdb_txn_begin(env, NULL, MDB_RDONLY, &txn));
    verify(txn, db, expected);
    mdb_txn_abort(txn);
    size_t before = high_water(env, stat.ms_psize);
    int retry_rc = 0;
    if (rc) {
        retry_rc = update(env, db, completed % KEYS, 250, &stage);
        if (retry_rc != MDB_MAP_FULL) exit(2);
    }
    if (reader) mdb_txn_abort(reader);
    int post = 0;
    for (int i = 0; i < FOLLOWUP; i++) {
        int key = i % KEYS;
        unsigned char value = (unsigned char)((i + 90) % 251);
        check(update(env, db, key, value, &stage));
        expected[key] = value;
        post++;
    }
    check(mdb_txn_begin(env, NULL, MDB_RDONLY, &txn));
    verify(txn, db, expected);
    mdb_txn_abort(txn);
    size_t after = high_water(env, stat.ms_psize);
    MDB_envinfo info;
    check(mdb_env_info(env, &info));
    if (info.me_mapsize != map_bytes) exit(2);
    printf("],\"updates_committed\":%d,\"failure_rc\":%d,"
           "\"retry_after_reader_check_rc\":%d,\"dead_readers_cleared\":%d,"
           "\"before_release_high_water_bytes\":%zu,"
           "\"after_release_high_water_bytes\":%zu,"
           "\"post_release_updates\":%d,\"full_payload_oracle\":true,"
           "\"pinned_snapshot_unchanged\":%s,\"map_unchanged\":true}\n",
           completed, rc, retry_rc, dead, before, after, post,
           !strcmp(mode, "pinned") ? "true" : "null");
    mdb_env_close(env);
    char file[128];
    snprintf(file, sizeof(file), "%s/data.mdb", path);
    if (unlink(file)) return 1;
    snprintf(file, sizeof(file), "%s/lock.mdb", path);
    if (unlink(file) || rmdir(path)) return 1;
    return 0;
}
