/**
 *
 * Ulfius Framework example program
 *
 * This example program implements a websocket
 *
 * Copyright 2017-2022 Nicolas Mora <mail@babelouest.org>
 *
 * License MIT
 *
 */

#include <sys/stat.h>
#include <sys/types.h>

#include <inttypes.h>
#include <string.h>
#include <unistd.h>
#include <ulfius.h>

#include "u_example.h"

#include "static_compressed_inmemory_website_callback.h"

#define PORT 9275
#define PREFIX_WEBSOCKET "/websocket"
#define PREFIX_STATIC "/static"

#if defined(U_DISABLE_WEBSOCKET)
#error You must build ulfius with websocket support enabled to compile this example, check the install documentation
#else

int callback_websocket (const struct _u_request * request, struct _u_response * response, void * user_data);
int callback_websocket_echo (const struct _u_request * request, struct _u_response * response, void * user_data);
int callback_websocket_file (const struct _u_request * request, struct _u_response * response, void * user_data);

static char * read_file(const char * filename) {
  char * buffer = NULL;
  long length;
  FILE * f;
  if (filename != NULL) {
    f = fopen (filename, "rb");
    if (f) {
      fseek (f, 0, SEEK_END);
      length = ftell (f);
      fseek (f, 0, SEEK_SET);
      buffer = o_malloc ((size_t)(length + 1));
      if (buffer != NULL) {
        fread (buffer, 1, (size_t)length, f);
        buffer[length] = '\0';
      }
      fclose (f);
    }
    return buffer;
  } else {
    return NULL;
  }
}

/**
 * main function
 * open the wbservice on port 9275
 */
int main(int argc, char ** argv) {
  int ret;
  struct _u_instance instance;
  struct _u_compressed_inmemory_website_config file_config;
  char * cert_file = NULL, * key_file = NULL;

  y_init_logs("websocket_example", Y_LOG_MODE_CONSOLE, Y_LOG_LEVEL_DEBUG, NULL, "Starting websocket_example");

  if (u_init_compressed_inmemory_website_config(&file_config) == U_OK) {
    u_map_put(&file_config.mime_types, ".html", "text/html");
    u_map_put(&file_config.mime_types, ".css", "text/css");
    u_map_put(&file_config.mime_types, ".js", "application/javascript");
    u_map_put(&file_config.mime_types, ".png", "image/png");
    u_map_put(&file_config.mime_types, ".jpg", "image/jpeg");
    u_map_put(&file_config.mime_types, ".jpeg", "image/jpeg");
    u_map_put(&file_config.mime_types, ".ttf", "font/ttf");
    u_map_put(&file_config.mime_types, ".woff", "font/woff");
    u_map_put(&file_config.mime_types, ".woff2", "font/woff2");
    u_map_put(&file_config.mime_types, ".map", "application/octet-stream");
    u_map_put(&file_config.mime_types, ".json", "application/json");
    u_map_put(&file_config.mime_types, "*", "application/octet-stream");
    file_config.files_path = realpath("static", NULL);
    file_config.url_prefix = PREFIX_STATIC;

    if (ulfius_init_instance(&instance, PORT, NULL, NULL) != U_OK) {
      y_log_message(Y_LOG_LEVEL_ERROR, "Error ulfius_init_instance, abort");
      return(1);
    }

    u_map_put(instance.default_headers, "Access-Control-Allow-Origin", "*");

    // Endpoint list declaration
    ulfius_add_endpoint_by_val(&instance, "GET", PREFIX_WEBSOCKET, NULL, 0, &callback_websocket, NULL);
    ulfius_add_endpoint_by_val(&instance, "GET", PREFIX_WEBSOCKET, "/echo", 0, &callback_websocket_echo, NULL);
    ulfius_add_endpoint_by_val(&instance, "GET", PREFIX_WEBSOCKET, "/file", 0, &callback_websocket_file, NULL);
    ulfius_add_endpoint_by_val(&instance, "GET", PREFIX_STATIC, "*", 0, &callback_static_compressed_inmemory_website, &file_config);

    // Start the framework
    if (argc > 3 && 0 == o_strcmp(argv[1], "-https")) {
      key_file = read_file(argv[2]);
      cert_file = read_file(argv[3]);
      if (key_file == NULL || cert_file == NULL) {
        printf("Error reading https certificate files\n");
        ret = U_ERROR_PARAMS;
      } else {
        ret = ulfius_start_secure_framework(&instance, key_file, cert_file);
      }
      o_free(key_file);
      o_free(cert_file);
    } else {
      ret = ulfius_start_framework(&instance);
    }

    if (ret == U_OK) {
      y_log_message(Y_LOG_LEVEL_INFO, "Start framework on port %d %s", instance.port, (argc > 1 && 0 == o_strcmp(argv[1], "-https"))?"https mode":"http mode");

      // Wait for the user to press <enter> on the console to quit the application
      getchar();
    } else {
      y_log_message(Y_LOG_LEVEL_ERROR, "Error starting framework");
    }
    y_log_message(Y_LOG_LEVEL_INFO, "End framework");

    ulfius_stop_framework(&instance);
    ulfius_clean_instance(&instance);
    u_clean_compressed_inmemory_website_config(&file_config);
    free(file_config.files_path);
  }

  y_close_logs();

  return 0;
}

/**
 * websocket_onclose_callback
 * onclose callback function
 * Used to clear data after the websocket connection is closed
 */
void websocket_onclose_callback (const struct _u_request * request,
                                struct _websocket_manager * websocket_manager,
                                void * websocket_onclose_user_data) {
  (void)(request);
  (void)(websocket_manager);
  if (websocket_onclose_user_data != NULL) {
    y_log_message(Y_LOG_LEVEL_DEBUG, "websocket_onclose_user_data is %s", websocket_onclose_user_data);
    o_free(websocket_onclose_user_data);
  }
}

void websocket_onclose_file_callback (const struct _u_request * request,
                                struct _websocket_manager * websocket_manager,
                                void * websocket_onclose_user_data) {
  (void)(request);
  (void)(websocket_manager);
  (void)(websocket_onclose_user_data);
  y_log_message(Y_LOG_LEVEL_DEBUG, "websocket_onclose_file_callback");
}

/**
 * websocket_manager_callback
 * send 5 text messages and 1 ping for 11 seconds, then closes the websocket
 */
void websocket_manager_callback(const struct _u_request * request,
                               struct _websocket_manager * websocket_manager,
                               void * websocket_manager_user_data) {
  if (websocket_manager_user_data != NULL) {
    y_log_message(Y_LOG_LEVEL_DEBUG, "websocket_manager_user_data is %s", websocket_manager_user_data);
  }

#ifndef U_DISABLE_WS_MESSAGE_LIST
  websocket_manager->keep_messages = U_WEBSOCKET_KEEP_OUTCOMING;
#endif

  // Send text message without fragmentation
  if (ulfius_websocket_wait_close(websocket_manager, 2000) == U_WEBSOCKET_STATUS_OPEN) {
    if (ulfius_websocket_send_message(websocket_manager, U_WEBSOCKET_OPCODE_TEXT, o_strlen("Message without fragmentation from server"), "Message without fragmentation from server") != U_OK) {
      y_log_message(Y_LOG_LEVEL_ERROR, "Error send message without fragmentation");
    }
  }

  // Send text message with fragmentation for ulfius clients only, browsers seem to dislike fragmented messages
  if (o_strncmp(u_map_get(request->map_header, "User-Agent"), U_WEBSOCKET_USER_AGENT, o_strlen(U_WEBSOCKET_USER_AGENT)) == 0 &&
      ulfius_websocket_wait_close(websocket_manager, 2000) == U_WEBSOCKET_STATUS_OPEN) {
    if (ulfius_websocket_send_fragmented_message(websocket_manager, U_WEBSOCKET_OPCODE_TEXT, o_strlen("Message with fragmentation from server"), "Message with fragmentation from server", 5) != U_OK) {
      y_log_message(Y_LOG_LEVEL_ERROR, "Error send message with fragmentation");
    }
  }

  // Send ping message
  if (ulfius_websocket_wait_close(websocket_manager, 2000) == U_WEBSOCKET_STATUS_OPEN) {
    if (ulfius_websocket_send_message(websocket_manager, U_WEBSOCKET_OPCODE_PING, 0, NULL) != U_OK) {
      y_log_message(Y_LOG_LEVEL_ERROR, "Error send ping message");
    }
  }

  // Send binary message without fragmentation
  if (ulfius_websocket_wait_close(websocket_manager, 2000) == U_WEBSOCKET_STATUS_OPEN) {
    if (ulfius_websocket_send_message(websocket_manager, U_WEBSOCKET_OPCODE_BINARY, o_strlen("Message without fragmentation from server"), "Message without fragmentation from server") != U_OK) {
      y_log_message(Y_LOG_LEVEL_ERROR, "Error send binary message without fragmentation");
    }
  }

  // Send JSON message without fragmentation
#ifndef U_DISABLE_JANSSON
  if (ulfius_websocket_wait_close(websocket_manager, 2000) == U_WEBSOCKET_STATUS_OPEN) {
    json_t * message = json_pack("{ss}", "send", "JSON message without fragmentation");
    if (ulfius_websocket_send_json_message(websocket_manager, message) != U_OK) {
      y_log_message(Y_LOG_LEVEL_ERROR, "Error send JSON message without fragmentation");
    }
    json_decref(message);
  }
#endif

  y_log_message(Y_LOG_LEVEL_DEBUG, "Closing websocket_manager_callback");
}

void websocket_manager_file_callback(const struct _u_request * request,
                               struct _websocket_manager * websocket_manager,
                               void * websocket_manager_user_data) {
  (void)(request);
  (void)(websocket_manager_user_data);
  y_log_message(Y_LOG_LEVEL_DEBUG, "Opening websocket_manager_file_callback");
  for (;;) {
    sleep(1);
    if (websocket_manager == NULL || !websocket_manager->connected) {
      break;
    }
  }
  y_log_message(Y_LOG_LEVEL_DEBUG, "Closing websocket_manager_file_callback");
}

/**
 * websocket_incoming_message_callback
 * Read incoming message and prints it on the console
 */
void websocket_incoming_message_callback (const struct _u_request * request,
                                         struct _websocket_manager * websocket_manager,
                                         const struct _websocket_message * last_message,
                                         void * websocket_incoming_message_user_data) {
  (void)(request);
  (void)(websocket_manager);
  if (websocket_incoming_message_user_data != NULL) {
    y_log_message(Y_LOG_LEVEL_DEBUG, "websocket_incoming_message_user_data is %s", websocket_incoming_message_user_data);
  }
  y_log_message(Y_LOG_LEVEL_DEBUG, "Incoming message, rsv: 0x%02x, opcode: 0x%02x, mask: %d, len: %zu", last_message->rsv, last_message->opcode, last_message->has_mask, last_message->data_len);
  if (last_message->opcode == U_WEBSOCKET_OPCODE_TEXT) {
    y_log_message(Y_LOG_LEVEL_DEBUG, "text payload '%.*s'", last_message->data_len, last_message->data);
  } else if (last_message->opcode == U_WEBSOCKET_OPCODE_BINARY) {
    y_log_message(Y_LOG_LEVEL_DEBUG, "binary payload");
  }
}

void websocket_echo_message_callback (const struct _u_request * request,
                                         struct _websocket_manager * websocket_manager,
                                         const struct _websocket_message * last_message,
                                         void * websocket_incoming_message_user_data) {
  (void)(request);
  (void)(websocket_incoming_message_user_data);
  y_log_message(Y_LOG_LEVEL_DEBUG, "Incoming message, rsv: 0x%02x, opcode: 0x%02x, mask: %d, len: %zu, text payload '%.*s'", last_message->rsv, last_message->opcode, last_message->has_mask, last_message->data_len, last_message->data_len, last_message->data);
  if (ulfius_websocket_send_message(websocket_manager, last_message->opcode, last_message->data_len, last_message->data) != U_OK) {
    y_log_message(Y_LOG_LEVEL_ERROR, "Error ulfius_websocket_send_message");
  }
}

void websocket_incoming_file_callback (const struct _u_request * request,
                                         struct _websocket_manager * websocket_manager,
                                         const struct _websocket_message * last_message,
                                         void * websocket_incoming_message_user_data) {
  (void)(request);
  (void)(websocket_incoming_message_user_data);
  char * my_message = msprintf("Incoming file %p, rsv: 0x%02x, opcode: 0x%02x, mask: %d, len: %zu", last_message, last_message->rsv, last_message->opcode, last_message->has_mask, last_message->data_len);
  y_log_message(Y_LOG_LEVEL_DEBUG, my_message);
  ulfius_websocket_send_message(websocket_manager, U_WEBSOCKET_OPCODE_TEXT, o_strlen(my_message), my_message);
  o_free(my_message);
}

/**
 * Ulfius main callback function that simply calls the websocket manager and closes
 */
int callback_websocket (const struct _u_request * request, struct _u_response * response, void * user_data) {
  char * websocket_user_data = o_strdup("my_user_data");
  int ret;
  (void)(request);
  (void)(user_data);

  if ((ret = ulfius_set_websocket_response(response, NULL, NULL, &websocket_manager_callback, websocket_user_data, &websocket_incoming_message_callback, websocket_user_data, &websocket_onclose_callback, websocket_user_data)) == U_OK) {
    ulfius_add_websocket_deflate_extension(response);
    return U_CALLBACK_CONTINUE;
  } else {
    return U_CALLBACK_ERROR;
  }
}

int callback_websocket_echo (const struct _u_request * request, struct _u_response * response, void * user_data) {
  char * websocket_user_data = o_strdup("my_user_data");
  int ret;
  (void)(request);
  (void)(user_data);

  y_log_message(Y_LOG_LEVEL_DEBUG, "Client connected to echo websocket");
  if ((ret = ulfius_set_websocket_response(response, NULL, NULL, NULL, NULL, &websocket_echo_message_callback, websocket_user_data, &websocket_onclose_callback, websocket_user_data)) == U_OK) {
    ulfius_add_websocket_deflate_extension(response);
    return U_CALLBACK_CONTINUE;
  } else {
    return U_CALLBACK_ERROR;
  }
}

int callback_websocket_file (const struct _u_request * request, struct _u_response * response, void * user_data) {
  int ret;
  (void)(request);
  (void)(user_data);

  if ((ret = ulfius_set_websocket_response(response, NULL, NULL, &websocket_manager_file_callback, NULL, &websocket_incoming_file_callback, NULL, &websocket_onclose_file_callback, NULL)) == U_OK) {
    ulfius_add_websocket_deflate_extension(response);
    return U_CALLBACK_CONTINUE;
  } else {
    return U_CALLBACK_ERROR;
  }
}
#endif
