/*
 * out-limd.c
 * libplist *output-only* format introduced by libimobiledevice/ideviceinfo
 *  - NOT for machine parsing
 *
 * Copyright (c) 2022-2023 Nikias Bassen All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include <inttypes.h>
#include <ctype.h>
#include <math.h>
#include <limits.h>

#include <node.h>

#include "plist.h"
#include "strbuf.h"
#include "time64.h"
#include "base64.h"

#define MAC_EPOCH 978307200

static size_t dtostr(char *buf, size_t bufsize, double realval)
{
    size_t len = 0;
    if (isnan(realval)) {
        len = snprintf(buf, bufsize, "nan");
    } else if (isinf(realval)) {
        len = snprintf(buf, bufsize, "%cinfinity", (realval > 0.0) ? '+' : '-');
    } else if (realval == 0.0f) {
        len = snprintf(buf, bufsize, "0.0");
    } else {
        size_t i = 0;
        len = snprintf(buf, bufsize, "%.*g", 17, realval);
        for (i = 0; buf && i < len; i++) {
            if (buf[i] == ',') {
                buf[i] = '.';
                break;
            } else if (buf[i] == '.') {
                break;
            }
        }
    }
    return len;
}

static plist_err_t node_to_string(node_t node, bytearray_t **outbuf, uint32_t depth, uint32_t indent)
{
    plist_data_t node_data = NULL;

    char *val = NULL;
    size_t val_len = 0;
    char buf[16];

    uint32_t i = 0;

    if (!node)
        return PLIST_ERR_INVALID_ARG;

    node_data = plist_get_data(node);

    switch (node_data->type)
    {
    case PLIST_BOOLEAN:
    {
        if (node_data->boolval) {
            str_buf_append(*outbuf, "true", 4);
        } else {
            str_buf_append(*outbuf, "false", 5);
        }
    }
    break;

    case PLIST_NULL:
        str_buf_append(*outbuf, "null", 4);
	break;

    case PLIST_INT:
        val = (char*)malloc(64);
        if (node_data->length == 16) {
            val_len = snprintf(val, 64, "%" PRIu64, node_data->intval);
        } else {
            val_len = snprintf(val, 64, "%" PRIi64, node_data->intval);
        }
        str_buf_append(*outbuf, val, val_len);
        free(val);
        break;

    case PLIST_REAL:
        val = (char*)malloc(64);
        val_len = dtostr(val, 64, node_data->realval);
        str_buf_append(*outbuf, val, val_len);
        free(val);
        break;

    case PLIST_STRING:
    case PLIST_KEY: {
        const char *charmap[32] = {
            "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007",
            "\\b",     "\\t",     "\\n",     "\\u000b", "\\f",     "\\r",     "\\u000e", "\\u000f",
            "\\u0010", "\\u0011", "\\u0012", "\\u0013", "\\u0014", "\\u0015", "\\u0016", "\\u0017",
            "\\u0018", "\\u0019", "\\u001a", "\\u001b", "\\u001c", "\\u001d", "\\u001e", "\\u001f",
        };
        size_t j = 0;
        size_t len = 0;
        off_t start = 0;
        off_t cur = 0;

        len = node_data->length;
        for (j = 0; j < len; j++) {
            unsigned char ch = (unsigned char)node_data->strval[j];
            if (ch < 0x20) {
                str_buf_append(*outbuf, node_data->strval + start, cur - start);
                str_buf_append(*outbuf, charmap[ch], (charmap[ch][1] == 'u') ? 6 : 2);
                start = cur+1;
            }
            cur++;
        }
        str_buf_append(*outbuf, node_data->strval + start, cur - start);
        } break;

    case PLIST_ARRAY: {
        node_t ch;
        uint32_t cnt = 0;
        for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) {
            if (cnt > 0 || (cnt == 0 && node->parent != NULL)) {
                str_buf_append(*outbuf, "\n", 1);
                for (i = 0; i < depth+indent; i++) {
                    str_buf_append(*outbuf, " ", 1);
                }
            }
            size_t sl = sprintf(buf, "%u: ", cnt);
            str_buf_append(*outbuf, buf, sl);
            plist_err_t res = node_to_string(ch, outbuf, depth+1, indent);
            if (res < 0) {
                return res;
            }
            cnt++;
        }
        } break;
    case PLIST_DICT: {
        node_t ch;
        uint32_t cnt = 0;
        for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) {
            if (cnt > 0 && cnt % 2 == 0) {
                str_buf_append(*outbuf, "\n", 1);
                for (i = 0; i < depth+indent; i++) {
                    str_buf_append(*outbuf, " ", 1);
                }
            }
            plist_err_t res = node_to_string(ch, outbuf, depth+1, indent);
            if (res < 0) {
                return res;
            }
            if (cnt % 2 == 0) {
                plist_t valnode = (plist_t)node_next_sibling(ch);
                if (PLIST_IS_ARRAY(valnode)) {
                    size_t sl = sprintf(buf, "[%u]:", plist_array_get_size(valnode));
                    str_buf_append(*outbuf, buf, sl);
                } else {
                    str_buf_append(*outbuf, ": ", 2);
                }
            }
            cnt++;
        }
        } break;
    case PLIST_DATA:
        {
            val = (char*)malloc(4096);
            size_t done = 0;
            while (done < node_data->length) {
                size_t amount = node_data->length - done;
                if (amount > 3072) {
                    amount = 3072;
                }
                size_t bsize = base64encode(val, node_data->buff + done, amount);
                str_buf_append(*outbuf, val, bsize);
                done += amount;
            }
        }
        break;
    case PLIST_DATE:
        {
            Time64_T timev = (Time64_T)node_data->realval + MAC_EPOCH;
            struct TM _btime;
            struct TM *btime = gmtime64_r(&timev, &_btime);
            if (btime) {
                val = (char*)calloc(1, 24);
                struct tm _tmcopy;
                copy_TM64_to_tm(btime, &_tmcopy);
                val_len = strftime(val, 24, "%Y-%m-%dT%H:%M:%SZ", &_tmcopy);
                if (val_len > 0) {
                    str_buf_append(*outbuf, val, val_len);
                }
                free(val);
                val = NULL;
            }
        }
        break;
    case PLIST_UID:
        {
            str_buf_append(*outbuf, "CF$UID:", 7);
            val = (char*)malloc(64);
            if (node_data->length == 16) {
                val_len = snprintf(val, 64, "%" PRIu64, node_data->intval);
            } else {
                val_len = snprintf(val, 64, "%" PRIi64, node_data->intval);
            }
            str_buf_append(*outbuf, val, val_len);
            free(val);
        }
        break;
    default:
        return PLIST_ERR_UNKNOWN;
    }

    return PLIST_ERR_SUCCESS;
}

#define PO10i_LIMIT (INT64_MAX/10)

/* based on https://stackoverflow.com/a/4143288 */
static int num_digits_i(int64_t i)
{
    int n;
    int64_t po10;
    n=1;
    if (i < 0) {
        i = (i == INT64_MIN) ? INT64_MAX : -i;
        n++;
    }
    po10=10;
    while (i>=po10) {
        n++;
        if (po10 > PO10i_LIMIT) break;
        po10*=10;
    }
    return n;
}

#define PO10u_LIMIT (UINT64_MAX/10)

/* based on https://stackoverflow.com/a/4143288 */
static int num_digits_u(uint64_t i)
{
    int n;
    uint64_t po10;
    n=1;
    po10=10;
    while (i>=po10) {
        n++;
        if (po10 > PO10u_LIMIT) break;
        po10*=10;
    }
    return n;
}

static plist_err_t node_estimate_size(node_t node, uint64_t *size, uint32_t depth, uint32_t indent)
{
    plist_data_t data;
    if (!node) {
        return PLIST_ERR_INVALID_ARG;
    }
    data = plist_get_data(node);
    if (node->children) {
        node_t ch;
        unsigned int n_children = node_n_children(node);
        for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) {
            plist_err_t res = node_estimate_size(ch, size, depth + 1, indent);
            if (res < 0) {
                return res;
            }
        }
        switch (data->type) {
        case PLIST_DICT:
            *size += n_children-1; // number of ':' and ' '
            *size += n_children; // number of '\n' and extra space
            *size += (uint64_t)n_children * (depth+indent+1); // indent for every 2nd child
            *size += indent+1; // additional '\n'
            break;
        case PLIST_ARRAY:
            *size += n_children-1; // number of ','
            *size += n_children; // number of '\n'
            *size += (uint64_t)n_children * ((depth+indent+1)<<1); // indent for every child
            *size += indent+1; // additional '\n'
            break;
        default:
            break;
	}
    } else {
        switch (data->type) {
        case PLIST_STRING:
        case PLIST_KEY:
            *size += data->length;
            break;
        case PLIST_INT:
            if (data->length == 16) {
                *size += num_digits_u(data->intval);
            } else {
                *size += num_digits_i((int64_t)data->intval);
            }
            break;
        case PLIST_REAL:
            *size += dtostr(NULL, 0, data->realval);
            break;
        case PLIST_BOOLEAN:
            *size += ((data->boolval) ? 4 : 5);
            break;
        case PLIST_NULL:
            *size += 4;
            break;
        case PLIST_DICT:
        case PLIST_ARRAY:
            *size += 3;
            break;
        case PLIST_DATA:
            *size += (data->length / 3) * 4 + 4;
            break;
        case PLIST_DATE:
            *size += 23;
            break;
        case PLIST_UID:
            *size += 7; // "CF$UID:"
            *size += num_digits_u(data->intval);
            break;
        default:
#ifdef DEBUG
            fprintf(stderr, "%s: invalid node type encountered\n", __func__);
#endif
            return PLIST_ERR_UNKNOWN;
        }
    }
    if (depth == 0) {
        *size += 1; // final newline
    }
    return PLIST_ERR_SUCCESS;
}

static plist_err_t _plist_write_to_strbuf(plist_t plist, strbuf_t *outbuf, plist_write_options_t options)
{
    uint8_t indent = 0;
    if (options & PLIST_OPT_INDENT) {
        indent = (options >> 24) & 0xFF;
    }
    uint8_t i;
    for (i = 0; i < indent; i++) {
        str_buf_append(outbuf, " ", 1);
    }
    plist_err_t res = node_to_string((node_t)plist, &outbuf, 0, indent);
    if (res < 0) {
        return res;
    }
    if (!(options & PLIST_OPT_NO_NEWLINE)) {
        str_buf_append(outbuf, "\n", 1);
    }
    return res;
}

plist_err_t plist_write_to_string_limd(plist_t plist, char **output, uint32_t* length, plist_write_options_t options)
{
    uint64_t size = 0;
    plist_err_t res;

    if (!plist || !output || !length) {
        return PLIST_ERR_INVALID_ARG;
    }

    uint8_t indent = 0;
    if (options & PLIST_OPT_INDENT) {
        indent = (options >> 24) & 0xFF;
    }

    res = node_estimate_size((node_t)plist, &size, 0, indent);
    if (res < 0) {
        return res;
    }

    strbuf_t *outbuf = str_buf_new(size);
    if (!outbuf) {
#if DEBUG
        fprintf(stderr, "%s: Could not allocate output buffer\n", __func__);
#endif
        return PLIST_ERR_NO_MEM;
    }

    res = _plist_write_to_strbuf(plist, outbuf, options);
    if (res < 0) {
        str_buf_free(outbuf);
        *output = NULL;
        *length = 0;
        return res;
    }
    str_buf_append(outbuf, "\0", 1);

    *output = (char*)outbuf->data;
    *length = outbuf->len - 1;

    outbuf->data = NULL;
    str_buf_free(outbuf);

    return PLIST_ERR_SUCCESS;
}

plist_err_t plist_write_to_stream_limd(plist_t plist, FILE *stream, plist_write_options_t options)
{
    if (!plist || !stream) {
        return PLIST_ERR_INVALID_ARG;
    }
    strbuf_t *outbuf = str_buf_new_for_stream(stream);
    if (!outbuf) {
#if DEBUG
        fprintf(stderr, "%s: Could not allocate output buffer\n", __func__);
#endif
        return PLIST_ERR_NO_MEM;
    }

    plist_err_t res = _plist_write_to_strbuf(plist, outbuf, options);
    if (res < 0) {
        str_buf_free(outbuf);
        return res;
    }

    str_buf_free(outbuf);

    return PLIST_ERR_SUCCESS;
}
