#include <stdio.h>
#include <stdlib.h> /*for atoi*/
#include <string.h>
#include "contiki.h"

#include "http-server.h"
#include "buffer.h"
#include "rest-util.h"

#if !UIP_CONF_IPV6_RPL
#include "static-routing.h"
#endif

#define DEBUG 1
#if DEBUG
#include <stdio.h>
#define PRINTF(...) printf(__VA_ARGS__)
#define PRINT6ADDR(addr) PRINTF(" %02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x ", ((u8_t *)addr)[0], ((u8_t *)addr)[1], ((u8_t *)addr)[2], ((u8_t *)addr)[3], ((u8_t *)addr)[4], ((u8_t *)addr)[5], ((u8_t *)addr)[6], ((u8_t *)addr)[7], ((u8_t *)addr)[8], ((u8_t *)addr)[9], ((u8_t *)addr)[10], ((u8_t *)addr)[11], ((u8_t *)addr)[12], ((u8_t *)addr)[13], ((u8_t *)addr)[14], ((u8_t *)addr)[15])
#define PRINTLLADDR(lladdr) PRINTF(" %02x:%02x:%02x:%02x:%02x:%02x ",(lladdr)->addr[0], (lladdr)->addr[1], (lladdr)->addr[2], (lladdr)->addr[3],(lladdr)->addr[4], (lladdr)->addr[5])
#else
#define PRINTF(...)
#define PRINT6ADDR(addr)
#define PRINTLLADDR(addr)
#endif

static void
init_response(http_response_t* response)
{
  response->status_code = OK_200;
  response->status_string = NULL;
  response->headers = NULL;
  response->payload = NULL;
  response->payload_len = 0;
}

static void
init_request(http_request_t* request)
{
  request->request_type = 0;
  request->url = NULL;
  request->url_len = 0;
  request->query = NULL;
  request->query_len = 0;
  request->headers = NULL;
  request->payload = NULL;
  request->payload_len = 0;
}

/**
 * Initializes the connection state by clearing out the data structures
 */
static void
init_connection(connection_state_t* conn_state)
{
  conn_state->state = STATE_WAITING;

  init_request(&conn_state->request);
  init_response(&conn_state->response);
}

void
http_set_status(http_response_t* response, status_code_t status)
{
  response->status_code = status;
}

static http_header_t*
allocate_header(uint16_t variable_len)
{
  PRINTF("sizeof http_header_t %u variable size %u\n", sizeof(http_header_t), variable_len);
  uint8_t* buffer = allocate_buffer(sizeof(http_header_t) + variable_len);
  if (buffer) {
    http_header_t* option = (http_header_t*) buffer;
    option->next = NULL;
    option->name = NULL;
    option->value = buffer + sizeof(http_header_t);
    return option;
  }

  return NULL;
}

int
http_set_res_header(http_response_t* response, const char* name, const char* value, int copy)
{
  PRINTF("http_set_res_header (copy:%d) %s:%s\n", copy, name, value);
  uint16_t size = 0;
  http_header_t* current_header = NULL;
  http_header_t* head = NULL;

  if (copy) {
    size += strlen(value) + 1;
  }

  current_header = allocate_header(size);

  if (current_header) {
    current_header->name = (char*)name;
    if (copy) {
      strcpy(current_header->value, value);
    } else {
      current_header->value = (char*)value;
    }

    head = response->headers;
    response->headers = current_header;
    if (head) {
      current_header->next = head;
    }

    return 1;
  }

  return 0;
}

static const char* is_request_hdr_needed(const char* header_name)
{
  const char* header = NULL;
  /*FIXME add needed headers here*/
  if (strcmp(header_name, HTTP_HEADER_NAME_CONTENT_LENGTH) == 0) {
    header = HTTP_HEADER_NAME_CONTENT_LENGTH;
  } else if (strcmp(header_name, HTTP_HEADER_NAME_CONTENT_TYPE) == 0) {
    header = HTTP_HEADER_NAME_CONTENT_TYPE;
  }

  return header;
}

static service_callback service_cbk = NULL;

void
http_set_service_callback(service_callback callback)
{
  service_cbk = callback;
}

const char* content_types[] = {
  "text/plain",
  "text/xml",
  "text/csv",
  "text/html",
  "application/xml",
  "application/exi",
  "application/json",
  "application/link-format",
  "application/x-www-form-urlencoded",
};

const char*
http_get_content_type_string(content_type_t content_type)
{
  return content_types[content_type];
}

char*
get_default_status_string(status_code_t status_code)
{
  char* value = NULL;
  switch(status_code) {
    case 200:
      value = "OK";
      break;
    case 201:
      value = "Created";
      break;
    case 202:
      value = "Accepted";
      break;
    case 204:
      value = "No Content";
      break;
    case 304:
      value = "Not Modified";
      break;
    case 400:
      value = "Bad Request" ;
      break;
    case 404:
      value = "Not Found" ;
      break;
    case 405:
      value = "Method Not Allowed" ;
      break;
    case 406:
      value = "Not Acceptable" ;
      break;
    case 414:
      value = "Request-URI Too Long" ;
      break;
    case 415:
      value = "Unsupported Media Type" ;
      break;
    case 500:
      value = "Internal Server Error" ;
      break;
    case 501:
      value = "Not Implemented" ;
      break;
    case 503:
      value = "Service Unavailable" ;
      break;
    /*FIXME : will be removed later, put to catch the unhandled statuses.*/
    default:
      value = "$$BUG$$";
      break;
  }

  return value;
}

int
http_get_query_variable(http_request_t* request, const char *name, char* output, uint16_t output_size)
{
  if (request->query) {
    return get_variable(name, request->query, request->query_len, output, output_size, 0);
  }

  return 0;
}

int
http_get_post_variable(http_request_t* request, const char *name, char* output, uint16_t output_size)
{
  if (request->payload) {
    return get_variable(name, request->payload, request->payload_len, output, output_size, 1);
  }

  return 0;
}

static int
is_method_handled(connection_state_t* conn_state, const char* method)
{
  /*other method types can be added here if needed*/
  if(strncmp(method, http_get_string, 3) == 0) {
    conn_state->request.request_type = HTTP_METHOD_GET;
  } else if(strncmp(method, http_head_string, 4) == 0) {
    conn_state->request.request_type = HTTP_METHOD_HEAD;
  } else if (strncmp(method, http_post_string, 4) == 0) {
    conn_state->request.request_type = HTTP_METHOD_POST;
  } else if (strncmp(method, http_put_string, 3) == 0) {
    conn_state->request.request_type = HTTP_METHOD_PUT;
  } else if (strncmp(method, http_delete_string, 3) == 0) {
    conn_state->request.request_type = HTTP_METHOD_DELETE;
  } else {
    PRINTF("No Method supported : %s\nstate : %d\n", conn_state->inputbuf, conn_state->state);
    return 0;
  }

  return 1;
}

static int
parse_url(connection_state_t* conn_state, char* url)
{
  int error = HTTP_NO_ERROR;
  int full_url_path = 0;
  /*even for default index.html there is / Ex: GET / HTTP/1.1*/
  if (url[0] != '/') {
    /*if url is complete (http://...) rather than relative*/
    if (strncmp(url, http_string, 4) != 0 ) {
      PRINTF("Url not valid : %s \n",url);
      error = HTTP_URL_INVALID;
    } else {
      full_url_path = 1;
    }
  }

  if (error == HTTP_NO_ERROR) {
    char* url_buffer = url;
    if (full_url_path) {
      unsigned char num_of_slash = 0;
      do {
        url_buffer = strchr( ++url_buffer, '/' );

        PRINTF("Buffer : %s %d\n", url_buffer, num_of_slash);

      } while (url_buffer && ++num_of_slash < 3);
    }

    PRINTF("Url found :%s\n", url_buffer);

    /*Get rid of the first slash*/
    if (url_buffer && ++url_buffer) {
      conn_state->request.url = (char*) copy_text_to_buffer(url_buffer);
      conn_state->request.url_len = strlen(url_buffer);

      if ((conn_state->request.query = strchr(conn_state->request.url, '?'))) {
        *(conn_state->request.query++) = 0;
        /*update url len - decrease the size of query*/
        conn_state->request.url_len = strlen(conn_state->request.url);
        conn_state->request.query_len = strlen(conn_state->request.query);
      }

      PRINTF("url %s, url_len %u, query %s, query_len %u\n", conn_state->request.url, conn_state->request.url_len, conn_state->request.query, conn_state->request.query_len);

      /*FIXME url is not decoded - should be done here*/
    } else {
      error = HTTP_URL_INVALID;
    }
  }

  return error;
}

static int
parse_header(connection_state_t* conn_state, char* inputbuf)
{
  PRINTF("parse_header --->\n");
  const char* header_name = NULL;

  char* delimiter = strchr(inputbuf, ':');
  if (delimiter) {
    *delimiter++ = 0; /*after increment delimiter will point space char*/

    header_name = is_request_hdr_needed(inputbuf);
    if (header_name && delimiter) {
      char* buffer = delimiter;

      if (buffer[0] == ' ') {
        buffer++;
      }

      http_header_t* current_header = NULL;
      http_header_t* head = NULL;

      current_header = allocate_header(strlen(buffer));

      if (current_header) {
        current_header->name = (char*)header_name;
        strcpy(current_header->value, buffer);
      }

      head = conn_state->request.headers;
      conn_state->request.headers = current_header;
      if (head) {
        current_header->next = head;
      }

      return 1;
    }
  }

  return 0;
}

int
http_set_payload(http_response_t* response, uint8_t* payload, uint16_t size)
{
  response->payload = copy_to_buffer(payload, size);
  if (response->payload) {
    response->payload_len = size;
    return 1;
  }

  return 0;
}

static const char*
get_header(http_header_t* headers, const char* hdr_name)
{
  for (;headers; headers = headers->next) {
    if (strcmp(headers->name, hdr_name) == 0) {
      return headers->value;
    }
  }

  return NULL;
}

const char* http_get_req_header(http_request_t* request, const char* name)
{
  return get_header(request->headers, name);
}

content_type_t http_get_header_content_type(http_request_t* request)
{
  const char* content_type_string = http_get_req_header(request, HTTP_HEADER_NAME_CONTENT_TYPE);
  if (content_type_string) {
    int i = 0;
    for(; i < sizeof(content_types)/sizeof(const char*) ; i++) {
      if (strcmp(content_types[i], content_type_string)) {
        return (content_type_t)i;
      }
    }
  }

  return UNKNOWN_CONTENT_TYPE;
}

static
PT_THREAD(handle_request(connection_state_t* conn_state))
{
  PSOCK_BEGIN(&(conn_state->sin));

  static int error;
  const char* content_len = NULL;

  error = HTTP_NO_ERROR; /*always reinit static variables due to protothreads*/

  PRINTF("Request--->\n");

  //read method
  PSOCK_READTO(&(conn_state->sin), ' ');

  if (!is_method_handled(conn_state, conn_state->inputbuf)) {
    /*method not handled*/
    http_set_status(&conn_state->response, SERVICE_UNAVAILABLE_503);
    conn_state->state = STATE_OUTPUT;
  } else {
    /*read until the end of url*/
    PSOCK_READTO(&(conn_state->sin), ' ');

    /*-1 is needed since it also includes space char*/
    if (conn_state->inputbuf[PSOCK_DATALEN(&(conn_state->sin)) - 1] != ' ' ) {
      error = HTTP_URL_TOO_LONG;
    }

    conn_state->inputbuf[PSOCK_DATALEN(&(conn_state->sin)) - 1] = 0;

    PRINTF("Read URL:%s\n", conn_state->inputbuf);

    if (error == HTTP_NO_ERROR) {
      error = parse_url(conn_state, conn_state->inputbuf);
    }

    if (error != HTTP_NO_ERROR) {
      if (error == HTTP_URL_TOO_LONG) {
        http_set_status(&conn_state->response, REQUEST_URI_TOO_LONG_414);
      } else {
        http_set_status(&conn_state->response, BAD_REQUEST_400);
      }

      conn_state->state = STATE_OUTPUT;
    } else {
      /*read until the end of HTTP version - not used yet*/
      PSOCK_READTO(&(conn_state->sin), LINE_FEED_CHAR);

      PRINTF("After URL:%s\n", conn_state->inputbuf);

      /*FIXME : PSOCK_READTO takes just a single delimiter so I read till the end of line
      but now it may not fit in the buffer. If PSOCK_READTO would take two delimiters,
      we would have read until : and <CR> so it would not be blocked.*/

      /*Read the headers and store the necessary ones*/
      do {
        /*read the next line*/
        PSOCK_READTO(&(conn_state->sin), LINE_FEED_CHAR);
        conn_state->inputbuf[ PSOCK_DATALEN(&(conn_state->sin)) - 1] = 0;

        /*if headers finished then stop the infinite loop*/
        if (conn_state->inputbuf[0] == CARRIAGE_RETURN_CHAR || conn_state->inputbuf[0] == 0) {
          PRINTF("Finished Headers!\n\n");
          break;
        }

        parse_header(conn_state, conn_state->inputbuf);
      }
      while(1);

      content_len = get_header(conn_state->request.headers, HTTP_HEADER_NAME_CONTENT_LENGTH);
      if (content_len) {
        conn_state->request.payload_len = atoi(content_len);

        PRINTF("Post Data Size string: %s int: %d\n", content_len, conn_state->request.payload_len);
      }

      if (conn_state->request.payload_len) {
        static uint16_t read_bytes = 0;
        /*init the static variable again*/
        read_bytes = 0;

        conn_state->request.payload = allocate_buffer(conn_state->request.payload_len + 1);

        if (conn_state->request.payload) {
          do {
            PSOCK_READBUF(&(conn_state->sin));
            /*null terminate the buffer in case it is a string.*/
            conn_state->inputbuf[PSOCK_DATALEN(&(conn_state->sin))] = 0;

            memcpy(conn_state->request.payload + read_bytes, conn_state->inputbuf, PSOCK_DATALEN(&(conn_state->sin)));

            read_bytes += PSOCK_DATALEN(&(conn_state->sin));

          } while (read_bytes < conn_state->request.payload_len);

          conn_state->request.payload[read_bytes++] = 0;

          PRINTF("PostData => %s \n", conn_state->request.payload);
        } else {
          error = HTTP_MEMORY_ALLOC_ERR;
        }
      }

      if (error == HTTP_NO_ERROR) {
        if (service_cbk) {
          service_cbk(&conn_state->request, &conn_state->response);
        }
      } else {
        PRINTF("Error:%d\n",error);
        http_set_status(&conn_state->response, INTERNAL_SERVER_ERROR_500);
      }

      conn_state->state = STATE_OUTPUT;
    }
  }

  PSOCK_END(&(conn_state->sin));
}

static
PT_THREAD(send_data(connection_state_t* conn_state))
{
  PSOCK_BEGIN(&(conn_state->sout));

  PRINTF("send_data -> \n");

  uint16_t index = 0;
  http_response_t* response = &conn_state->response;
  http_header_t* header = response->headers;
  uint8_t* buffer = allocate_buffer(200);

  /*FIXME: what is the best solution here to send the data. Right now, if buffer is not allocated, no data is sent!*/
  if (buffer) {
    index += sprintf(buffer + index, "%s %d %s%s", httpv1_1, response->status_code, response->status_string, line_end);
    for (;header;header = header->next) {
      PRINTF("header %u \n", (uint16_t)header);
      index += sprintf(buffer + index, "%s%s%s%s", header->name, header_delimiter, header->value, line_end);
    }
    index += sprintf(buffer + index, "%s", line_end);

    memcpy(buffer + index, response->payload, response->payload_len);
    index += response->payload_len;

    PRINTF("Sending Data(size %d): %s \n", index, buffer);

    PSOCK_SEND(&(conn_state->sout), buffer, index);
  } else {
    PRINTF("BUFF ERROR: send_data!\n");
  }

  PSOCK_END(&(conn_state->sout));
}

static
PT_THREAD(handle_response(connection_state_t* conn_state))
{
  PT_BEGIN(&(conn_state->outputpt));

  PRINTF("handle_response ->\n");

  http_set_res_header(&conn_state->response, HTTP_HEADER_NAME_CONNECTION, close, 0);
  http_set_res_header(&conn_state->response, HTTP_HEADER_NAME_SERVER, contiki, 0);

  if (!(conn_state->response.status_string)) {
    conn_state->response.status_string =
        get_default_status_string(conn_state->response.status_code);
  }

  PT_WAIT_THREAD(&(conn_state->outputpt), send_data(conn_state));

  PRINTF("<-- handle_response\n\n\n");

  PSOCK_CLOSE(&(conn_state->sout));

  PT_END(&(conn_state->outputpt));
}

static void
handle_connection(connection_state_t* conn_state)
{
  if (conn_state->state == STATE_WAITING) {
    handle_request(conn_state);
  }

  if (conn_state->state == STATE_OUTPUT) {
    handle_response(conn_state);
  }
}

PROCESS(http_server, "Httpd Process");

PROCESS_THREAD(http_server, ev, data)
{
  PROCESS_BEGIN();

  /* if static routes are used rather than RPL */
#if !UIP_CONF_IPV6_RPL
  set_global_address();
  configure_routing();
#endif /*!UIP_CONF_IPV6_RPL*/

  #ifdef CONTIKI_TARGET_SKY
    PRINTF("##RF CHANNEL : %d##\n",RF_CHANNEL);
  #endif //CONTIKI_TARGET_SKY

  tcp_listen(uip_htons(HTTP_PORT));

  /*
   * We loop for ever, accepting new connections.
   */
  while(1) {
    PROCESS_WAIT_EVENT_UNTIL(ev == tcpip_event);

    connection_state_t *conn_state = (connection_state_t *)data;

    if(uip_connected()) {
      PRINTF("##Connected##\n");

      if(init_buffer(HTTP_DATA_BUFF_SIZE)) {
        conn_state = (connection_state_t*)allocate_buffer(sizeof(connection_state_t));

        if (conn_state) {
          tcp_markconn(uip_conn, conn_state);

          /*initialize connection state*/
          init_connection(conn_state);

          /*-1 is needed to be able to null terminate the strings in the buffer, especially good for debugging (to have null terminated strings)*/
          PSOCK_INIT(&(conn_state->sin), (uint8_t*)conn_state->inputbuf, sizeof(conn_state->inputbuf) - 1);
          PSOCK_INIT(&(conn_state->sout), (uint8_t*)conn_state->inputbuf, sizeof(conn_state->inputbuf) - 1);
          PT_INIT(&(conn_state->outputpt));

          handle_connection(conn_state);
        } else {
          PRINTF("Memory Alloc Error. Aborting!\n");
          uip_abort();
        }
      }
    } else if (uip_aborted() || uip_closed() || uip_timedout()) {
      if (conn_state) {
        delete_buffer();

        /*Following 2 lines are needed since this part of code is somehow executed twice so it tries to free the same region twice.
        Potential bug in uip*/
        conn_state = NULL;
        tcp_markconn(uip_conn, conn_state);
      }
    } else {
      handle_connection(conn_state);
    }
  }

  PROCESS_END();
}