/* vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: */
/*
 * Copyright 2017 Red Hat, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "jose.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <ctype.h>

#define SUMMARY "Converts JSON between serialization formats"
#define JAIN json_array_insert_new

#ifdef __MINGW32__
#define sscanf __mingw_sscanf
#endif

static const char *prefix = "jose fmt [OPTIONS]\n\n" SUMMARY;

typedef struct {
    json_t *args;
} jcmd_opt_t;

static size_t
convert_int(const json_t *arr, const char *arg)
{
    ssize_t indx = 0;

    if (sscanf(arg, "%zd", &indx) != 1)
        return SIZE_MAX;

    if (indx < 0)
        indx += json_array_size(arr);

    if (indx < 0)
        return SIZE_MAX;

    return indx;
}

static void
jcmd_opt_cleanup(jcmd_opt_t *opt)
{
    json_decref(opt->args);
}

static bool
cmd_output(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    const int wflags = JSON_ENCODE_ANY | JSON_COMPACT | JSON_SORT_KEYS;
    const char *s = json_string_value(arg);
    FILE *file = NULL;
    bool ret = false;

    if (strcmp(s, "-") == 0)
        file = stdout;
    else
        file = fopen(s, "w");
    if (!file)
        return false;

    if (json_dumpf(cur, file, wflags) < 0)
        goto egress;

    if (isatty(fileno(file)) && fwrite("\n", 1, 1, file) != 1)
        goto egress;

    ret = true;

egress:
    if (strcmp(s, "-") != 0)
        fclose(file);
    return ret;
}

static bool
cmd_foreach(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    const int wflags = JSON_ENCODE_ANY | JSON_COMPACT | JSON_SORT_KEYS;
    const char *s = json_string_value(arg);
    FILE *file = NULL;
    bool ret = false;

    if (!json_is_array(cur) && !json_is_object(cur))
        return false;

    if (strcmp(s, "-") == 0)
        file = stdout;
    else
        file = fopen(s, "w");
    if (!file)
        return false;

    if (json_is_array(cur)) {
        json_t *v = NULL;
        size_t i = 0;

        json_array_foreach(cur, i, v) {
            if (json_dumpf(v, file, wflags) < 0 ||
                fprintf(file, "\n") < 0)
                goto egress;
        }
    } else if (json_is_object(cur)) {
        const char *k = NULL;
        json_t *v = NULL;

        json_object_foreach(cur, k, v) {
            if (fprintf(file, "%s=", k) < 0 ||
                json_dumpf(v, file, wflags) < 0 ||
                fprintf(file, "\n") < 0)
                goto egress;
        }
    }

    ret = true;

egress:
    if (strcmp(s, "-") != 0)
        fclose(file);
    return ret;
}

static bool
cmd_unquote(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    const char *s = json_string_value(arg);
    FILE *file = NULL;
    bool ret = false;

    if (!json_is_string(cur))
        return false;

    if (strcmp(s, "-") == 0)
        return fprintf(stdout, "%s\n", json_string_value(cur)) >= 0;

    file = fopen(s, "w");
    if (!file)
        return false;

    ret = fprintf(file, "%s\n", json_string_value(cur)) >= 0;
    fclose(file);
    return ret;
}

static bool
cmd_move(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    json_int_t i = json_integer_value(arg);

    if (json_array_insert(stk, i + 1, cur) < 0)
        return false;

    if (json_array_remove(stk, 0) < 0)
        return false;

    return true;
}

static bool
cmd_trunc(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    size_t i = json_integer_value(arg);
    size_t s;

    for (s = json_array_size(cur); s > i; s--) {
        if (json_array_remove(cur, s - 1) < 0)
            return false;
    }

    return true;
}

static bool
cmd_insert(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    size_t i = json_integer_value(arg);
    return json_array_insert(lst, i, cur) >= 0;
}

static bool
cmd_append(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    if (json_is_array(lst))
        return json_array_append(lst, cur) >= 0;

    if (json_is_object(lst))
        return json_object_update_missing(lst, cur) >= 0;

    return false;
}

static bool
cmd_extend(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    if (json_is_array(lst))
        return json_array_extend(lst, cur) >= 0;

    if (json_is_object(lst))
        return json_object_update(lst, cur) >= 0;

    return false;
}

static bool
cmd_delete(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    const char *s = json_string_value(arg);

    if (json_is_array(cur)) {
        size_t indx;

        indx = convert_int(cur, s);
        if (indx == SIZE_MAX)
            return false;

        return json_array_remove(cur, indx) >= 0;
    }

    if (json_is_object(cur))
        return json_object_del(cur, s) >= 0;

    return false;
}

static bool
cmd_length(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    size_t count = 0;

    if (json_is_array(cur))
        count = json_array_size(cur);
    else if (json_is_object(cur))
        count = json_object_size(cur);
    else if (json_is_string(cur))
        count = json_string_length(cur);
    else
        return false;

    return json_array_insert_new(stk, 0, json_integer(count)) >= 0;
}

static bool
cmd_empty(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    if (json_is_array(cur))
        return json_array_clear(cur) >= 0;

    if (json_is_object(cur))
        return json_object_clear(cur) >= 0;

    return false;
}

static bool
cmd_get(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    const char *s = json_string_value(arg);
    json_t *v = NULL;

    if (json_is_array(cur)) {
        size_t indx;

        indx = convert_int(cur, s);
        if (indx == SIZE_MAX)
            return false;

        v = json_array_get(cur, indx);
    } else if (json_is_object(cur)) {
        v = json_object_get(cur, s);
    } else {
        return false;
    }

    return json_array_insert(stk, 0, v) >= 0;
}

static bool
cmd_set(const json_t *arg, json_t *stk, json_t *cur, json_t *lst)
{
    const char *s = json_string_value(arg);

    if (json_is_array(lst)) {
        size_t indx;

        indx = convert_int(lst, s);
        if (indx == SIZE_MAX)
            return false;

        return json_array_set(lst, indx, cur) >= 0;
    }

    if (json_is_object(lst))
        return json_object_set(lst, s, cur) >= 0;

    return false;
}

static const jcmd_doc_t doc_not[] = {
    { .doc = "Invert the following assertion" },
    {}
};

static const jcmd_doc_t doc_object[] = {
    { .doc = "Assert TOP to be an object" },
    {}
};

static const jcmd_doc_t doc_array[] = {
    { .doc = "Assert TOP to be an array" },
    {}
};

static const jcmd_doc_t doc_string[] = {
    { .doc = "Assert TOP to be a string" },
    {}
};

static const jcmd_doc_t doc_int[] = {
    { .doc = "Assert TOP to be an integer" },
    {}
};

static const jcmd_doc_t doc_real[] = {
    { .doc = "Assert TOP to be a real" },
    {}
};

static const jcmd_doc_t doc_number[] = {
    { .doc = "Assert TOP to be a number" },
    {}
};

static const jcmd_doc_t doc_true[] = {
    { .doc = "Assert TOP to be true" },
    {}
};

static const jcmd_doc_t doc_false[] = {
    { .doc = "Assert TOP to be false" },
    {}
};

static const jcmd_doc_t doc_bool[] = {
    { .doc = "Assert TOP to be a boolean" },
    {}
};

static const jcmd_doc_t doc_null[] = {
    { .doc = "Assert TOP to be null" },
    {}
};

static const jcmd_doc_t doc_equal[] = {
    { .doc = "Assert TOP to be equal to PREV" },
    {}
};

static const jcmd_doc_t doc_json[] = {
    { .arg = "JSON", .doc = "Parse JSON constant, push onto TOP" },
    { .arg = "FILE", .doc = "Read from FILE, push onto TOP" },
    { .arg = "-",    .doc = "Read from STDIN, push onto TOP" },
    {}
};

static const jcmd_doc_t doc_quote[] = {
    { .arg = "STR",  .doc = "Convert STR to a string, push onto TOP" },
    {}
};

static const jcmd_doc_t doc_output[] = {
    { .arg = "FILE", .doc = "Write TOP to FILE" },
    { .arg = "-",    .doc = "Write TOP to STDOUT" },
    {}
};

static const jcmd_doc_t doc_foreach[] = {
    { .arg = "FILE", .doc = "Write TOP (obj./arr.) to FILE, one line/item" },
    { .arg = "-",    .doc = "Write TOP (obj./arr.) to STDOUT, one line/item" },
    {}
};

static const jcmd_doc_t doc_unquote[] = {
    { .arg = "FILE", .doc = "Write TOP (str.) to FILE without quotes" },
    { .arg = "-",    .doc = "Write TOP (str.) to STDOUT without quotes" },
    {}
};

static const jcmd_doc_t doc_copy[] = {
    { .doc = "Deep copy TOP, push onto TOP" },
    {}
};

static const jcmd_doc_t doc_query[] = {
    { .doc = "Query the stack by deep copying and pushing onto TOP" },
    {}
};

static const jcmd_doc_t doc_move[] = {
    { .arg = "#", .doc = "Move TOP back # places on the stack" },
    {}
};

static const jcmd_doc_t doc_unwind[] = {
    { .doc = "Discard TOP from the stack" },
    {}
};

static const jcmd_doc_t doc_trunc[] = {
    { .arg = "#",  .doc = "Shrink TOP (arr.) to length #" },
    { .arg = "-#", .doc = "Discard last # items from TOP (arr.)" },
    {}
};

static const jcmd_doc_t doc_insert[] = {
    { .arg = "#", .doc = "Insert TOP into PREV (arr.) at #" },
    {}
};

static const jcmd_doc_t doc_append[] = {
    { .doc = "Append TOP to the end of PREV (arr.)" },
    { .doc = "Set missing values from TOP (obj.) into PREV (obj.)" },
    {}
};

static const jcmd_doc_t doc_extend[] = {
    { .doc = "Append items from TOP to the end of PREV (arr.)" },
    { .doc = "Set all values from TOP (obj.) into PREV (obj.)" },
    {}
};

static const jcmd_doc_t doc_delete[] = {
    { .arg = "NAME", .doc = "Delete NAME from TOP (obj.)" },
    { .arg = "#",    .doc = "Delete # from TOP (arr.)" },
    { .arg = "-#",   .doc = "Delete # from the end of TOP (arr.)" },
    {}
};

static const jcmd_doc_t doc_length[] = {
    { .doc = "Push length of TOP (arr./str./obj.) to TOP" },
    {}
};

static const jcmd_doc_t doc_empty[] = {
    { .doc = "Erase all items from TOP (arr./obj.)" },
    {}
};

static const jcmd_doc_t doc_get[] = {
    { .arg = "NAME", .doc = "Get item with NAME from TOP (obj.), push to TOP" },
    { .arg = "#",    .doc = "Get # item from TOP (arr.), push to TOP" },
    { .arg = "-#",   .doc = "Get # item from the end of TOP (arr.), push to TOP" },
    {}
};

static const jcmd_doc_t doc_set[] = {
    { .arg = "NAME", .doc = "Sets TOP into PREV (obj.) with NAME" },
    { .arg = "#",    .doc = "Sets TOP into PREV (obj.) at #" },
    { .arg = "-#",   .doc = "Sets TOP into PREV (obj.) at # from the end" },
    {}
};

static const jcmd_doc_t doc_b64l[] = {
    { .doc = "URL-safe Base64 decode TOP (str.), push onto TOP" },
    {}
};

static const jcmd_doc_t doc_b64d[] = {
    { .doc = "URL-safe Base64 encode TOP, push onto TOP" },
    {}
};

static bool
opt_set_null(const jcmd_cfg_t *cfg, void *vopt, const char *arg)
{
    json_t **x = vopt;
    if (!*x) *x = json_array();
    return json_array_append_new(*x, json_pack("[i,n]", cfg->opt.val)) >= 0;
}


static bool
opt_set_str(const jcmd_cfg_t *cfg, void *vopt, const char *arg)
{
    json_t **x = vopt;
    if (!*x) *x = json_array();
    return json_array_append_new(*x, json_pack("[i,s]", cfg->opt.val, arg)) >= 0;
}

static bool
opt_set_int(const jcmd_cfg_t *cfg, void *vopt, const char *arg)
{
    json_t **x = vopt;
    json_int_t j = 0;
    int i = 0;

    if (sscanf(arg, "%d", &i) != 1)
        return false;

    j = i;
    if (!*x) *x = json_array();
    return json_array_append_new(*x, json_pack("[i,I]", cfg->opt.val, j)) >= 0;
}

static bool
opt_set_uint(const jcmd_cfg_t *cfg, void *vopt, const char *arg)
{
    unsigned int i = 0;
    json_t **x = vopt;
    json_int_t j = 0;

    if (sscanf(arg, "%u", &i) != 1)
        return false;

    j = i;
    if (!*x) *x = json_array();
    return json_array_append_new(*x, json_pack("[i,I]", cfg->opt.val, j)) >= 0;
}

static bool
opt_set_json(const jcmd_cfg_t *cfg, void *vopt, const char *arg)
{
    json_auto_t *j = NULL;
    json_t **x = vopt;

    if (!jcmd_opt_set_json(cfg, &j, arg))
        return false;

    if (!*x) *x = json_array();
    return json_array_append_new(*x, json_pack("[i,O]", cfg->opt.val, j)) >= 0;
}

static const jcmd_cfg_t cfgs[] = {
    { .opt = { "not",      no_argument,       .val = 'X' }, .doc = doc_not,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "object",   no_argument,       .val = 'O' }, .doc = doc_object,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "array",    no_argument,       .val = 'A' }, .doc = doc_array,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "string",   no_argument,       .val = 'S' }, .doc = doc_string,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "integer",  no_argument,       .val = 'I' }, .doc = doc_int,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "real",     no_argument,       .val = 'R' }, .doc = doc_real,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "number",   no_argument,       .val = 'N' }, .doc = doc_number,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "true",     no_argument,       .val = 'T' }, .doc = doc_true,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "false",    no_argument,       .val = 'F' }, .doc = doc_false,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "boolean",  no_argument,       .val = 'B' }, .doc = doc_bool,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "null",     no_argument,       .val = '0' }, .doc = doc_null,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "equal",    no_argument,       .val = 'E' }, .doc = doc_equal,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },

    { .opt = { "query",    no_argument,       .val = 'Q' }, .doc = doc_query,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "move",     required_argument, .val = 'M' }, .doc = doc_move,
      .set = opt_set_uint, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "unwind",   no_argument,       .val = 'U' }, .doc = doc_unwind,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },

    { .opt = { "json",     required_argument, .val = 'j' }, .doc = doc_json,
      .set = opt_set_json, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "copy",     no_argument,       .val = 'c' }, .doc = doc_copy,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "quote",    required_argument, .val = 'q' }, .doc = doc_quote,
      .set = opt_set_str,  .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "output",   required_argument, .val = 'o' }, .doc = doc_output,
      .set = opt_set_str,  .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "foreach",  required_argument, .val = 'f' }, .doc = doc_foreach,
      .set = opt_set_str,  .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "unquote",  required_argument, .val = 'u' }, .doc = doc_unquote,
      .set = opt_set_str,  .off = offsetof(jcmd_opt_t, args) },

    { .opt = { "truncate", required_argument, .val = 't' }, .doc = doc_trunc,
      .set = opt_set_int,  .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "insert",   required_argument, .val = 'i' }, .doc = doc_insert,
      .set = opt_set_uint, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "append",   no_argument,       .val = 'a' }, .doc = doc_append,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "extend",   no_argument,       .val = 'x' }, .doc = doc_extend,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },

    { .opt = { "delete",   required_argument, .val = 'd' }, .doc = doc_delete,
      .set = opt_set_str,  .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "length",   no_argument,       .val = 'l' }, .doc = doc_length,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "empty",    no_argument,       .val = 'e' }, .doc = doc_empty,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "get",      required_argument, .val = 'g' }, .doc = doc_get,
      .set = opt_set_str,  .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "set",      required_argument, .val = 's' }, .doc = doc_set,
      .set = opt_set_str,  .off = offsetof(jcmd_opt_t, args) },

    { .opt = { "b64load",  no_argument,       .val = 'y' }, .doc = doc_b64l,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },
    { .opt = { "b64dump",  no_argument,       .val = 'Y' }, .doc = doc_b64d,
      .set = opt_set_null, .off = offsetof(jcmd_opt_t, args) },

    {}
};

static int
jcmd_fmt(int argc, char *argv[])
{
    json_auto_t *stk = json_array();
    jcmd_opt_auto_t opt = {};
    unsigned char ret = 0;
    bool not = false;

    if (!jcmd_opt_parse(argc, argv, cfgs, &opt, prefix))
        return -1;

    for (size_t i = 0; i < json_array_size(opt.args); i++) {
        json_t *lst = NULL;
        json_t *cur = NULL;
        json_t *p = NULL;
        bool ok = false;
        int o = 0;

        if (json_unpack(json_array_get(opt.args, i), "[i,o!]", &o, &p) < 0)
            return ++ret;

        if (not && !strchr("OASIRNTFB0E", o))
            return ret;

        cur = json_array_get(stk, 0);
        lst = json_array_get(stk, 1);
        ret++;

        switch (o) {
        case 'X': ok = not = true;                                break;
        case 'O': ok = not ^ json_is_object(cur);  not = false;   break;
        case 'A': ok = not ^ json_is_array(cur);   not = false;   break;
        case 'S': ok = not ^ json_is_string(cur);  not = false;   break;
        case 'I': ok = not ^ json_is_integer(cur); not = false;   break;
        case 'R': ok = not ^ json_is_real(cur);    not = false;   break;
        case 'N': ok = not ^ json_is_number(cur);  not = false;   break;
        case 'T': ok = not ^ json_is_true(cur);    not = false;   break;
        case 'F': ok = not ^ json_is_false(cur);   not = false;   break;
        case 'B': ok = not ^ json_is_boolean(cur); not = false;   break;
        case '0': ok = not ^ json_is_null(cur);    not = false;   break;
        case 'E': ok = not ^ json_equal(cur, lst); not = false;   break;
        case 'Q': ok = JAIN(stk, 0, json_deep_copy(stk)) >= 0;    break;
        case 'M': ok = cmd_move(p, stk, cur, lst);                break;
        case 'U': ok = json_array_remove(stk, 0) >= 0;            break;
        case 'j': ok = json_array_insert(stk, 0, p) >= 0;         break;
        case 'c': ok = JAIN(stk, 0, json_deep_copy(cur)) >= 0;    break;
        case 'q': ok = json_array_insert(stk, 0, p) >= 0;         break;
        case 'o': ok = cmd_output(p, stk, cur, lst);              break;
        case 'f': ok = cmd_foreach(p, stk, cur, lst);             break;
        case 'u': ok = cmd_unquote(p, stk, cur, lst);             break;
        case 't': ok = cmd_trunc(p, stk, cur, lst);               break;
        case 'i': ok = cmd_insert(p, stk, cur, lst);              break;
        case 'a': ok = cmd_append(p, stk, cur, lst);              break;
        case 'x': ok = cmd_extend(p, stk, cur, lst);              break;
        case 'd': ok = cmd_delete(p, stk, cur, lst);              break;
        case 'l': ok = cmd_length(p, stk, cur, lst);              break;
        case 'e': ok = cmd_empty(p, stk, cur, lst);               break;
        case 'g': ok = cmd_get(p, stk, cur, lst);                 break;
        case 's': ok = cmd_set(p, stk, cur, lst);                 break;
        case 'Y': ok = JAIN(stk, 0, jose_b64_enc_dump(cur)) >= 0; break;
        case 'y': ok = JAIN(stk, 0, jose_b64_dec_load(cur)) >= 0; break;
        default:  ok = false;                                     break;
        }

        if (!ok)
            return ret;
    }

    if (not)
        return ret;

    return EXIT_SUCCESS;
}

JCMD_REGISTER(SUMMARY, jcmd_fmt, "fmt")
