#include <cstdint>
#include <assert.h>
#include <cerrno>
#include <cstring>
#include <sys/socket.h>
#include "mesa/util/os_socket.h"
#include "overlay.h"
#include "version.h"
#include "app/mangoapp.h"

int global_control_client;

using namespace std;
static void parse_command(overlay_params &params,
                          const char *cmd, unsigned cmdlen,
                          const char *param, unsigned paramlen)
{
   if (!strncmp(cmd, "hud", cmdlen)) {
      params.no_display = !params.no_display;
   } else if (!strncmp(cmd, "logging", cmdlen)) {
      if (param && param[0])
      {
         int value = atoi(param);
         if (!value && logger->is_active())
            logger->stop_logging();
         else if (value > 0 && !logger->is_active())
            logger->start_logging();
      }
      else
      {
         if (logger->is_active())
            logger->stop_logging();
         else
            logger->start_logging();
      }
   } else if (!strncmp(cmd, "fcat", cmdlen)) {
      params.enabled[OVERLAY_PARAM_ENABLED_fcat] = !params.enabled[OVERLAY_PARAM_ENABLED_fcat];
   }
}

#define BUFSIZE 4096

/**
 * This function will process commands through the control file.
 *
 * A command starts with a colon, followed by the command, and followed by an
 * option '=' and a parameter.  It has to end with a semi-colon. A full command
 * + parameter looks like:
 *
 *    :cmd=param;
 */
static void process_char(const int control_client, overlay_params &params, char c)
{
   static char cmd[BUFSIZE];
   static char param[BUFSIZE];

   static unsigned cmdpos = 0;
   static unsigned parampos = 0;
   static bool reading_cmd = false;
   static bool reading_param = false;

   switch (c) {
   case ':':
      cmdpos = 0;
      parampos = 0;
      reading_cmd = true;
      reading_param = false;
      break;
   case ';':
      if (!reading_cmd)
         break;
      cmd[cmdpos++] = '\0';
      param[parampos++] = '\0';
      parse_command(params, cmd, cmdpos, param, parampos);
      reading_cmd = false;
      reading_param = false;
      break;
   case '=':
      if (!reading_cmd)
         break;
      reading_param = true;
      break;
   default:
      if (!reading_cmd)
         break;

      if (reading_param) {
         /* overflow means an invalid parameter */
         if (parampos >= BUFSIZE - 1) {
            reading_cmd = false;
            reading_param = false;
            break;
         }

         param[parampos++] = c;
      } else {
         /* overflow means an invalid command */
         if (cmdpos >= BUFSIZE - 1) {
            reading_cmd = false;
            break;
         }

         cmd[cmdpos++] = c;
      }
   }
}

void control_send(int control_client,
                         const char *cmd, unsigned cmdlen,
                         const char *param, unsigned paramlen)
{
   unsigned msglen = 0;
   char buffer[BUFSIZE];

   assert(cmdlen + paramlen + 3 < BUFSIZE);

   buffer[msglen++] = ':';

   memcpy(&buffer[msglen], cmd, cmdlen);
   msglen += cmdlen;

   if (paramlen > 0) {
      buffer[msglen++] = '=';
      memcpy(&buffer[msglen], param, paramlen);
      msglen += paramlen;
      buffer[msglen++] = ';';
   }

   os_socket_send(control_client, buffer, msglen, MSG_NOSIGNAL);
}

static void control_send_connection_string(int control_client, const std::string& deviceName)
{
   const char *controlVersionCmd = "MangoHudControlVersion";
   const char *controlVersionString = "1";

   control_send(control_client, controlVersionCmd, strlen(controlVersionCmd),
                controlVersionString, strlen(controlVersionString));

   const char *deviceCmd = "DeviceName";

   control_send(control_client, deviceCmd, strlen(deviceCmd),
                deviceName.c_str(), deviceName.size());

   const char *versionCmd = "MangoHudVersion";
   const char *versionString = "MangoHud " MANGOHUD_VERSION;

   control_send(control_client, versionCmd, strlen(versionCmd),
                versionString, strlen(versionString));

}

void control_client_check(int control, int& control_client, const std::string& deviceName)
{
   /* Already connected, just return. */
   if (control_client >= 0){
      global_control_client = control_client;
      return;
   }

   int socket = os_socket_accept(control);
   if (socket == -1) {
      if (errno != EAGAIN && errno != EWOULDBLOCK && errno != ECONNABORTED)
         fprintf(stderr, "ERROR on socket: %s\n", strerror(errno));
      return;
   }

   if (socket >= 0) {
      os_socket_block(socket, false);
      control_client = socket;
      control_send_connection_string(control_client, deviceName);
   }
}

static void control_client_disconnected(int& control_client)
{
   os_socket_close(control_client);
   control_client = -1;
}

void process_control_socket(int& control_client, overlay_params &params)
{
   if (control_client >= 0) {
      char buf[BUFSIZE];

      while (true) {
         ssize_t n = os_socket_recv(control_client, buf, BUFSIZE, 0);

         if (n == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
               /* nothing to read, try again later */
               break;
            }

            if (errno != ECONNRESET)
               fprintf(stderr, "ERROR on connection: %s\n", strerror(errno));

            control_client_disconnected(control_client);
         } else if (n == 0) {
            /* recv() returns 0 when the client disconnects */
            control_client_disconnected(control_client);
         }

         for (ssize_t i = 0; i < n; i++) {
            process_char(control_client, params, buf[i]);
         }

         /* If we try to read BUFSIZE and receive BUFSIZE bytes from the
          * socket, there's a good chance that there's still more data to be
          * read, so we will try again. Otherwise, simply be done for this
          * iteration and try again on the next frame.
          */
         if (n < BUFSIZE)
            break;
      }
   }
}
