/**
 * @file out.c
 * @author Radek Krejci <rkrejci@cesnet.cz>
 * @brief libyang output functions.
 *
 * Copyright (c) 2015 - 2020 CESNET, z.s.p.o.
 *
 * This source code is licensed under BSD 3-Clause License (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://opensource.org/licenses/BSD-3-Clause
 */

#define _GNU_SOURCE /* asprintf, strdup */

#include "out.h"
#include "out_internal.h"

#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "common.h"
#include "compat.h"
#include "log.h"
#include "printer_data.h"
#include "tree_data.h"
#include "tree_schema.h"

/**
 * @brief Align the desired size to 1 KB.
 */
#define REALLOC_CHUNK(NEW_SIZE) \
    NEW_SIZE + (1024 - (NEW_SIZE % 1024))

LIBYANG_API_DEF ly_bool
lyd_node_should_print(const struct lyd_node *node, uint32_t options)
{
    const struct lyd_node *elem;

    if (options & LYD_PRINT_WD_TRIM) {
        /* do not print default nodes */
        if (node->flags & LYD_DEFAULT) {
            /* implicit default node/NP container with only default nodes */
            return 0;
        } else if (node->schema && (node->schema->nodetype & LYD_NODE_TERM)) {
            if (lyd_is_default(node)) {
                /* explicit default node */
                return 0;
            }
        }
    } else if ((node->flags & LYD_DEFAULT) && (node->schema->nodetype == LYS_CONTAINER)) {
        if (options & LYD_PRINT_KEEPEMPTYCONT) {
            /* explicit request to print */
            return 1;
        }

        /* avoid empty default containers */
        LYD_TREE_DFS_BEGIN(node, elem) {
            if ((elem != node) && lyd_node_should_print(elem, options)) {
                return 1;
            }
            assert(elem->flags & LYD_DEFAULT);
            LYD_TREE_DFS_END(node, elem)
        }
        return 0;
    } else if ((node->flags & LYD_DEFAULT) && !(options & LYD_PRINT_WD_MASK) && !(node->schema->flags & LYS_CONFIG_R)) {
        /* LYD_PRINT_WD_EXPLICIT, find out if this is some input/output */
        if (!(node->schema->flags & (LYS_IS_INPUT | LYS_IS_OUTPUT | LYS_IS_NOTIF)) && (node->schema->flags & LYS_CONFIG_W)) {
            /* print only if it contains status data in its subtree */
            LYD_TREE_DFS_BEGIN(node, elem) {
                if ((elem->schema->nodetype != LYS_CONTAINER) || (elem->schema->flags & LYS_PRESENCE)) {
                    if (elem->schema->flags & LYS_CONFIG_R) {
                        return 1;
                    }
                }
                LYD_TREE_DFS_END(node, elem)
            }
        }
        return 0;
    }

    return 1;
}

LIBYANG_API_DEF LY_OUT_TYPE
ly_out_type(const struct ly_out *out)
{
    LY_CHECK_ARG_RET(NULL, out, LY_OUT_ERROR);
    return out->type;
}

LIBYANG_API_DEF LY_ERR
ly_out_new_clb(ly_write_clb writeclb, void *user_data, struct ly_out **out)
{
    LY_CHECK_ARG_RET(NULL, out, writeclb, LY_EINVAL);

    *out = calloc(1, sizeof **out);
    LY_CHECK_ERR_RET(!*out, LOGMEM(NULL), LY_EMEM);

    (*out)->type = LY_OUT_CALLBACK;
    (*out)->method.clb.func = writeclb;
    (*out)->method.clb.arg = user_data;

    return LY_SUCCESS;
}

LIBYANG_API_DEF ly_write_clb
ly_out_clb(struct ly_out *out, ly_write_clb writeclb)
{
    ly_write_clb prev_clb;

    LY_CHECK_ARG_RET(NULL, out, out->type == LY_OUT_CALLBACK, NULL);

    prev_clb = out->method.clb.func;

    if (writeclb) {
        out->method.clb.func = writeclb;
    }

    return prev_clb;
}

LIBYANG_API_DEF void *
ly_out_clb_arg(struct ly_out *out, void *arg)
{
    void *prev_arg;

    LY_CHECK_ARG_RET(NULL, out, out->type == LY_OUT_CALLBACK, NULL);

    prev_arg = out->method.clb.arg;

    if (arg) {
        out->method.clb.arg = arg;
    }

    return prev_arg;
}

LIBYANG_API_DEF LY_ERR
ly_out_new_fd(int fd, struct ly_out **out)
{
    LY_CHECK_ARG_RET(NULL, out, fd != -1, LY_EINVAL);

    *out = calloc(1, sizeof **out);
    LY_CHECK_ERR_RET(!*out, LOGMEM(NULL), LY_EMEM);
    (*out)->type = LY_OUT_FD;
    (*out)->method.fd = fd;

    return LY_SUCCESS;
}

LIBYANG_API_DEF int
ly_out_fd(struct ly_out *out, int fd)
{
    int prev_fd;

    LY_CHECK_ARG_RET(NULL, out, out->type <= LY_OUT_FDSTREAM, -1);

    if (out->type == LY_OUT_FDSTREAM) {
        prev_fd = out->method.fdstream.fd;
    } else { /* LY_OUT_FD */
        prev_fd = out->method.fd;
    }

    if (fd != -1) {
        /* replace output stream */
        if (out->type == LY_OUT_FDSTREAM) {
            int streamfd;
            FILE *stream;

            streamfd = dup(fd);
            if (streamfd < 0) {
                LOGERR(NULL, LY_ESYS, "Unable to duplicate provided file descriptor (%d) for printing the output (%s).", fd, strerror(errno));
                return -1;
            }
            stream = fdopen(streamfd, "a");
            if (!stream) {
                LOGERR(NULL, LY_ESYS, "Unable to open provided file descriptor (%d) for printing the output (%s).", fd, strerror(errno));
                close(streamfd);
                return -1;
            }
            /* close only the internally created stream, file descriptor is returned and supposed to be closed by the caller */
            fclose(out->method.fdstream.f);
            out->method.fdstream.f = stream;
            out->method.fdstream.fd = streamfd;
        } else { /* LY_OUT_FD */
            out->method.fd = fd;
        }
    }

    return prev_fd;
}

LIBYANG_API_DEF LY_ERR
ly_out_new_file(FILE *f, struct ly_out **out)
{
    LY_CHECK_ARG_RET(NULL, out, f, LY_EINVAL);

    *out = calloc(1, sizeof **out);
    LY_CHECK_ERR_RET(!*out, LOGMEM(NULL), LY_EMEM);

    (*out)->type = LY_OUT_FILE;
    (*out)->method.f = f;

    return LY_SUCCESS;
}

LIBYANG_API_DEF FILE *
ly_out_file(struct ly_out *out, FILE *f)
{
    FILE *prev_f;

    LY_CHECK_ARG_RET(NULL, out, out->type == LY_OUT_FILE, NULL);

    prev_f = out->method.f;

    if (f) {
        out->method.f = f;
    }

    return prev_f;
}

LIBYANG_API_DEF LY_ERR
ly_out_new_memory(char **strp, size_t size, struct ly_out **out)
{
    LY_CHECK_ARG_RET(NULL, out, strp, LY_EINVAL);

    *out = calloc(1, sizeof **out);
    LY_CHECK_ERR_RET(!*out, LOGMEM(NULL), LY_EMEM);

    (*out)->type = LY_OUT_MEMORY;
    (*out)->method.mem.buf = strp;
    if (!size) {
        /* buffer is supposed to be allocated */
        *strp = NULL;
    } else if (*strp) {
        /* there is already buffer to use */
        (*out)->method.mem.size = size;
    }

    return LY_SUCCESS;
}

char *
ly_out_memory(struct ly_out *out, char **strp, size_t size)
{
    char *data;

    LY_CHECK_ARG_RET(NULL, out, out->type == LY_OUT_MEMORY, NULL);

    data = *out->method.mem.buf;

    if (strp) {
        out->method.mem.buf = strp;
        out->method.mem.len = out->method.mem.size = 0;
        out->printed = 0;
        if (!size) {
            /* buffer is supposed to be allocated */
            *strp = NULL;
        } else if (*strp) {
            /* there is already buffer to use */
            out->method.mem.size = size;
        }
    }

    return data;
}

LIBYANG_API_DEF LY_ERR
ly_out_reset(struct ly_out *out)
{
    LY_CHECK_ARG_RET(NULL, out, LY_EINVAL);

    switch (out->type) {
    case LY_OUT_ERROR:
        LOGINT(NULL);
        return LY_EINT;
    case LY_OUT_FD:
        if ((lseek(out->method.fd, 0, SEEK_SET) == -1) && (errno != ESPIPE)) {
            LOGERR(NULL, LY_ESYS, "Seeking output file descriptor failed (%s).", strerror(errno));
            return LY_ESYS;
        }
        if ((errno != ESPIPE) && (ftruncate(out->method.fd, 0) == -1)) {
            LOGERR(NULL, LY_ESYS, "Truncating output file failed (%s).", strerror(errno));
            return LY_ESYS;
        }
        break;
    case LY_OUT_FDSTREAM:
    case LY_OUT_FILE:
    case LY_OUT_FILEPATH:
        if ((fseek(out->method.f, 0, SEEK_SET) == -1) && (errno != ESPIPE)) {
            LOGERR(NULL, LY_ESYS, "Seeking output file stream failed (%s).", strerror(errno));
            return LY_ESYS;
        }
        if ((errno != ESPIPE) && (ftruncate(fileno(out->method.f), 0) == -1)) {
            LOGERR(NULL, LY_ESYS, "Truncating output file failed (%s).", strerror(errno));
            return LY_ESYS;
        }
        break;
    case LY_OUT_MEMORY:
        if (out->method.mem.buf && *out->method.mem.buf) {
            memset(*out->method.mem.buf, 0, out->method.mem.len);
        }
        out->printed = 0;
        out->method.mem.len = 0;
        break;
    case LY_OUT_CALLBACK:
        /* nothing to do (not seekable) */
        break;
    }

    return LY_SUCCESS;
}

LIBYANG_API_DEF LY_ERR
ly_out_new_filepath(const char *filepath, struct ly_out **out)
{
    LY_CHECK_ARG_RET(NULL, out, filepath, LY_EINVAL);

    *out = calloc(1, sizeof **out);
    LY_CHECK_ERR_RET(!*out, LOGMEM(NULL), LY_EMEM);

    (*out)->type = LY_OUT_FILEPATH;
    (*out)->method.fpath.f = fopen(filepath, "wb");
    if (!(*out)->method.fpath.f) {
        LOGERR(NULL, LY_ESYS, "Failed to open file \"%s\" (%s).", filepath, strerror(errno));
        free(*out);
        *out = NULL;
        return LY_ESYS;
    }
    (*out)->method.fpath.filepath = strdup(filepath);
    return LY_SUCCESS;
}

LIBYANG_API_DEF const char *
ly_out_filepath(struct ly_out *out, const char *filepath)
{
    FILE *f;

    LY_CHECK_ARG_RET(NULL, out, out->type == LY_OUT_FILEPATH, filepath ? NULL : ((void *)-1));

    if (!filepath) {
        return out->method.fpath.filepath;
    }

    /* replace filepath */
    f = out->method.fpath.f;
    out->method.fpath.f = fopen(filepath, "wb");
    if (!out->method.fpath.f) {
        LOGERR(NULL, LY_ESYS, "Failed to open file \"%s\" (%s).", filepath, strerror(errno));
        out->method.fpath.f = f;
        return (void *)-1;
    }
    fclose(f);
    free(out->method.fpath.filepath);
    out->method.fpath.filepath = strdup(filepath);

    return NULL;
}

LIBYANG_API_DEF void
ly_out_free(struct ly_out *out, void (*clb_arg_destructor)(void *arg), ly_bool destroy)
{
    if (!out) {
        return;
    }

    switch (out->type) {
    case LY_OUT_CALLBACK:
        if (clb_arg_destructor) {
            clb_arg_destructor(out->method.clb.arg);
        }
        break;
    case LY_OUT_FDSTREAM:
        fclose(out->method.fdstream.f);
        if (destroy) {
            close(out->method.fdstream.fd);
        }
        break;
    case LY_OUT_FD:
        if (destroy) {
            close(out->method.fd);
        }
        break;
    case LY_OUT_FILE:
        if (destroy) {
            fclose(out->method.f);
        }
        break;
    case LY_OUT_MEMORY:
        if (destroy) {
            free(*out->method.mem.buf);
        }
        break;
    case LY_OUT_FILEPATH:
        free(out->method.fpath.filepath);
        fclose(out->method.fpath.f);
        break;
    case LY_OUT_ERROR:
        LOGINT(NULL);
    }

    free(out->buffered);
    free(out);
}

static LY_ERR
ly_vprint_(struct ly_out *out, const char *format, va_list ap)
{
    LY_ERR ret;
    int written = 0;
    char *msg = NULL;

    switch (out->type) {
    case LY_OUT_FD:
        written = vdprintf(out->method.fd, format, ap);
        break;
    case LY_OUT_FDSTREAM:
    case LY_OUT_FILEPATH:
    case LY_OUT_FILE:
        written = vfprintf(out->method.f, format, ap);
        break;
    case LY_OUT_MEMORY:
        if ((written = vasprintf(&msg, format, ap)) < 0) {
            break;
        }
        if (out->method.mem.len + written + 1 > out->method.mem.size) {
            *out->method.mem.buf = ly_realloc(*out->method.mem.buf, out->method.mem.len + written + 1);
            if (!*out->method.mem.buf) {
                out->method.mem.len = 0;
                out->method.mem.size = 0;
                LOGMEM(NULL);
                return LY_EMEM;
            }
            out->method.mem.size = out->method.mem.len + written + 1;
        }
        if (written) {
            memcpy(&(*out->method.mem.buf)[out->method.mem.len], msg, written);
        }
        out->method.mem.len += written;
        (*out->method.mem.buf)[out->method.mem.len] = '\0';
        free(msg);
        break;
    case LY_OUT_CALLBACK:
        if ((written = vasprintf(&msg, format, ap)) < 0) {
            break;
        }
        written = out->method.clb.func(out->method.clb.arg, msg, written);
        free(msg);
        break;
    case LY_OUT_ERROR:
        LOGINT(NULL);
        return LY_EINT;
    }

    if (written < 0) {
        LOGERR(NULL, LY_ESYS, "%s: writing data failed (%s).", __func__, strerror(errno));
        written = 0;
        ret = LY_ESYS;
    } else {
        if (out->type == LY_OUT_FDSTREAM) {
            /* move the original file descriptor to the end of the output file */
            lseek(out->method.fdstream.fd, 0, SEEK_END);
        }
        ret = LY_SUCCESS;
    }

    out->printed += written;
    out->func_printed += written;
    return ret;
}

LY_ERR
ly_print_(struct ly_out *out, const char *format, ...)
{
    LY_ERR ret;
    va_list ap;

    va_start(ap, format);
    ret = ly_vprint_(out, format, ap);
    va_end(ap);

    return ret;
}

LIBYANG_API_DEF LY_ERR
ly_print(struct ly_out *out, const char *format, ...)
{
    LY_ERR ret;
    va_list ap;

    out->func_printed = 0;

    va_start(ap, format);
    ret = ly_vprint_(out, format, ap);
    va_end(ap);

    return ret;
}

LIBYANG_API_DEF void
ly_print_flush(struct ly_out *out)
{
    switch (out->type) {
    case LY_OUT_FDSTREAM:
        /* move the original file descriptor to the end of the output file */
        lseek(out->method.fdstream.fd, 0, SEEK_END);
        fflush(out->method.fdstream.f);
        break;
    case LY_OUT_FILEPATH:
    case LY_OUT_FILE:
        fflush(out->method.f);
        break;
    case LY_OUT_FD:
        fsync(out->method.fd);
        break;
    case LY_OUT_MEMORY:
    case LY_OUT_CALLBACK:
        /* nothing to do */
        break;
    case LY_OUT_ERROR:
        LOGINT(NULL);
    }

    free(out->buffered);
    out->buf_size = out->buf_len = 0;
}

LY_ERR
ly_write_(struct ly_out *out, const char *buf, size_t len)
{
    LY_ERR ret = LY_SUCCESS;
    size_t written = 0, new_mem_size;

    if (out->hole_count) {
        /* we are buffering data after a hole */
        if (out->buf_len + len > out->buf_size) {
            out->buffered = ly_realloc(out->buffered, out->buf_len + len);
            if (!out->buffered) {
                out->buf_len = 0;
                out->buf_size = 0;
                LOGMEM(NULL);
                return LY_EMEM;
            }
            out->buf_size = out->buf_len + len;
        }

        if (len) {
            memcpy(&out->buffered[out->buf_len], buf, len);
        }
        out->buf_len += len;

        out->printed += len;
        out->func_printed += len;
        return LY_SUCCESS;
    }

repeat:
    switch (out->type) {
    case LY_OUT_MEMORY:
        new_mem_size = out->method.mem.len + len + 1;
        if (new_mem_size > out->method.mem.size) {
            new_mem_size = REALLOC_CHUNK(new_mem_size);
            *out->method.mem.buf = ly_realloc(*out->method.mem.buf, new_mem_size);
            if (!*out->method.mem.buf) {
                out->method.mem.len = 0;
                out->method.mem.size = 0;
                LOGMEM(NULL);
                return LY_EMEM;
            }
            out->method.mem.size = new_mem_size;
        }
        if (len) {
            memcpy(&(*out->method.mem.buf)[out->method.mem.len], buf, len);
        }
        out->method.mem.len += len;
        (*out->method.mem.buf)[out->method.mem.len] = '\0';

        written = len;
        break;
    case LY_OUT_FD: {
        ssize_t r;

        r = write(out->method.fd, buf, len);
        if (r < 0) {
            ret = LY_ESYS;
        } else {
            written = (size_t)r;
        }
        break;
    }
    case LY_OUT_FDSTREAM:
    case LY_OUT_FILEPATH:
    case LY_OUT_FILE:
        written = fwrite(buf, sizeof *buf, len, out->method.f);
        if (written != len) {
            ret = LY_ESYS;
        }
        break;
    case LY_OUT_CALLBACK: {
        ssize_t r;

        r = out->method.clb.func(out->method.clb.arg, buf, len);
        if (r < 0) {
            ret = LY_ESYS;
        } else {
            written = (size_t)r;
        }
        break;
    }
    case LY_OUT_ERROR:
        LOGINT(NULL);
        return LY_EINT;
    }

    if (ret) {
        if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
            ret = LY_SUCCESS;
            goto repeat;
        }
        LOGERR(NULL, LY_ESYS, "%s: writing data failed (%s).", __func__, strerror(errno));
        written = 0;
    } else if (written != len) {
        LOGERR(NULL, LY_ESYS, "%s: writing data failed (unable to write %" PRIu32 " from %" PRIu32 " data).", __func__,
                (uint32_t)(len - written), (uint32_t)len);
        ret = LY_ESYS;
    } else {
        if (out->type == LY_OUT_FDSTREAM) {
            /* move the original file descriptor to the end of the output file */
            lseek(out->method.fdstream.fd, 0, SEEK_END);
        }
        ret = LY_SUCCESS;
    }

    out->printed += written;
    out->func_printed += written;
    return ret;
}

LIBYANG_API_DEF LY_ERR
ly_write(struct ly_out *out, const char *buf, size_t len)
{
    out->func_printed = 0;

    return ly_write_(out, buf, len);
}

LIBYANG_API_DEF size_t
ly_out_printed(const struct ly_out *out)
{
    return out->func_printed;
}

LY_ERR
ly_write_skip(struct ly_out *out, size_t count, size_t *position)
{
    switch (out->type) {
    case LY_OUT_MEMORY:
        if (out->method.mem.len + count > out->method.mem.size) {
            *out->method.mem.buf = ly_realloc(*out->method.mem.buf, out->method.mem.len + count);
            if (!(*out->method.mem.buf)) {
                out->method.mem.len = 0;
                out->method.mem.size = 0;
                LOGMEM(NULL);
                return LY_EMEM;
            }
            out->method.mem.size = out->method.mem.len + count;
        }

        /* save the current position */
        *position = out->method.mem.len;

        /* skip the memory */
        out->method.mem.len += count;
        break;
    case LY_OUT_FD:
    case LY_OUT_FDSTREAM:
    case LY_OUT_FILEPATH:
    case LY_OUT_FILE:
    case LY_OUT_CALLBACK:
        /* buffer the hole */
        if (out->buf_len + count > out->buf_size) {
            out->buffered = ly_realloc(out->buffered, out->buf_len + count);
            if (!out->buffered) {
                out->buf_len = 0;
                out->buf_size = 0;
                LOGMEM(NULL);
                return LY_EMEM;
            }
            out->buf_size = out->buf_len + count;
        }

        /* save the current position */
        *position = out->buf_len;

        /* skip the memory */
        out->buf_len += count;

        /* increase hole counter */
        ++out->hole_count;
        break;
    case LY_OUT_ERROR:
        LOGINT(NULL);
        return LY_EINT;
    }

    /* update printed bytes counter despite we actually printed just a hole */
    out->printed += count;
    out->func_printed += count;
    return LY_SUCCESS;
}

LY_ERR
ly_write_skipped(struct ly_out *out, size_t position, const char *buf, size_t count)
{
    LY_ERR ret = LY_SUCCESS;

    assert(count);

    switch (out->type) {
    case LY_OUT_MEMORY:
        /* write */
        memcpy(&(*out->method.mem.buf)[position], buf, count);
        break;
    case LY_OUT_FD:
    case LY_OUT_FDSTREAM:
    case LY_OUT_FILEPATH:
    case LY_OUT_FILE:
    case LY_OUT_CALLBACK:
        if (out->buf_len < position + count) {
            LOGMEM(NULL);
            return LY_EMEM;
        }

        /* write into the hole */
        memcpy(&out->buffered[position], buf, count);

        /* decrease hole counter */
        --out->hole_count;

        if (!out->hole_count) {
            /* all holes filled, we can write the buffer,
             * printed bytes counter is updated by ly_write_() */
            ret = ly_write_(out, out->buffered, out->buf_len);
            out->buf_len = 0;
        }
        break;
    case LY_OUT_ERROR:
        LOGINT(NULL);
        return LY_EINT;
    }

    if (out->type == LY_OUT_FILEPATH) {
        /* move the original file descriptor to the end of the output file */
        lseek(out->method.fdstream.fd, 0, SEEK_END);
    }
    return ret;
}
