/*
 * Copyright © 2022 Codethink Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice (including the
 * next paragraph) shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <microhttpd.h>

#include "backend.h"
#include "backends/input/evdev_input.h"
#include "backends/input/input_common.h"
#include "backends/input/uinput.h"
#include "cjson/cJSON.h"
#include "config.h"
#ifdef HAVE_ILM
#include "backends/screen/ilm.h"
#endif
#ifdef HAVE_KMS
#include "backends/screen/kms.h"
#endif
#include "version.h"

typedef enum { JSON_INT, JSON_STRING } JSON_VALUE_TYPE;

typedef struct connection_info_struct_s {
  char *post_buffer;
  int post_buffer_size;
  unsigned char *get_buffer;
  int get_buffer_size;
} connection_info_struct_t;

cJSON *parse_json(const char *post_data, int post_data_size, char *error) {
  cJSON *json = cJSON_ParseWithLength(post_data, post_data_size);

  if (!json) {
    sprintf(error, "Error - Posted data must be JSON");
    return NULL;
  }
  return json;
}

int get_json_value(JSON_VALUE_TYPE type, cJSON *json, char *jvalue,
                   void *value) {
  cJSON *json_value = cJSON_GetObjectItemCaseSensitive(json, jvalue);
  switch (type) {
  case JSON_INT:
    if (!cJSON_IsNumber(json_value)) {
      return 0;
    }
    *(int *)value = json_value->valueint;
    return 1;
  case JSON_STRING:
    if (!cJSON_IsString(json_value)) {
      return 0;
    }
    *(char **)value = strdup(json_value->valuestring);
    return 1;
  }

  return 0;
}

int get_json_int_array(cJSON *json, char *jvalue, int **out_array,
                       int *out_num_integers) {
  cJSON *json_array = cJSON_GetObjectItemCaseSensitive(json, jvalue);
  if (!cJSON_IsArray(json_array)) {
    return 0;
  }

  int num_items = cJSON_GetArraySize(json_array);
  int num_integers = 0;
  int *array = malloc(sizeof(int) * num_items);

  for (int i = 0; i < num_items; i++) {
    cJSON *int_value = cJSON_GetArrayItem(json_array, i);
    if (cJSON_IsNumber(int_value)) {
      array[num_integers] = int_value->valueint;
      num_integers++;
    } else {
      free(array);
      return 0;
    }
  }

  *out_array = array;
  *out_num_integers = num_integers;

  return 1;
}

void qad_move(const char *post_data, int post_data_size, qad_backend_t *backend,
              char *error) {
  int x;
  int y;
  int event;

  cJSON *json = parse_json(post_data, post_data_size, error);

  if (!json)
    return;

  if (!get_json_value(JSON_INT, json, "x", &x) ||
      !get_json_value(JSON_INT, json, "y", &y) ||
      !get_json_value(JSON_INT, json, "event", &event)) {

    sprintf(error, "Coordinates and event number must be integers\n");
    return;
  }

  cJSON_Delete(json);

  if (backend->input_backend.move(x, y, event) < 0) {
    int err = errno;
    sprintf(error, "Error sending move: %s\n", strerror(err));
    return;
  }
  return;
}

void qad_button(const char *post_data, int post_data_size,
                qad_backend_t *backend, char *error) {
  int value;
  int event;

  cJSON *json = parse_json(post_data, post_data_size, error);
  if (!json)
    return;

  if (!get_json_value(JSON_INT, json, "value", &value) ||
      !get_json_value(JSON_INT, json, "event", &event)) {

    sprintf(error, "Button value and event number must be integers\n");
    return;
  }

  cJSON_Delete(json);

  if (backend->input_backend.button(value, event) < 0) {
    int err = errno;
    sprintf(error, "Error sending button: %s\n", strerror(err));
    return;
  }
  return;
}

void qad_touch(const char *post_data, int post_data_size,
               qad_backend_t *backend, char *error) {
  int x;
  int y;
  int event;
  int duration;

  cJSON *json = parse_json(post_data, post_data_size, error);
  if (!json) {
    return;
  }

  if (!get_json_value(JSON_INT, json, "x", &x) ||
      !get_json_value(JSON_INT, json, "y", &y) ||
      !get_json_value(JSON_INT, json, "event", &event) ||
      !get_json_value(JSON_INT, json, "duration", &duration)) {
    sprintf(
        error,
        "Error - Coordinates, event number and duration must be integers\n");
    return;
  }

  cJSON_Delete(json);

  if (backend->input_backend.touch(x, y, duration, event) < 0) {
    int err = errno;
    sprintf(error, "Error - Error sending touch %s\n", strerror(err));
    return;
  }
  return;
}

void qad_swipe(const char *post_data, int post_data_size,
               qad_backend_t *backend, char *error) {
  int x;
  int y;
  int x2;
  int y2;
  int event;
  int velocity;
  int stop;

  cJSON *json = parse_json(post_data, post_data_size, error);
  if (!json)
    return;

  if (!get_json_value(JSON_INT, json, "x", &x) ||
      !get_json_value(JSON_INT, json, "x2", &x2) ||
      !get_json_value(JSON_INT, json, "y", &y) ||
      !get_json_value(JSON_INT, json, "y2", &y2) ||
      !get_json_value(JSON_INT, json, "event", &event) ||
      !get_json_value(JSON_INT, json, "velocity", &velocity)) {

    sprintf(error, "Coordinates, event number and velocity must "
                   "be integers\n");
    return;
  }
  if (!cJSON_HasObjectItem(json, "stop")) {
    stop = 0;
  } else if (!get_json_value(JSON_INT, json, "stop", &stop)) {
    snprintf(error, 255,
             "Stop value must be an integer specifying a time in ms\n");
    return;
  }
  cJSON_Delete(json);
  if (stop > 10000) {
    snprintf(error, 255,
             "Stop time cannot be more than 10000ms (10 seconds)\n");
    return;
  }

  if (backend->input_backend.swipe(x, y, x2, y2, velocity, event, stop) < 0) {
    int err = errno;
    sprintf(error, "Error sending swipe: %s\n", strerror(err));
    return;
  }
  return;
}

void qad_key(const char *post_data, int post_data_size, qad_backend_t *backend,
             char *error) {
  int key;
  int event;

  cJSON *json = parse_json(post_data, post_data_size, error);
  if (!json)
    return;

  if (!get_json_value(JSON_INT, json, "key", &key) ||
      !get_json_value(JSON_INT, json, "event", &event)) {

    sprintf(error, "key and event number must be integers\n");
    return;
  }

  cJSON_Delete(json);

  if (backend->input_backend.key(key, event) < 0) {
    int err = errno;
    sprintf(error, "Error pressing key: %s\n", strerror(err));
    return;
  }
  return;
}

void qad_text(const char *post_data, int post_data_size, qad_backend_t *backend,
              char *error) {
  int *keycodes;
  int num_keycodes;
  int event;

  cJSON *json = parse_json(post_data, post_data_size, error);
  if (!json)
    return;

  if (!get_json_int_array(json, "text", &keycodes, &num_keycodes) ||
      !get_json_value(JSON_INT, json, "event", &event)) {

    sprintf(
        error,
        "text must be an array of integers and event number must be integer\n");
    return;
  }

  cJSON_Delete(json);

  if (backend->input_backend.text(keycodes, num_keycodes, event) < 0) {
    int err = errno;
    sprintf(error, "Error sending text: %s\n", strerror(err));
    return;
  }
  return;
}

void qad_screenshot(struct MHD_Connection *connection, const char *url,
                    qad_backend_t *backend, connection_info_struct_t *con_info,
                    char *error) {
  const char *pLastSlash = strrchr(url, '/');
  const char *screenName = pLastSlash ? pLastSlash + 1 : NULL;
  int screenNumber = -1;

  if (screenName && *screenName != '\0') {
    char *endptr;
    screenNumber = strtol(screenName, &endptr, 10);
    if (*endptr != '\0') {
      fprintf(stderr, "Could not parse screen number\n");
      sprintf(error, "Could not parse screen number\n");
      return;
    }
  } else {
    sprintf(error, "Could not parse screen number\n");
    backend->screen_backend.list_fbs(error);
    fprintf(stderr, "%s", error);
    return;
  }

  qad_screen_buffer_t buffer;
  memset(&buffer, 0, sizeof(qad_screen_buffer_t));

  if (backend->screen_backend.grab_fb(&buffer, screenNumber)) {
    fprintf(stderr, "Error taking screenshot\n");
    sprintf(error, "Error taking screenshot\n");
  } else {
    con_info->get_buffer = buffer.buffer;
    con_info->get_buffer_size = buffer.buffer_size;
  }
}

void qad_get_handler(struct MHD_Connection *connection, const char *url,
                     qad_backend_t *backend, connection_info_struct_t *con_info,
                     char *error) {
  if (strcmp(url, "/screen") == 0) {
    backend->screen_backend.list_fbs(error);
    fprintf(stderr, "%s", error);
  } else if (strstr(url, "/screen/") != NULL) {
    qad_screenshot(connection, url, backend, con_info, error);
  } else {
    sprintf(error, "%s unimplemented\n", url);
  }
}

void qad_post_handler(struct MHD_Connection *connection, const char *url,
                      const char *post_data, int post_data_size,
                      qad_backend_t *backend, char *error) {
  if (strcmp(url, "/move") == 0) {
    qad_move(post_data, post_data_size, backend, error);
  } else if (strcmp(url, "/button") == 0) {
    qad_button(post_data, post_data_size, backend, error);
  } else if (strcmp(url, "/touch") == 0) {
    qad_touch(post_data, post_data_size, backend, error);
  } else if (strcmp(url, "/swipe") == 0) {
    qad_swipe(post_data, post_data_size, backend, error);
  } else if (strcmp(url, "/key") == 0) {
    qad_key(post_data, post_data_size, backend, error);
  } else if (strcmp(url, "/text") == 0) {
    qad_text(post_data, post_data_size, backend, error);
  }
}

static enum MHD_Result
qad_server_handler(void *cls, struct MHD_Connection *connection,
                   const char *url, const char *method, const char *version,
                   const char *upload_data, size_t *upload_data_size,
                   void **con_cls) {
  char error[255] = {0};
  struct MHD_Response *response;
  int ret;
  qad_backend_t *backend = (qad_backend_t *)cls;

  if (*con_cls == NULL) {
    connection_info_struct_t *con_info;
    con_info = malloc(sizeof(connection_info_struct_t));
    if (con_info == NULL)
      return MHD_NO;
    *con_cls = (void *)con_info;
    return MHD_YES;
  }

  if (strcmp(method, "GET") == 0) {
    connection_info_struct_t *con_info = *con_cls;
    qad_get_handler(connection, url, backend, con_info, error);
    if (strlen(error) == 0) {
      response = MHD_create_response_from_buffer(
          con_info->get_buffer_size, (unsigned char *)con_info->get_buffer,
          MHD_RESPMEM_MUST_FREE);
      MHD_add_response_header(response, "Access-Control-Allow-Origin", "*");
      MHD_add_response_header(response, "Access-Control-Allow-Methods",
                              "OPTIONS, GET, POST, PUT, PATCH, DELETE");
      MHD_add_response_header(response, "Access-Control-Allow-Headers",
                              "Content-Type, Authorization");
      ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
      MHD_destroy_response(response);
      free(con_info);
      *con_cls = NULL;
      return ret;
    }
  } else if (strcmp(method, "POST") == 0) {
    connection_info_struct_t *con_info = *con_cls;

    if (*upload_data_size != 0) {
      con_info->post_buffer = malloc(*upload_data_size + 1);
      con_info->post_buffer_size = *upload_data_size;
      strncpy(con_info->post_buffer, upload_data, *upload_data_size);
      con_info->post_buffer[*upload_data_size] = 0;
      *upload_data_size = 0;
      return MHD_YES;
    }
    qad_post_handler(connection, url, con_info->post_buffer,
                     con_info->post_buffer_size, backend, error);
    free(con_info->post_buffer);
    free(con_info);
    *con_cls = NULL;
  }

  if (strlen(error) == 0) {
    response = MHD_create_response_from_buffer(0, (void *)NULL,
                                               MHD_RESPMEM_PERSISTENT);
    MHD_add_response_header(response, "Access-Control-Allow-Origin", "*");
    MHD_add_response_header(response, "Access-Control-Allow-Methods",
                            "OPTIONS, GET, POST, PUT, PATCH, DELETE");
    MHD_add_response_header(response, "Access-Control-Allow-Headers",
                            "Content-Type, Authorization");
    ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
  } else {
    response = MHD_create_response_from_buffer(strlen(error), (void *)error,
                                               MHD_RESPMEM_MUST_COPY);
    ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
  }

  MHD_destroy_response(response);
  return ret;
}

static void quit(int sig) {
  /* Exit cleanly on ^C in case we're valgrinding. */
  exit(0);
}

int init_backends(qad_backend_t *backend, char input_type[],
                  char screen_backend[], char kms_backend_card[],
                  int kms_format_rgb) {
  if (strcmp(screen_backend, "kms") == 0) {
    printf("Using KMS backend\n");
#ifdef HAVE_KMS
    if (kms_create_backend(&backend->screen_backend, kms_backend_card,
                           kms_format_rgb) != 0) {
      fprintf(stderr,
              "Cannot create kms backend with option kms-backend-card=%s\n",
              kms_backend_card);
      return -1;
    }
#else
    fprintf(stderr, "Attempting to use KMS backend without support!\n");
    return -1;
#endif
  } else if (strcmp(screen_backend, "ilm") == 0) {
    printf("Using ILM backend\n");
#ifdef HAVE_ILM
    if (ilm_create_backend(&backend->screen_backend) != 0) {
      return -1;
    }
#else
    fprintf(stderr, "Attempting to use ILM backend without support!\n");
    exit(1);
#endif
  }

  return create_input_backend(&backend->input_backend, input_type);
}

void deinit_backends(char *screen_backend) {
#ifdef HAVE_ILM
  if (strcmp(screen_backend, "ilm") == 0) {
    ilm_destroy_backend();
  }
#endif
}

static void print_version() { printf("QAD version: %s\n", QAD_VERSION); }

static void print_usage(FILE *stream, const char *prog) {
  fprintf(stream, "Usage: %s [OPTION]...\n", prog);
  fprintf(
      stream,
      "  -h, --help                    display this help\n"
      "  -p, --port PORT               port to bind to\n"
      "  -i, --input-type TYPE         uinput or evdev; defaults to uinput\n"
      "  -v, --version                 display version\n"
      "  -s, --screen-backend BACKEND  kms or ilm; defaults to kms\n"
      "  -k, --kms-backend-card CARD   set DRM device; defaults to card0\n"
      "  -r, --kms-format-rgb          use RGB pixel format instead of BGR\n");
}

int main(int argc, char **argv) {
  struct MHD_Daemon *daemon;
  static int port = 8080;
  static char input_type[100] = "uinput";
  static char screen_backend[10] = "kms";
  static char kms_backend_card[6] = "card0";
  static int kms_format_rgb = 0;
  static int show_version = 0;
  int opt = 0;

  static struct option options[] = {
      {"help", no_argument, NULL, 'h'},
      {"port", required_argument, NULL, 'p'},
      {"input-type", required_argument, NULL, 'i'},
      {"version", no_argument, NULL, 'v'},
      {"screen-backend", required_argument, NULL, 's'},
      {"kms-backend-card", required_argument, NULL, 'k'},
      {"kms-format-rgb", no_argument, NULL, 'r'},
      {NULL, 0, NULL, 0}};

  while ((opt = getopt_long(argc, argv, "hvp:i:s:k:r", options, NULL)) != -1) {
    switch (opt) {
    case 'h':
      print_usage(stdout, argv[0]);
      return EXIT_SUCCESS;
    case 'v':
      show_version = 1;
      break;
    case 'p':
      port = atoi(optarg);
      break;
    case 'i':
      sprintf(input_type, "%s", optarg);
      break;
    case 's':
      sprintf(screen_backend, "%s", optarg);
      break;
    case 'k':
      sprintf(kms_backend_card, "%s", optarg);
      break;
    case 'r':
      kms_format_rgb = 1;
      break;
    default:
      print_usage(stderr, argv[0]);
      return EXIT_FAILURE;
    }
  }

  if (show_version) {
    print_version();
    return EXIT_SUCCESS;
  }

  signal(SIGINT, quit);

  /* Create backends */
  qad_backend_t backend;
  memset(&backend, 0, sizeof(qad_backend_t));
  if (init_backends(&backend, input_type, screen_backend, kms_backend_card,
                    kms_format_rgb) != 0) {
    fprintf(stderr, "Failed to create QAD backends!\n");
    return EXIT_FAILURE;
  }

  printf("Starting QAD on port %i\n", port);
  daemon = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, port, NULL, NULL,
                            &qad_server_handler, &backend, MHD_OPTION_END);

  if (!daemon) {
    fprintf(stderr, "Failed to start QAD server daemon!\n");
    return EXIT_FAILURE;
  }

  printf("Running QAD, Enter q to quit\n");
  char c = getchar();
  while (c != 'q') {
    c = getchar();
  }
  MHD_stop_daemon(daemon);
  deinit_backends(screen_backend);
  return EXIT_SUCCESS;
}
