
/*
 * Copyright (C) Yichun Zhang (agentzh)
 */


#ifndef DDEBUG
#define DDEBUG 0
#endif
#include "ddebug.h"


#include "ngx_http_rds_csv_filter_module.h"
#include "ngx_http_rds_csv_util.h"
#include "ngx_http_rds_csv_processor.h"
#include "ngx_http_rds_csv_output.h"

#include <ngx_config.h>


#define ngx_http_rds_csv_content_type  "text/csv"
#define ngx_http_rds_csv_row_term  "\r\n"


static volatile ngx_cycle_t  *ngx_http_rds_csv_prev_cycle = NULL;


ngx_http_output_header_filter_pt  ngx_http_rds_csv_next_header_filter;
ngx_http_output_body_filter_pt    ngx_http_rds_csv_next_body_filter;


static void *ngx_http_rds_csv_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_rds_csv_merge_loc_conf(ngx_conf_t *cf, void *parent,
    void *child);
static ngx_int_t ngx_http_rds_csv_filter_init(ngx_conf_t *cf);
static char *ngx_http_rds_csv_row_terminator(ngx_conf_t *cf,
    ngx_command_t *cmd, void *conf);
static char *ngx_http_rds_csv_field_separator(ngx_conf_t *cf,
    ngx_command_t *cmd, void *conf);
static char *ngx_http_rds_csv(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static void *ngx_http_rds_csv_create_main_conf(ngx_conf_t *cf);


static ngx_command_t  ngx_http_rds_csv_commands[] = {

    { ngx_string("rds_csv"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF
          |NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
          |NGX_CONF_FLAG,
      ngx_http_rds_csv,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_rds_csv_loc_conf_t, enabled),
      NULL },

    { ngx_string("rds_csv_row_terminator"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF
          |NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
          |NGX_CONF_TAKE1,
      ngx_http_rds_csv_row_terminator,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_rds_csv_loc_conf_t, row_term),
      NULL },

    { ngx_string("rds_csv_field_separator"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF
          |NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
          |NGX_CONF_TAKE1,
      ngx_http_rds_csv_field_separator,
      NGX_HTTP_LOC_CONF_OFFSET,
      0,
      NULL },

    { ngx_string("rds_csv_field_name_header"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF
          |NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
          |NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_rds_csv_loc_conf_t, field_name_header),
      NULL },

    { ngx_string("rds_csv_content_type"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF
          |NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
          |NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_rds_csv_loc_conf_t, content_type),
      NULL },

    { ngx_string("rds_csv_buffer_size"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
          |NGX_CONF_TAKE1,
      ngx_conf_set_size_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_rds_csv_loc_conf_t, buf_size),
      NULL },

      ngx_null_command
};


static ngx_http_module_t  ngx_http_rds_csv_filter_module_ctx = {
    NULL,                                 /* preconfiguration */
    ngx_http_rds_csv_filter_init,         /* postconfiguration */

    ngx_http_rds_csv_create_main_conf,    /* create main configuration */
    NULL,                                 /* init main configuration */

    NULL,                                 /* create server configuration */
    NULL,                                 /* merge server configuration */

    ngx_http_rds_csv_create_loc_conf,     /* create location configuration */
    ngx_http_rds_csv_merge_loc_conf       /* merge location configuration */
};


ngx_module_t  ngx_http_rds_csv_filter_module = {
    NGX_MODULE_V1,
    &ngx_http_rds_csv_filter_module_ctx,  /* module context */
    ngx_http_rds_csv_commands,            /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};


static ngx_int_t
ngx_http_rds_csv_header_filter(ngx_http_request_t *r)
{
    ngx_http_rds_csv_ctx_t          *ctx;
    ngx_http_rds_csv_loc_conf_t     *conf;
    size_t                           len;
    u_char                          *p;

    /* XXX maybe we can generate stub JSON strings like
     * {"errcode":403,"error":"Permission denied"}
     * for HTTP error pages? */
    if ((r->headers_out.status < NGX_HTTP_OK)
        || (r->headers_out.status >= NGX_HTTP_SPECIAL_RESPONSE)
        || (r->headers_out.status == NGX_HTTP_NO_CONTENT)
        || (r->headers_out.status == NGX_HTTP_RESET_CONTENT))
    {
        ngx_http_set_ctx(r, NULL, ngx_http_rds_csv_filter_module);

        dd("status is not OK: %d, skipping", (int) r->headers_out.status);

        return ngx_http_rds_csv_next_header_filter(r);
    }

    /* r->headers_out.status = 0; */

    conf = ngx_http_get_module_loc_conf(r, ngx_http_rds_csv_filter_module);

    if (!conf->enabled) {
        return ngx_http_rds_csv_next_header_filter(r);
    }

    if (ngx_http_rds_csv_test_content_type(r) != NGX_OK) {
        return ngx_http_rds_csv_next_header_filter(r);
    }

    if (conf->content_type.len == sizeof(ngx_http_rds_csv_content_type) - 1
        && ngx_strncmp(conf->content_type.data, ngx_http_rds_csv_content_type,
                       sizeof(ngx_http_rds_csv_content_type) - 1) == 0)
    {
        /* MIME type is text/csv, we process Content-Type
         * according to RFC 4180 */

        len = sizeof(ngx_http_rds_csv_content_type) - 1
              + sizeof("; header=") - 1;

        if (conf->field_name_header) {
            len += sizeof("presence") - 1;

        } else {
            len += sizeof("absence") - 1;
        }

        p = ngx_palloc(r->pool, len);
        if (p == NULL) {
            return NGX_ERROR;
        }

        r->headers_out.content_type.len = len;
        r->headers_out.content_type_len = len;

        r->headers_out.content_type.data = p;

        p = ngx_copy(p, conf->content_type.data, conf->content_type.len);

        if (conf->field_name_header) {
            p = ngx_copy_literal(p, "; header=presence");

        } else {
            p = ngx_copy_literal(p, "; header=absence");
        }

        if (p - r->headers_out.content_type.data != (ssize_t) len) {
            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                          "rds_csv: content type buffer error: %uz != %uz",
                          (size_t) (p - r->headers_out.content_type.data),
                          len);

            return NGX_ERROR;
        }

    } else {
        /* custom MIME-type, we just pass it through */

        r->headers_out.content_type = conf->content_type;
        r->headers_out.content_type_len = conf->content_type.len;
    }

    ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_rds_csv_ctx_t));

    if (ctx == NULL) {
        return NGX_ERROR;
    }

    ctx->tag = (ngx_buf_tag_t) &ngx_http_rds_csv_filter_module;

    ctx->state = state_expect_header;

    ctx->header_sent = 0;

    ctx->last_out = &ctx->out;

    /* set by ngx_pcalloc
     *      ctx->out       = NULL
     *      ctx->busy_bufs = NULL
     *      ctx->free_bufs = NULL
     *      ctx->cached = (ngx_buf_t) 0
     *      ctx->postponed = (ngx_buf_t) 0
     *      ctx->avail_out = 0
     *      ctx->col_names = NULL
     *      ctx->col_count = 0
     *      ctx->cur_col = 0
     *      ctx->field_offset = 0
     *      ctx->field_total = 0
     *      ctx->field_data_rest = 0
     */

    ngx_http_set_ctx(r, ctx, ngx_http_rds_csv_filter_module);

    ngx_http_clear_content_length(r);

    r->filter_need_in_memory = 1;

    /* we do postpone the header sending to the body filter */
    return NGX_OK;
}


static ngx_int_t
ngx_http_rds_csv_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    ngx_http_rds_csv_ctx_t    *ctx;
    ngx_int_t                  rc;

    if (in == NULL || r->header_only) {
        return ngx_http_rds_csv_next_body_filter(r, in);
    }

    ctx = ngx_http_get_module_ctx(r, ngx_http_rds_csv_filter_module);

    if (ctx == NULL) {
        return ngx_http_rds_csv_next_body_filter(r, in);
    }

    switch (ctx->state) {

    case state_expect_header:
        rc = ngx_http_rds_csv_process_header(r, in, ctx);
        break;

    case state_expect_col:
        rc = ngx_http_rds_csv_process_col(r, in, ctx);
        break;

    case state_expect_row:
        rc = ngx_http_rds_csv_process_row(r, in, ctx);
        break;

    case state_expect_field:
        rc = ngx_http_rds_csv_process_field(r, in, ctx);
        break;

    case state_expect_more_field_data:
        rc = ngx_http_rds_csv_process_more_field_data(r, in, ctx);
        break;

    case state_done:

        /* mark the remaining bufs as consumed */

        dd("discarding bufs");

        ngx_http_rds_csv_discard_bufs(r->pool, in);

        return NGX_OK;
        break;
    default:
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "rds_csv: invalid internal state: %d",
                      ctx->state);

        rc = NGX_ERROR;

        break;
    }

    dd("body filter rc: %d", (int) rc);

    if (rc == NGX_ERROR || rc >= NGX_HTTP_SPECIAL_RESPONSE) {
        ctx->state = state_done;

        if (!ctx->header_sent) {
            ctx->header_sent = 1;

            if (rc == NGX_ERROR) {
                rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
            }

            r->headers_out.status = rc;

            dd("sending ERROR headers");

            ngx_http_rds_csv_next_header_filter(r);
            ngx_http_send_special(r, NGX_HTTP_LAST);

            return NGX_ERROR;
        }

        return NGX_ERROR;
    }

    dd("output bufs");

    return ngx_http_rds_csv_output_bufs(r, ctx);
}


static ngx_int_t
ngx_http_rds_csv_filter_init(ngx_conf_t *cf)
{
    int                             multi_http_blocks;
    ngx_http_rds_csv_main_conf_t   *rmcf;

    rmcf = ngx_http_conf_get_module_main_conf(cf,
                                              ngx_http_rds_csv_filter_module);

    if (ngx_http_rds_csv_prev_cycle != ngx_cycle) {
        ngx_http_rds_csv_prev_cycle = ngx_cycle;
        multi_http_blocks = 0;

    } else {
        multi_http_blocks = 1;
    }

    if (multi_http_blocks || rmcf->requires_filter) {
        ngx_http_rds_csv_next_header_filter = ngx_http_top_header_filter;
        ngx_http_top_header_filter = ngx_http_rds_csv_header_filter;

        ngx_http_rds_csv_next_body_filter = ngx_http_top_body_filter;
        ngx_http_top_body_filter = ngx_http_rds_csv_body_filter;
    }

    return NGX_OK;
}


static void *
ngx_http_rds_csv_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_rds_csv_loc_conf_t  *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_rds_csv_loc_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    /*
     * set by ngx_pcalloc():
     *
     *     conf->content_type = { 0, NULL };
     *     conf->row_term = { 0, NULL };
     */

    conf->enabled = NGX_CONF_UNSET;
    conf->field_sep = NGX_CONF_UNSET_UINT;
    conf->buf_size = NGX_CONF_UNSET_SIZE;
    conf->field_name_header = NGX_CONF_UNSET;

    return conf;
}


static char *
ngx_http_rds_csv_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_rds_csv_loc_conf_t *prev = parent;
    ngx_http_rds_csv_loc_conf_t *conf = child;

    ngx_conf_merge_value(conf->enabled, prev->enabled, 0);

    ngx_conf_merge_value(conf->field_name_header, prev->field_name_header, 1);

    ngx_conf_merge_uint_value(conf->field_sep, prev->field_sep,
                              (ngx_uint_t) ',');

    ngx_conf_merge_str_value(conf->row_term, prev->row_term,
                             ngx_http_rds_csv_row_term);

    ngx_conf_merge_str_value(conf->content_type, prev->content_type,
                             ngx_http_rds_csv_content_type);

    ngx_conf_merge_size_value(conf->buf_size, prev->buf_size,
                              (size_t) ngx_pagesize);

    return NGX_CONF_OK;
}


static char *
ngx_http_rds_csv_row_terminator(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf)
{
    ngx_http_rds_csv_loc_conf_t         *rlcf = conf;
    ngx_str_t                           *value;
    ngx_str_t                           *term;

    if (rlcf->row_term.len != 0) {
        return "is duplicate";
    }

    value = cf->args->elts;

    term = &value[1];

    if (term->len == 0) {
        return "takes empty string value";
    }

    if ((term->len == 1 && term->data[0] == '\n')
        || (term->len == 2 && term->data[0] == '\r' && term->data[1] == '\n'))
    {
        return ngx_conf_set_str_slot(cf, cmd, conf);
    }

    return "takes a value other than \"\\n\" and \"\\r\\n\"";
}


static char *
ngx_http_rds_csv_field_separator(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf)
{
    ngx_http_rds_csv_loc_conf_t         *rlcf = conf;
    ngx_str_t                           *value;
    ngx_str_t                           *sep;

    if (rlcf->field_sep != NGX_CONF_UNSET_UINT) {
        return "is duplicate";
    }

    value = cf->args->elts;

    sep = &value[1];

    if (sep->len != 1) {
        return "takes a string value not of length 1";
    }

    if (sep->data[0] == ',' || sep->data[0] == ';' || sep->data[0] == '\t') {
        rlcf->field_sep = (ngx_uint_t) (sep->data[0]);
        return NGX_CONF_OK;
    }

    return "takes a value other than \",\", \";\", and \"\\t\"";
}


static char *
ngx_http_rds_csv(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_rds_csv_main_conf_t     *rmcf;

    rmcf = ngx_http_conf_get_module_main_conf(cf,
                                              ngx_http_rds_csv_filter_module);

    rmcf->requires_filter = 1;

    return ngx_conf_set_flag_slot(cf, cmd, conf);
}


static void *
ngx_http_rds_csv_create_main_conf(ngx_conf_t *cf)
{
    ngx_http_rds_csv_main_conf_t    *rmcf;

    rmcf = ngx_pcalloc(cf->pool, sizeof(ngx_http_rds_csv_main_conf_t));
    if (rmcf == NULL) {
        return NULL;
    }

    /* set by ngx_pcalloc:
     *      rmcf->requires_filter = 0;
     */

    return rmcf;
}
