#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <uchar.h>
#include <err.h>

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "memory.h"
#include "dupstr.h"
#include "fake-lib.h"

typedef struct MsiTypePrefix {
    enum { MAIN, VIEW, RECORD } type;
} MsiTypePrefix;

typedef struct MsiMainCtx {
    MsiTypePrefix t;

    char *tempdir;
    char *outfile;

    char **args;
    int nargs, argsize;
} MsiMainCtx;


uint32_t MsiOpenDatabaseW(const char16_t *filename,
                          const char16_t *persist,
                          MsiMainCtx **out_ctx)
{
    MsiMainCtx *ctx = snew(MsiMainCtx);
    ctx->t.type = MAIN;
    ctx->outfile = ascii(filename, true);
    close(open(ctx->outfile, O_CREAT | O_WRONLY, 0666));
    ctx->outfile = realpath(ctx->outfile, NULL);
    unlink(ctx->outfile);
    ctx->tempdir = dupcat(ctx->outfile, "-msiXXXXXX", cNULL);
    if (!mkdtemp(ctx->tempdir))
        err(1, "%s: mkdtemp", ctx->tempdir);
    ctx->nargs = 0;
    ctx->argsize = 16;
    ctx->args = snewn(ctx->argsize, char *);
    ctx->args[ctx->nargs++] = dupstr("sh");
    ctx->args[ctx->nargs++] = dupstr("-c");
    ctx->args[ctx->nargs++] = dupstr("cd \"$0\" && \"$@\"");
    ctx->args[ctx->nargs++] = dupstr(ctx->tempdir);
    ctx->args[ctx->nargs++] = dupstr("msibuild");
    ctx->args[ctx->nargs++] = dupstr(ctx->outfile);
    *out_ctx = ctx;
    return 0;
}

uint32_t MsiDatabaseImportW(MsiMainCtx *ctx, const char16_t *folder,
                            const char16_t *file)
{
    assert(ctx->t.type == MAIN);
    system_argv("sh", "-c", "cd \"$0\" && cp \"$1\" \"$2\"",
                ascii(folder, true), ascii(file, true), ctx->tempdir, cNULL);
    if (ctx->nargs + 2 >= ctx->argsize) {
        ctx->argsize = ctx->nargs * 5 / 4 + 16;
        ctx->args = sresize(ctx->args, ctx->argsize, char *);
    }
    ctx->args[ctx->nargs++] = dupstr("-i");
    ctx->args[ctx->nargs++] = dupcat(ctx->tempdir, "/", ascii(file, true),
                                     cNULL);
    return 0;
}

typedef struct MsiView {
    MsiTypePrefix t;

    FILE *fp;
    char *targetdir;
    MsiMainCtx *ctx;
} MsiView;

uint32_t MsiDatabaseOpenViewW(MsiMainCtx *ctx, const char16_t *query,
                              MsiView **outview)
{
    assert(ctx->t.type == MAIN);
    MsiView *view = snew(MsiView);
    view->t.type = VIEW;
    view->ctx = ctx;
    char *cquery = ascii(query, false);
    if (!strcmp(cquery, "SELECT  `Name`, `Data` FROM `_Streams`"))
        view->fp = NULL;               /* special case */
    else {
        if (!strcmp(cquery, "SELECT  `Name`, `Data` FROM `Binary`")) {
            view->fp = fopen(dupcat(ctx->tempdir, "/", "Binary.idt", cNULL),
                             "a");
            view->targetdir = dupcat(ctx->tempdir, "/", "Binary", cNULL);
        } else if (!strcmp(cquery, "SELECT  `Name`, `Data` FROM `Icon`")) {
            view->fp = fopen(dupcat(ctx->tempdir, "/", "Icon.idt", cNULL),
                             "a");
            view->targetdir = dupcat(ctx->tempdir, "/", "Icon", cNULL);
        } else
            errx(1, "unrecognised query: %s", cquery);
        if (!view->fp)
            err(1, "open");
        if (mkdir(view->targetdir, 0777) < 0)
            err(1, "%s: mkdir", view->targetdir);
    }
    *outview = view;
    return 0;
}

uint32_t MsiViewExecute(MsiView *view, void *params)
{
    assert(view->t.type == VIEW);
    return 0;
}

typedef struct MsiRecord {
    MsiTypePrefix t;

    char *name, *data;
} MsiRecord;

MsiRecord *MsiCreateRecord(uint32_t nparams)
{
    MsiRecord *rec = snew(MsiRecord);
    rec->t.type = RECORD;

    if (nparams != 2)
        errx(1, "bad MsiCreateRecord param count %u", (unsigned)nparams);
    rec->name = rec->data = NULL;
    return rec;
}

uint32_t MsiRecordSetStringW(MsiRecord *rec, uint32_t field, char16_t *value)
{
    assert(rec->t.type == RECORD);
    if (field != 1)
        errx(1, "bad MsiRecordSetString param index %u", (unsigned)field);
    rec->name = ascii(value, false);
    return 0;
}

uint32_t MsiRecordSetStreamW(MsiRecord *rec, uint32_t field, char16_t *path)
{
    assert(rec->t.type == RECORD);
    if (field != 2)
        errx(1, "bad MsiRecordSetStream param index %u", (unsigned)field);
    rec->data = ascii(path, true);
    return 0;
}

uint32_t MsiViewModify(MsiView *view, uint32_t mode, MsiRecord *rec)
{
    assert(view->t.type == VIEW);
    assert(rec->t.type == RECORD);
    if (view->fp) {
        system_argv("sh", "-c", "cp \"$0\" \"$1\"/\"$2\"",
                    rec->data, view->targetdir, rec->name, cNULL);
        fprintf(view->fp, "%s\t%s\r\n", rec->name, rec->name);
    } else {
        MsiMainCtx *ctx = view->ctx;
        if (ctx->nargs + 3 >= ctx->argsize) {
            ctx->argsize = ctx->nargs * 5 / 4 + 16;
            ctx->args = sresize(ctx->args, ctx->argsize, char *);
        }
        ctx->args[ctx->nargs++] = dupstr("-a");
        ctx->args[ctx->nargs++] = dupstr(rec->name);
        ctx->args[ctx->nargs++] = dupstr(rec->data);
    }
    return 0;
}

uint32_t MsiCloseHandle(MsiTypePrefix *t)
{
    if (t->type == VIEW) {
        MsiView *view = (MsiView *)t;
        if (view->fp)
            fclose(view->fp);
    }
    return 0;
}

uint32_t MsiDatabaseCommit(MsiMainCtx *ctx)
{
    assert(ctx->t.type == MAIN);
    printf("commit:");
    for (int i = 0; i < ctx->nargs; i++) {
        printf(" '");
        for (const char *p = ctx->args[i]; *p; p++) {
            if (*p == '\'')
                printf("'\\''");
            else
                putchar(*p);
        }
        printf("'");
    }
    printf("\n");

    if (ctx->nargs + 1 >= ctx->argsize) {
        ctx->argsize = ctx->nargs * 5 / 4 + 16;
        ctx->args = sresize(ctx->args, ctx->argsize, char *);
    }
    ctx->args[ctx->nargs++] = NULL;
    system_argv_array(ctx->args);
    return 0;
}