/*
 * Copyright 2019 OARC, Inc.
 * Copyright 2017-2018 Akamai Technologies
 * Copyright 2006-2016 Nominum, Inc.
 * All rights reserved.
 *
 * 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 <fcntl.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

#define ISC_BUFFER_USEINLINE

#include <isc/buffer.h>
#include <isc/mem.h>

#include "datafile.h"
#include "log.h"
#include "os.h"
#include "util.h"

#define BUFFER_SIZE (64 * 1024)

#ifndef ISC_TRUE
#define ISC_TRUE true
#endif
#ifndef ISC_FALSE
#define ISC_FALSE false
#endif

struct perf_datafile {
    isc_mem_t*      mctx;
    pthread_mutex_t lock;
    int             pipe_fd;
    int             fd;
    bool            is_file;
    size_t          size;
    bool            cached;
    char            databuf[BUFFER_SIZE + 1];
    isc_buffer_t    data;
    unsigned int    maxruns;
    unsigned int    nruns;
    bool            read_any;
};

static inline void
nul_terminate(perf_datafile_t* dfile)
{
    unsigned char* data;

    data  = isc_buffer_used(&dfile->data);
    *data = '\0';
}

perf_datafile_t*
perf_datafile_open(isc_mem_t* mctx, const char* filename)
{
    perf_datafile_t* dfile;
    struct stat      buf;

    dfile = isc_mem_get(mctx, sizeof(*dfile));
    if (dfile == NULL) {
        perf_log_fatal("out of memory");
        return 0; // fix clang scan-build
    }

    dfile->mctx = mctx;
    MUTEX_INIT(&dfile->lock);
    dfile->pipe_fd  = -1;
    dfile->is_file  = ISC_FALSE;
    dfile->size     = 0;
    dfile->cached   = ISC_FALSE;
    dfile->maxruns  = 1;
    dfile->nruns    = 0;
    dfile->read_any = ISC_FALSE;
    isc_buffer_init(&dfile->data, dfile->databuf, BUFFER_SIZE);
    if (filename == NULL) {
        dfile->fd = STDIN_FILENO;
    } else {
        dfile->fd = open(filename, O_RDONLY);
        if (dfile->fd < 0)
            perf_log_fatal("unable to open file: %s", filename);
        if (fstat(dfile->fd, &buf) == 0 && S_ISREG(buf.st_mode)) {
            dfile->is_file = ISC_TRUE;
            dfile->size    = buf.st_size;
        }
    }
    nul_terminate(dfile);

    return dfile;
}

void perf_datafile_close(perf_datafile_t** dfilep)
{
    perf_datafile_t* dfile;

    ISC_INSIST(dfilep != NULL && *dfilep != NULL);

    dfile   = *dfilep;
    *dfilep = NULL;

    if (dfile->fd >= 0 && dfile->fd != STDIN_FILENO)
        close(dfile->fd);
    MUTEX_DESTROY(&dfile->lock);
    isc_mem_put(dfile->mctx, dfile, sizeof(*dfile));
}

void perf_datafile_setpipefd(perf_datafile_t* dfile, int pipe_fd)
{
    dfile->pipe_fd = pipe_fd;
}

void perf_datafile_setmaxruns(perf_datafile_t* dfile, unsigned int maxruns)
{
    dfile->maxruns = maxruns;
}

static void
reopen_file(perf_datafile_t* dfile)
{
    if (dfile->cached) {
        isc_buffer_first(&dfile->data);
    } else {
        if (lseek(dfile->fd, 0L, SEEK_SET) < 0)
            perf_log_fatal("cannot reread input");
        isc_buffer_clear(&dfile->data);
        nul_terminate(dfile);
    }
}

static isc_result_t
read_more(perf_datafile_t* dfile)
{
    unsigned char*         data;
    size_t                 size;
    ssize_t                n;
    isc_result_t           result;
    struct perf_net_socket sock = {.mode = sock_file, .fd = dfile->fd };

    if (!dfile->is_file && dfile->pipe_fd >= 0) {
        result = perf_os_waituntilreadable(&sock, dfile->pipe_fd, -1);
        if (result != ISC_R_SUCCESS)
            return (result);
    }

    isc_buffer_compact(&dfile->data);
    data = isc_buffer_used(&dfile->data);
    size = isc_buffer_availablelength(&dfile->data);

    n = read(dfile->fd, data, size);
    if (n < 0)
        return (ISC_R_FAILURE);

    isc_buffer_add(&dfile->data, n);
    nul_terminate(dfile);

    if (dfile->is_file && isc_buffer_usedlength(&dfile->data) == dfile->size)
        dfile->cached = ISC_TRUE;

    return (ISC_R_SUCCESS);
}

static isc_result_t
read_one_line(perf_datafile_t* dfile, isc_buffer_t* lines)
{
    const char*  cur;
    unsigned int length, curlen, nrem;
    isc_result_t result;

    while (ISC_TRUE) {
        /* Get the current line */
        cur    = isc_buffer_current(&dfile->data);
        curlen = strcspn(cur, "\n");

        /*
         * If the current line contains the rest of the buffer,
         * we need to read more (unless the full file is cached).
         */
        nrem = isc_buffer_remaininglength(&dfile->data);
        if (curlen == nrem) {
            if (!dfile->cached) {
                result = read_more(dfile);
                if (result != ISC_R_SUCCESS)
                    return (result);
            }
            if (isc_buffer_remaininglength(&dfile->data) == 0) {
                dfile->nruns++;
                return (ISC_R_EOF);
            }
            if (isc_buffer_remaininglength(&dfile->data) > nrem)
                continue;
        }

        /* We now have a line.  Advance the buffer past it. */
        isc_buffer_forward(&dfile->data, curlen);
        if (isc_buffer_remaininglength(&dfile->data) > 0)
            isc_buffer_forward(&dfile->data, 1);

        /* If the line is empty or a comment, we need to try again. */
        if (curlen > 0 && cur[0] != ';')
            break;
    }

    length = isc_buffer_availablelength(lines);
    if (curlen > length - 1)
        curlen = length - 1;
    isc_buffer_putmem(lines, cur, curlen);
    isc_buffer_putuint8(lines, 0);

    return (ISC_R_SUCCESS);
}

isc_result_t
perf_datafile_next(perf_datafile_t* dfile, isc_buffer_t* lines,
    bool is_update)
{
    const char*  current;
    isc_result_t result;

    LOCK(&dfile->lock);

    if (dfile->maxruns > 0 && dfile->maxruns == dfile->nruns) {
        result = ISC_R_EOF;
        goto done;
    }

    result = read_one_line(dfile, lines);
    if (result == ISC_R_EOF) {
        if (!dfile->read_any) {
            result = ISC_R_INVALIDFILE;
            goto done;
        }
        if (dfile->maxruns != dfile->nruns) {
            reopen_file(dfile);
            result = read_one_line(dfile, lines);
        }
    }
    if (result != ISC_R_SUCCESS) {
        goto done;
    }
    dfile->read_any = ISC_TRUE;

    if (is_update) {
        while (ISC_TRUE) {
            current = isc_buffer_used(lines);
            result  = read_one_line(dfile, lines);
            if (result == ISC_R_EOF && dfile->maxruns != dfile->nruns) {
                reopen_file(dfile);
            }
            if (result != ISC_R_SUCCESS || strcasecmp(current, "send") == 0)
                break;
        };
    }

    result = ISC_R_SUCCESS;
done:
    UNLOCK(&dfile->lock);
    return (result);
}

unsigned int
perf_datafile_nruns(const perf_datafile_t* dfile)
{
    return dfile->nruns;
}
