/*
copyright: Boaz segev, 2016-2017
license: MIT

Feel free to copy, use and enjoy according to the license provided.
*/
#include "spnlock.inc"

#include "http.h"
#include "http1.h"
#include "http1_request.h"
#include "http1_simple_parser.h"

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>

char *HTTP1_Protocol_String = "facil_http/1.1_protocol";
/* *****************************************************************************
HTTP/1.1 data structures
*/

typedef struct http1_protocol_s {
  protocol_s protocol;
  http_settings_s *settings;
  void (*on_request)(http_request_s *request);
  struct http1_protocol_s *next;
  http1_request_s request;
  ssize_t len; /* used as a persistent socket `read` indication. */
} http1_protocol_s;

static void http1_on_data(intptr_t uuid, http1_protocol_s *protocol);

/* *****************************************************************************
HTTP/1.1 pool
*/

static struct {
  spn_lock_i lock;
  uint8_t init;
  http1_protocol_s *next;
  http1_protocol_s protocol_mem[HTTP1_POOL_SIZE];
} http1_pool = {.lock = SPN_LOCK_INIT, .init = 0};

static void http1_free(intptr_t uuid, http1_protocol_s *pr) {
  if ((uintptr_t)pr < (uintptr_t)http1_pool.protocol_mem ||
      (uintptr_t)pr >= (uintptr_t)(http1_pool.protocol_mem + HTTP1_POOL_SIZE))
    goto use_free;
  spn_lock(&http1_pool.lock);
  pr->next = http1_pool.next;
  http1_pool.next = pr;
  spn_unlock(&http1_pool.lock);
  return;
use_free:
  free(pr);
  return;
  (void)uuid;
}

static inline void http1_set_protocol_data(http1_protocol_s *pr) {
  pr->protocol = (protocol_s){
      .on_data = (void (*)(intptr_t, protocol_s *))http1_on_data,
      .on_close = (void (*)(intptr_t uuid, protocol_s *))http1_free};
  pr->request.request = (http_request_s){.fd = 0, .http_version = HTTP_V1};
  pr->request.header_pos = pr->request.buffer_pos = pr->len = 0;
}

static http1_protocol_s *http1_alloc(void) {
  http1_protocol_s *pr;
  spn_lock(&http1_pool.lock);
  if (!http1_pool.next)
    goto use_malloc;
  pr = http1_pool.next;
  http1_pool.next = pr->next;
  spn_unlock(&http1_pool.lock);
  http1_request_clear(&pr->request.request);
  pr->request.request.settings = pr->settings;
  pr->len = 0;
  return pr;
use_malloc:
  if (http1_pool.init == 0)
    goto initialize;
  spn_unlock(&http1_pool.lock);
  // fprintf(stderr, "using malloc\n");
  pr = malloc(sizeof(*pr));
  http1_set_protocol_data(pr);
  return pr;
initialize:
  http1_pool.init = 1;
  for (size_t i = 1; i < (HTTP1_POOL_SIZE - 1); i++) {
    http1_set_protocol_data(http1_pool.protocol_mem + i);
    http1_pool.protocol_mem[i].next = http1_pool.protocol_mem + (i + 1);
  }
  http1_pool.protocol_mem[HTTP1_POOL_SIZE - 1].next = NULL;
  http1_pool.next = http1_pool.protocol_mem + 1;
  spn_unlock(&http1_pool.lock);
  http1_set_protocol_data(http1_pool.protocol_mem);
  return http1_pool.protocol_mem;
}

/* *****************************************************************************
HTTP callbacks
*/
#define HTTP_BODY_CHUNK_SIZE 3072 // 4096

static void http1_on_header_found(http_request_s *request,
                                  http_header_s *header) {
  ((http1_request_s *)request)
      ->headers[((http1_request_s *)request)->header_pos] = *header;
  ((http1_request_s *)request)->header_pos += 1;
}

static void http1_on_data(intptr_t uuid, http1_protocol_s *pr);
static void http1_on_data_def(intptr_t uuid, protocol_s *pr, void *ignr) {
  sock_touch(uuid);
  http1_on_data(uuid, (http1_protocol_s *)pr);
  (void)ignr;
}
/* parse and call callback */
static void http1_on_data(intptr_t uuid, http1_protocol_s *pr) {
  ssize_t result;
  char buff[HTTP_BODY_CHUNK_SIZE];
  http1_request_s *request = &pr->request;
  char *buffer = request->buffer;
  for (;;) {
    // handle requests with no file data
    if (request->request.body_file <= 0) {
      // request headers parsing
      if (pr->len == 0) {
        buffer = request->buffer;
        // make sure headers don't overflow
        pr->len = sock_read(uuid, buffer + request->buffer_pos,
                            HTTP1_MAX_HEADER_SIZE - request->buffer_pos);
        // update buffer read position.
        request->buffer_pos += pr->len;
        // if (len > 0) {
        //   fprintf(stderr, "\n----\nRead from socket, %lu bytes, total
        //   %lu:\n",
        //           len, request->buffer_pos);
        //   for (size_t i = 0; i < request->buffer_pos; i++) {
        //     fprintf(stderr, "%c", buffer[i] ? buffer[i] : '-');
        //   }
        //   fprintf(stderr, "\n");
        // }
      }
      if (pr->len <= 0)
        goto finished_reading;

      // parse headers
      result =
          http1_parse_request_headers(buffer, request->buffer_pos,
                                      &request->request, http1_on_header_found);
      // review result
      if (result >= 0) { // headers comeplete
        // are we done?
        if (request->request.content_length == 0 || request->request.body_str) {
          goto handle_request;
        }
        if (request->request.content_length > pr->settings->max_body_size) {
          goto body_to_big;
        }
        // initialize or submit body data
        result = http1_parse_request_body(buffer + result, pr->len - result,
                                          (http_request_s *)request);
        if (result >= 0) {
          request->buffer_pos += result;
          goto handle_request;
        } else if (result == -1) // parser error
          goto parser_error;
        goto parse_body;
      } else if (result == -1) // parser error
        goto parser_error;
      // assume incomplete (result == -2), even if wrong, we're right.
      pr->len = 0;
      continue;
    }
    if (request->request.body_file > 0) {
    // fprintf(stderr, "Body File\n");
    parse_body:
      buffer = buff;
      // request body parsing
      pr->len = sock_read(uuid, buffer, HTTP_BODY_CHUNK_SIZE);
      if (pr->len <= 0)
        goto finished_reading;
      result = http1_parse_request_body(buffer, pr->len, &request->request);
      if (result >= 0) {
        goto handle_request;
      } else if (result == -1) // parser error
        goto parser_error;
      if (pr->len < HTTP_BODY_CHUNK_SIZE) // pause parser for more data
        goto finished_reading;
      goto parse_body;
    }
    continue;
  handle_request:
    // review required headers / data
    if (request->request.host == NULL)
      goto bad_request;
    http_settings_s *settings = pr->settings;
    request->request.settings = settings;
    // call request callback
    if (pr && settings &&
        (request->request.upgrade || settings->public_folder == NULL ||
         http_response_sendfile2(
             NULL, &request->request, settings->public_folder,
             settings->public_folder_length, request->request.path,
             request->request.path_len, settings->log_static))) {
      pr->on_request(&request->request);
      // fprintf(stderr, "Called on_request\n");
    }
    // rotate buffer for HTTP pipelining
    if ((ssize_t)request->buffer_pos <= result) {
      pr->len = 0;
      // fprintf(stderr, "\n----\nAll data consumed.\n");
    } else {
      memmove(request->buffer, buffer + result, request->buffer_pos - result);
      pr->len = request->buffer_pos - result;
      // fprintf(stderr, "\n----\ndata after move, %lu long:\n%.*s\n", len,
      //         (int)len, request->buffer);
    }
    // fprintf(stderr, "data in buffer, %lu long:\n%.*s\n", len, (int)len,
    //         request->buffer);
    // clear request state
    http1_request_clear(&request->request);
    request->buffer_pos = pr->len;
    // make sure to use the correct buffer.
    buffer = request->buffer;
    if (pr->len) {
      /* prevent this connection from hogging the thread by pipelining endless
       * requests.
       */
      facil_defer(.task = http1_on_data_def, .task_type = FIO_PR_LOCK_TASK,
                  .uuid = uuid);
      return;
    }
  }
  // no routes lead here.
  fprintf(stderr,
          "I am lost on a deserted island, no code can reach me here :-)\n");
  goto finished_reading; // How did we get here?
parser_error:
  if (request->request.headers_count >= HTTP1_MAX_HEADER_COUNT)
    goto too_big;
bad_request:
  /* handle generally bad requests */
  {
    http_response_s *response = http_response_create(&request->request);
    response->status = 400;
    if (!pr->settings->public_folder ||
        http_response_sendfile2(response, &request->request,
                                pr->settings->public_folder,
                                pr->settings->public_folder_length, "400.html",
                                8, pr->settings->log_static)) {
      http_response_write_body(response, "Bad Request", 11);
      http_response_finish(response);
    }
    sock_close(uuid);
    request->buffer_pos = 0;
    goto finished_reading;
  }
too_big:
  /* handle oversized headers */
  {
    http_response_s *response = http_response_create(&request->request);
    response->status = 431;
    if (!pr->settings->public_folder ||
        http_response_sendfile2(response, &request->request,
                                pr->settings->public_folder,
                                pr->settings->public_folder_length, "431.html",
                                8, pr->settings->log_static)) {
      http_response_write_body(response, "Request Header Fields Too Large", 31);
      http_response_finish(response);
    }
    sock_close(uuid);
    request->buffer_pos = 0;
    goto finished_reading;
  body_to_big:
    /* handle oversized body */
    {
      http_response_s *response = http_response_create(&request->request);
      response->status = 413;
      if (!pr->settings->public_folder ||
          http_response_sendfile2(response, &request->request,
                                  pr->settings->public_folder,
                                  pr->settings->public_folder_length,
                                  "413.html", 8, pr->settings->log_static)) {
        http_response_write_body(response, "Payload Too Large", 17);
        http_response_finish(response);
      }
      sock_close(uuid);
      request->buffer_pos = 0;
      goto finished_reading;
    }
  }
finished_reading:
  pr->len = 0;
}

/* *****************************************************************************
HTTP listening helpers
*/

/**
Allocates memory for an upgradable HTTP/1.1 protocol.

The protocol self destructs when the `on_close` callback is called.
*/
protocol_s *http1_on_open(intptr_t fd, http_settings_s *settings) {
  if (sock_uuid2fd(fd) >= (sock_max_capacity() - HTTP_BUSY_UNLESS_HAS_FDS))
    goto is_busy;
  http1_protocol_s *pr = http1_alloc();
  pr->request.request.fd = fd;
  pr->settings = settings;
  pr->on_request = settings->on_request;
  facil_set_timeout(fd, pr->settings->timeout);
  return (protocol_s *)pr;

is_busy:
  if (settings->public_folder && settings->public_folder_length) {
    size_t p_len = settings->public_folder_length;
    struct stat file_data = {.st_mode = 0};
    char fname[p_len + 8 + 1];
    memcpy(fname, settings->public_folder, p_len);
    if (settings->public_folder[p_len - 1] == '/' ||
        settings->public_folder[p_len - 1] == '\\')
      p_len--;
    memcpy(fname + p_len, "/503.html", 9);
    p_len += 9;
    if (stat(fname, &file_data))
      goto busy_no_file;
    // check that we have a file and not something else
    if (!S_ISREG(file_data.st_mode) && !S_ISLNK(file_data.st_mode))
      goto busy_no_file;
    int file = open(fname, O_RDONLY);
    if (file == -1)
      goto busy_no_file;
    sock_buffer_s *buffer;
    buffer = sock_buffer_checkout();
    memcpy(buffer->buf,
           "HTTP/1.1 503 Service Unavailable\r\n"
           "Content-Type: text/html\r\n"
           "Connection: close\r\n"
           "Content-Length: ",
           94);
    p_len = 94 + http_ul2a((char *)buffer->buf + 94, file_data.st_size);
    memcpy(buffer->buf + p_len, "\r\n\r\n", 4);
    p_len += 4;
    if ((off_t)(BUFFER_PACKET_SIZE - p_len) > file_data.st_size) {
      if (read(file, buffer->buf + p_len, file_data.st_size) < 0) {
        close(file);
        sock_buffer_free(buffer);
        goto busy_no_file;
      }
      close(file);
      buffer->len = p_len + file_data.st_size;
      sock_buffer_send(fd, buffer);
    } else {
      buffer->len = p_len;
      sock_buffer_send(fd, buffer);
      sock_sendfile(fd, file, 0, file_data.st_size);
      sock_close(fd);
    }
    return NULL;
  }

busy_no_file:
  sock_write(fd,
             "HTTP/1.1 503 Service Unavailable\r\nContent-Length: "
             "13\r\n\r\nServer Busy.",
             68);
  sock_close(fd);
  return NULL;
}
