/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2010-2014 - Hans-Kristian Arntzen
 *  Copyright (C) 2011-2016 - Daniel De Matteis
 *  Copyright (C)      2016 - Gregor Richards
 *
 *  RetroArch is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include <compat/strl.h>

#include "netplay_private.h"
#include <net/net_socket.h>
#include <compat/strl.h>

#include <encodings/crc32.h>

#include "../../movie.h"
#include "../../msg_hash.h"
#include "../../content.h"
#include "../../runloop.h"
#include "../../version.h"

bool netplay_get_nickname(netplay_t *netplay, int fd)
{
   uint8_t nick_size;

   if (!socket_receive_all_blocking(fd, &nick_size, sizeof(nick_size)))
   {
      RARCH_ERR("%s\n",
            msg_hash_to_str(MSG_FAILED_TO_RECEIVE_NICKNAME_SIZE_FROM_HOST));
      return false;
   }

   if (nick_size >= sizeof(netplay->other_nick))
   {
      RARCH_ERR("%s\n",
            msg_hash_to_str(MSG_INVALID_NICKNAME_SIZE));
      return false;
   }

   if (!socket_receive_all_blocking(fd, netplay->other_nick, nick_size))
   {
      RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_RECEIVE_NICKNAME));
      return false;
   }

   return true;
}
bool netplay_send_nickname(netplay_t *netplay, int fd)
{
   uint8_t nick_size = strlen(netplay->nick);

   if (!socket_send_all_blocking(fd, &nick_size, sizeof(nick_size), false))
   {
      RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_SEND_NICKNAME_SIZE));
      return false;
   }

   if (!socket_send_all_blocking(fd, netplay->nick, nick_size, false))
   {
      RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_SEND_NICKNAME));
      return false;
   }

   return true;
}

/**
 * netplay_impl_magic:
 *
 * Not really a hash, but should be enough to differentiate 
 * implementations from each other.
 *
 * Subtle differences in the implementation will not be possible to spot.
 * The alternative would have been checking serialization sizes, but it 
 * was troublesome for cross platform compat.
 **/
uint32_t netplay_impl_magic(void)
{
   size_t i, len;
   retro_ctx_api_info_t api_info;
   unsigned api;
   uint32_t res                        = 0;
   rarch_system_info_t *info           = NULL;
   const char *lib                     = NULL;
   const char *ver                     = PACKAGE_VERSION;

   core_api_version(&api_info);

   api                                 = api_info.version;

   runloop_ctl(RUNLOOP_CTL_SYSTEM_INFO_GET, &info);

   res |= api;

   if (info)
   {
      lib = info->info.library_name;

      len = strlen(lib);
      for (i = 0; i < len; i++)
         res ^= lib[i] << (i & 0xf);

      lib = info->info.library_version;
      len = strlen(lib);

      for (i = 0; i < len; i++)
         res ^= lib[i] << (i & 0xf);
   }

   len = strlen(ver);
   for (i = 0; i < len; i++)
      res ^= ver[i] << ((i & 0xf) + 16);

   res ^= NETPLAY_PROTOCOL_VERSION << 24;

   return res;
}

/**
 * netplay_platform_magic
 *
 * Just enough info to tell us if our platforms mismatch: Endianness and a
 * couple of type sizes.
 *
 * Format:
 *    bit 31:     Reserved
 *    bit 30:     1 for big endian
 *    bits 29-15: sizeof(size_t)
 *    bits 14-0:  sizeof(long)
 */
static uint32_t netplay_platform_magic(void)
{
   uint32_t ret =
       ((1 == htonl(1)) << 30)
      |(sizeof(size_t) << 15)
      |(sizeof(long));
   return ret;
}

/**
 * netplay_endian_mismatch
 *
 * Do the platform magics mismatch on endianness?
 */
static bool netplay_endian_mismatch(uint32_t pma, uint32_t pmb)
{
   uint32_t ebit = (1<<30);
   return (pma & ebit) != (pmb & ebit);
}

bool netplay_handshake_init_send(netplay_t *netplay)
{
   uint32_t *content_crc_ptr = NULL;
   uint32_t header[4] = {0};

   content_get_crc(&content_crc_ptr);

   header[0] = htonl(netplay_impl_magic());
   header[1] = htonl(*content_crc_ptr);
   header[2] = htonl(netplay_platform_magic());
   header[3] = htonl(NETPLAY_COMPRESSION_SUPPORTED);

   if (!netplay_send(&netplay->send_packet_buffer, netplay->fd, header,
         sizeof(header)) ||
       !netplay_send_flush(&netplay->send_packet_buffer, netplay->fd, false))
      return false;

   return true;
}

struct nick_buf_s
{
   uint32_t cmd[2];
   char nick[32];
};

#define RECV(buf, sz) \
   recvd = netplay_recv(&netplay->recv_packet_buffer, netplay->fd, (buf), (sz), false); \
   if (recvd >= 0 && recvd < (sz)) \
   { \
      netplay_recv_reset(&netplay->recv_packet_buffer); \
      return true; \
   } \
   else if (recvd < 0)

bool netplay_handshake_init(netplay_t *netplay, bool *had_input)
{
   uint32_t header[4] = {0};
   ssize_t recvd;
   char msg[512];
   struct nick_buf_s nick_buf;
   uint32_t *content_crc_ptr = NULL;
   uint32_t local_pmagic, remote_pmagic;
   uint32_t compression;

   msg[0] = '\0';

   RECV(header, sizeof(header))
   {
      strlcpy(msg, msg_hash_to_str(MSG_FAILED_TO_RECEIVE_HEADER_FROM_CLIENT), sizeof(msg));
      goto error;
   }

   if (netplay_impl_magic() != ntohl(header[0]))
   {
      strlcpy(msg, "Implementations differ. Make sure you're using exact same "
         "libretro implementations and RetroArch version.", sizeof(msg));
      goto error;
   }

   content_get_crc(&content_crc_ptr);
   if (*content_crc_ptr != ntohl(header[1]))
   {
      strlcpy(msg, msg_hash_to_str(MSG_CONTENT_CRC32S_DIFFER), sizeof(msg));
      goto error;
   }

   /* We only care about platform magic if our core is quirky */
   local_pmagic = netplay_platform_magic();
   remote_pmagic = ntohl(header[2]);
   if ((netplay->quirks & NETPLAY_QUIRK_ENDIAN_DEPENDENT) &&
       netplay_endian_mismatch(local_pmagic, remote_pmagic))
   {
      RARCH_ERR("Endianness mismatch with an endian-sensitive core.\n");
      strlcpy(msg, "This core does not support inter-architecture netplay "
         "between these systems.", sizeof(msg));
      goto error;
   }
   if ((netplay->quirks & NETPLAY_QUIRK_PLATFORM_DEPENDENT) &&
       (local_pmagic != remote_pmagic))
   {
      RARCH_ERR("Platform mismatch with a platform-sensitive core.\n");
      strlcpy(msg, "This core does not support inter-architecture netplay.",
         sizeof(msg));
      goto error;
   }

   /* Clear any existing compression */
   if (netplay->compression_stream)
      netplay->compression_backend->stream_free(netplay->compression_stream);
   if (netplay->decompression_stream)
      netplay->decompression_backend->stream_free(netplay->decompression_stream);

   /* Check what compression is supported */
   compression = ntohl(header[3]);
   compression &= NETPLAY_COMPRESSION_SUPPORTED;
   if (compression & NETPLAY_COMPRESSION_ZLIB)
   {
      netplay->compression_backend = trans_stream_get_zlib_deflate_backend();
      if (!netplay->compression_backend)
         netplay->compression_backend = trans_stream_get_pipe_backend();
   }
   else
   {
      netplay->compression_backend = trans_stream_get_pipe_backend();
   }
   netplay->decompression_backend = netplay->compression_backend->reverse;

   /* Allocate our compression stream */
   netplay->compression_stream = netplay->compression_backend->stream_new();
   netplay->decompression_stream = netplay->decompression_backend->stream_new();
   if (!netplay->compression_stream || !netplay->decompression_stream)
   {
      RARCH_ERR("Failed to allocate compression transcoder!\n");
      return false;
   }

   /* Send our nick */
   nick_buf.cmd[0] = htonl(NETPLAY_CMD_NICK);
   nick_buf.cmd[1] = htonl(sizeof(nick_buf.nick));
   memset(nick_buf.nick, 0, sizeof(nick_buf.nick));
   strlcpy(nick_buf.nick, netplay->nick, sizeof(nick_buf.nick));
   if (!netplay_send(&netplay->send_packet_buffer, netplay->fd, &nick_buf,
         sizeof(nick_buf)) ||
       !netplay_send_flush(&netplay->send_packet_buffer, netplay->fd, false))
      return false;

   /* Move on to the next mode */
   netplay->self_mode = NETPLAY_CONNECTION_PRE_NICK;
   *had_input = true;
   netplay_recv_flush(&netplay->recv_packet_buffer);
   return true;

error:
   if (msg[0])
   {
      RARCH_ERR("%s\n", msg);
      runloop_msg_queue_push(msg, 1, 180, false);
   }
   return false;
}

static void netplay_handshake_ready(netplay_t *netplay)
{
   size_t i;
   char msg[512];

   if (netplay->is_server)
   {
      netplay_log_connection(&netplay->other_addr, 0, netplay->other_nick);

      /* Send them the savestate */
      if (!(netplay->quirks & (NETPLAY_QUIRK_NO_SAVESTATES|NETPLAY_QUIRK_NO_TRANSMISSION)))
      {
         netplay->force_send_savestate = true;
      }
   }
   else
   {
      snprintf(msg, sizeof(msg), "%s: \"%s\"",
            msg_hash_to_str(MSG_CONNECTED_TO),
            netplay->other_nick);
      RARCH_LOG("%s\n", msg);
      runloop_msg_queue_push(msg, 1, 180, false);
   }

   /* Unstall if we were waiting for this */
   if (netplay->stall == RARCH_NETPLAY_STALL_NO_CONNECTION)
       netplay->stall = 0;

   netplay->remote_mode = NETPLAY_CONNECTION_PLAYING;
}

bool netplay_handshake_pre_nick(netplay_t *netplay, bool *had_input)
{
   struct nick_buf_s nick_buf;
   ssize_t recvd;
   char msg[512];

   msg[0] = '\0';

   RECV(&nick_buf, sizeof(nick_buf));

   /* Expecting only a nick command */
   if (recvd < 0 ||
       ntohl(nick_buf.cmd[0]) != NETPLAY_CMD_NICK ||
       ntohl(nick_buf.cmd[1]) != sizeof(nick_buf.nick))
   {
      if (netplay->is_server)
         strlcpy(msg, msg_hash_to_str(MSG_FAILED_TO_GET_NICKNAME_FROM_CLIENT),
            sizeof(msg));
      else
         strlcpy(msg,
            msg_hash_to_str(MSG_FAILED_TO_RECEIVE_NICKNAME_FROM_HOST),
            sizeof(msg));
      RARCH_ERR("%s\n", msg);
      runloop_msg_queue_push(msg, 1, 180, false);
      return false;
   }

   strlcpy(netplay->other_nick, nick_buf.nick,
      (sizeof(netplay->other_nick) < sizeof(nick_buf.nick)) ?
      sizeof(netplay->other_nick) : sizeof(nick_buf.nick));

   if (netplay->is_server)
   {
      /* If we're the server, now we send sync info */
      uint32_t cmd[4];
      retro_ctx_memory_info_t mem_info;

      mem_info.id = RETRO_MEMORY_SAVE_RAM;
      core_get_memory(&mem_info);

      cmd[0] = htonl(NETPLAY_CMD_SYNC);
      cmd[1] = htonl(sizeof(uint32_t) + mem_info.size);
      cmd[2] = htonl(netplay->self_frame_count);
      cmd[3] = htonl(1);

      if (!netplay_send(&netplay->send_packet_buffer, netplay->fd, cmd,
            sizeof(cmd)))
         return false;
      if (!netplay_send(&netplay->send_packet_buffer, netplay->fd,
            mem_info.data, mem_info.size) ||
          !netplay_send_flush(&netplay->send_packet_buffer, netplay->fd,
            false))
         return false;

      /* They start one frame after us */
      netplay->other_frame_count = netplay->read_frame_count =
         netplay->self_frame_count + 1;
      netplay->other_ptr = netplay->read_ptr =
         NEXT_PTR(netplay->self_ptr);

      /* Now we're ready! */
      netplay_handshake_ready(netplay);

   }
   else
   {
      /* Client needs to wait for sync info */
      netplay->self_mode = NETPLAY_CONNECTION_PRE_SYNC;

   }

   *had_input = true;
   netplay_recv_flush(&netplay->recv_packet_buffer);
   return true;
}

bool netplay_handshake_pre_sync(netplay_t *netplay, bool *had_input)
{
   uint32_t cmd[2];
   uint32_t local_sram_size, remote_sram_size;
   uint32_t new_frame_count, self_connection_num;
   size_t i;
   ssize_t recvd;
   retro_ctx_memory_info_t mem_info;

   RECV(cmd, sizeof(cmd))
      return false;

   /* Only expecting a sync command */
   if (ntohl(cmd[0]) != NETPLAY_CMD_SYNC ||
       ntohl(cmd[1]) < 2*sizeof(uint32_t))
   {
      RARCH_ERR("%s\n",
            msg_hash_to_str(MSG_FAILED_TO_RECEIVE_SRAM_DATA_FROM_HOST));
      return false;
   }

   /* Get the frame count */
   RECV(&new_frame_count, sizeof(new_frame_count))
      return false;
   new_frame_count = ntohl(new_frame_count);

   /* And the connection number */
   RECV(&self_connection_num, sizeof(self_connection_num))
      return false;
   netplay->self_connection_num = ntohl(self_connection_num);

   /* Reset our frame buffer so it's consistent between server and client */
   netplay->self_frame_count = netplay->other_frame_count =
      netplay->read_frame_count = new_frame_count;
   for (i = 0; i < netplay->buffer_size; i++)
   {
      struct delta_frame *ptr = &netplay->buffer[i];
      ptr->used = false;

      if (i == netplay->self_ptr)
      {
         /* Clear out any current data but still use this frame */
         netplay_delta_frame_ready(netplay, ptr, 0);
         ptr->frame = new_frame_count;
         ptr->have_local = true;
         netplay->other_ptr = netplay->read_ptr = i;

      }
   }

   /* Now check the SRAM */
   mem_info.id = RETRO_MEMORY_SAVE_RAM;
   core_get_memory(&mem_info);

   local_sram_size = mem_info.size;
   remote_sram_size = ntohl(cmd[1]) - 2*sizeof(uint32_t);

   if (local_sram_size != 0 && local_sram_size == remote_sram_size)
   {
      RECV(mem_info.data, local_sram_size)
      {
         RARCH_ERR("%s\n",
               msg_hash_to_str(MSG_FAILED_TO_RECEIVE_SRAM_DATA_FROM_HOST));
         return false;
      }

   }
   else if (remote_sram_size != 0)
   {
      /* We can't load this, but we still need to get rid of the data */
      uint32_t quickbuf;
      while (remote_sram_size > 0)
      {
         RECV(&quickbuf, (remote_sram_size > sizeof(uint32_t)) ? sizeof(uint32_t) : remote_sram_size)
         {
            RARCH_ERR("%s\n",
                  msg_hash_to_str(MSG_FAILED_TO_RECEIVE_SRAM_DATA_FROM_HOST));
            return false;
         }
         if (remote_sram_size > sizeof(uint32_t))
            remote_sram_size -= sizeof(uint32_t);
         else
            remote_sram_size = 0;
      }

   }

   /* We're ready! */
   netplay->self_mode = NETPLAY_CONNECTION_PLAYING;
   netplay_handshake_ready(netplay);
   *had_input = true;
   netplay_recv_flush(&netplay->recv_packet_buffer);
   return true;
}

bool netplay_is_server(netplay_t* netplay)
{
   if (!netplay)
      return false;
   return netplay->is_server;
}

bool netplay_is_spectate(netplay_t* netplay)
{
   if (!netplay)
      return false;
   return netplay->spectate.enabled;
}

bool netplay_delta_frame_ready(netplay_t *netplay, struct delta_frame *delta, uint32_t frame)
{
   void *remember_state;
   if (delta->used)
   {
      if (delta->frame == frame) return true;
      if (netplay->other_frame_count <= delta->frame)
      {
         /* We haven't even replayed this frame yet, so we can't overwrite it! */
         return false;
      }
   }
   remember_state = delta->state;
   memset(delta, 0, sizeof(struct delta_frame));
   delta->used = true;
   delta->frame = frame;
   delta->state = remember_state;
   return true;
}

uint32_t netplay_delta_frame_crc(netplay_t *netplay, struct delta_frame *delta)
{
   if (!netplay->state_size)
      return 0;
   return encoding_crc32(0L, (const unsigned char*)delta->state, netplay->state_size);
}