/*****************************************************************************/
/* D2K: A Doom Source Port for the 21st Century                              */
/*                                                                           */
/* Copyright (C) 2014: See COPYRIGHT file                                    */
/*                                                                           */
/* This file is part of D2K.                                                 */
/*                                                                           */
/* D2K 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 */
/* Foundation, either version 2 of the License, or (at your option) any      */
/* later version.                                                            */
/*                                                                           */
/* D2K 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 D2K.  If not, see <http://www.gnu.org/licenses/>.                    */
/*                                                                           */
/*****************************************************************************/

#include "z_zone.h"

#ifdef USE_SDL_NET
#include <SDL.h>
#endif /* ifdef USE_SDL_NET */

#include "doomstat.h"
#include "d_net.h"

#include "d_main.h"
#include "g_game.h"
#include "m_menu.h"
#include "p_checksum.h"

#include "protocol.h"
#include "i_network.h"
#include "i_system.h"
#include "i_main.h"
#include "i_video.h"
#include "m_argv.h"
#include "r_fps.h"
#include "e6y.h"

static bool server;
static int remotetic;      // Tic expected from the remote
static int remotesend;     // Tic expected by the remote
ticcmd_t netcmds[MAXPLAYERS][BACKUPTICS];
static unsigned numqueuedpackets;
static packet_header_t **queuedpacket;
static int ticdup = 1;
static int xtratics = 0;
int wanted_player_number;

static bool isExtraDDisplay = false;

static void D_QuitNetGame(void);

#ifndef HAVE_NET
doomcom_t *doomcom;
#endif /* ifndef HAVE_NET */

#ifdef HAVE_NET
void N_InitNetGame(void) {
  int i;
  int numplayers = 1;

  i = M_CheckParm("-net");
  if (i && i < myargc - 1) {
    i++;
  }

  if (!(netgame = server =  !!i)) {
    playeringame[consoleplayer = 0] = true;

    // e6y
    // for play, recording or playback using "single-player coop" mode.
    // Equivalent to using prboom_server with -N 1
    netgame = M_CheckParm("-solo-net");
  }
  else {
    // Get game info from server
    packet_header_t *packet = Z_Malloc(1000, PU_STATIC, NULL);
    struct setup_packet_s *sinfo = (void *)(packet + 1);
    struct {
      packet_header_t head;
      short           pn;
    }

    PACKEDATTR initpacket;

    I_InitNetwork();
    udp_socket = I_Socket(0);
    I_ConnectToServer(myargv[i]);

    do {
      do {
        // Send init packet
        initpacket.pn = doom_htons(wanted_player_number);
        packet_set(&initpacket.head, PKT_INIT, 0);
        I_SendPacket(&initpacket.head, sizeof(initpacket));
        I_WaitForPacket(5000);
      } while (!I_GetPacket(packet, 1000));
      if (packet->type == PKT_DOWN) {
        I_Error("Server aborted the game");
      }
    } while (packet->type != PKT_SETUP);

    // Once we have been accepted by the server, we should tell it when we leave
    atexit(D_QuitNetGame);

    // Get info from the setup packet
    consoleplayer = sinfo->yourplayer;
    compatibility_level = sinfo->complevel;
    G_Compatibility();
    startskill = sinfo->skill;
    deathmatch = sinfo->deathmatch;
    startmap = sinfo->level;
    startepisode = sinfo->episode;
    ticdup = sinfo->ticdup;

    /*
     * CG: no such thing as ticdup anymore, too lazy to change everything
     *     properly
     */
    ticdup = 1;
    xtratics = sinfo->extratic;
    G_ReadOptions(sinfo->game_options);

    D_Msg(MSG_INFO, "\tjoined game as player %d/%d; %d WADs specified\n",
      consoleplayer + 1, numplayers = sinfo->players, sinfo->numwads
    );

    {
      char *p = sinfo->wadnames;
      int i = sinfo->numwads;

      while (i--) {
        W_AddResource(p, source_net);
        p += strlen(p) + 1;
      }
    }
    Z_Free(packet);
  }
  for (i = 0; i < numplayers; i++) {
    playeringame[i] = true;
    M_CBufInitWithCapacity(
      P_GetPlayerCommands(i) sizeof(netticcmd_t), BACKUPTICS
    );
  }
  for (; i < MAXPLAYERS; i++) {
    playeringame[i] = false;
  }

  if (!playeringame[consoleplayer]) {
    I_Error("N_InitNetGame: consoleplayer not in game");
  }
}

#else  /* ifdef HAVE_NET */
void N_InitNetGame(void) {
  int i;

  doomcom = Z_Malloc(sizeof *doomcom, PU_STATIC, NULL);
  doomcom->consoleplayer = 0;
  doomcom->numnodes = 0;
  doomcom->numplayers = 1;
  netgame = (M_CheckParm("-solo-net") != 0);

  for (i = 0; i < doomcom->numplayers; i++) {
    playeringame[i] = true;
    M_CBufInitWithCapacity(
      P_GetPlayerCommands(i) sizeof(netticcmd_t), BACKUPTICS
    );
  }

  for (; i < MAXPLAYERS; i++) {
    playeringame[i] = false;
  }

  consoleplayer = displayplayer = doomcom->consoleplayer;
}

#endif // HAVE_NET

#ifdef HAVE_NET
void D_CheckNetGame(void) {
  packet_header_t *packet = Z_Malloc(sizeof(packet_header_t) + 1,
    PU_STATIC,
    NULL);

  if (server) {
    D_Msg(MSG_INFO,
      "D_CheckNetGame: waiting for server to signal game start\n");
    do {
      while (!I_GetPacket(packet, sizeof(packet_header_t) + 1)) {
        packet_set(packet, PKT_GO, 0);
        *(unsigned char *)(packet + 1) = consoleplayer;
        I_SendPacket(packet, sizeof(packet_header_t) + 1);
        I_Sleep(100);
      }
    } while (packet->type != PKT_GO);
  }
  Z_Free(packet);
}

bool N_GetWad(const char *name) {
#if defined(HAVE_WAIT_H)
  size_t psize = sizeof(packet_header_t) + strlen(name) + 500;
  packet_header_t *packet;
  bool done = false;

  if (!server || strchr(name, '/')) {
    return false;                                 // If it contains path info,
                                                  // reject
  }
  do {
    // Send WAD request to remote
    packet = Z_Malloc(psize, PU_STATIC, NULL);
    packet_set(packet, PKT_WAD, 0);
    *(unsigned char *)(packet + 1) = consoleplayer;
    strcpy(1 + (unsigned char *)(packet + 1), name);
    I_SendPacket(packet, sizeof(packet_header_t) + strlen(name) + 2);

    I_Sleep(10);
  } while (!I_GetPacket(packet, psize) || (packet->type != PKT_WAD));
  Z_Free(packet);

  if (!strcasecmp((void *)(packet + 1), name)) {
    pid_t pid;
    int rv;
    unsigned char *p = (unsigned char *)(packet + 1) + strlen(name) + 1;

    /* Automatic wad file retrieval using wget (supports http and ftp, using
     * URLs)
     * Unix systems have all these commands handy, this kind of thing is easy
     * Any windo$e port will have some awkward work replacing these.
     */

    /* cph - caution here. This is data from an untrusted source.
     * Don't pass it via a shell. */
    if ((pid = fork()) == -1) {
      perror("fork");
    }
    else if (!pid) {
      /* Child chains to wget, does the download */
      execlp("wget", "wget", p, NULL);
    }

    /* This is the parent, i.e. main LxDoom process */
    wait(&rv);
    if (!(done = !access(name, R_OK))) {
      if (!strcmp(p + strlen(p) - 4, ".zip")) {
        p = strrchr(p, '/') + 1;
        if ((pid = fork()) == -1) {
          perror("fork");
        }
        else if (!pid) {
          /* Child executes decompressor */
          execlp("unzip", "unzip", p, name, NULL);
        }

        /* Parent waits for the file */
        wait(&rv);
        done = !!access(name, R_OK);
      }

      /* Add more decompression protocols here as desired */
    }
    Z_Free(buffer);
  }
  return done;
#else /* HAVE_WAIT_H */
  return false;
#endif /* if defined(HAVE_WAIT_H) */
}

void NetUpdate(void) {
  static int lastmadetic;

  if (isExtraDDisplay) {
    return;
  }
  if (server) { // Receive network packets
    size_t recvlen;
    packet_header_t *packet = Z_Malloc(10000, PU_STATIC, NULL);
    while ((recvlen = I_GetPacket(packet, 10000))) {
      switch (packet->type) {
        case PKT_TICS:
        {
          unsigned char *p = (void *)(packet + 1);
          int tics = *p++;
          unsigned long ptic = doom_ntohl(packet->tic);
          if (ptic > (unsigned)remotetic) { // Missed some
            packet_set(packet, PKT_RETRANS, remotetic);
            *(unsigned char *)(packet + 1) = consoleplayer;
            I_SendPacket(packet, sizeof(*packet) + 1);
          }
          else {
            if (ptic + tics <= (unsigned)remotetic) {
              break;     // Will not improve things
            }
            remotetic = ptic;
            while (tics--) {
              int players = *p++;
              while (players--) {
                int n = *p++;
                RawToTic(&netcmds[n][remotetic % BACKUPTICS], p);
                p += sizeof(ticcmd_t);
              }
              remotetic++;
            }
          }
        }
        break;
        case PKT_RETRANS:// Resend request
          remotesend = doom_ntohl(packet->tic);
          break;
        case PKT_DOWN:   // Server downed
        {
          int j;
          for (j = 0; j < MAXPLAYERS; j++) {
            if (j != consoleplayer) {
              playeringame[j] = false;
            }
          }
          server = false;
          doom_printf(
            "Server is down\nAll other players are no longer in the game\n");
        }
        break;
        case PKT_EXTRA:  // Misc stuff
        case PKT_QUIT:   // Player quit
          // Queue packet to be processed when its tic time is reached
          queuedpacket = Z_Realloc(queuedpacket,
          ++numqueuedpackets * sizeof *queuedpacket,
          PU_STATIC,
          NULL);
          queuedpacket[numqueuedpackets -
          1] = Z_Malloc(recvlen, PU_STATIC, NULL);
          memcpy(queuedpacket[numqueuedpackets - 1], packet, recvlen);
          break;
        case PKT_BACKOFF:

          /* cph 2003-09-18 -
           * The server sends this when we have got ahead of the other clients.
           *We should
           * stall the input side on this client, to allow other clients to
           *catch up.
           */
          lastmadetic++;
          break;
        default:         // Other packet, unrecognised or redundant
          break;
      }
    }
    Z_Free(packet);
  }
  {                      // Build new ticcmds
    int newtics = I_GetTime() - lastmadetic;

// e6y    newtics = (newtics > 0 ? newtics : 0);
    lastmadetic += newtics;
    if (ffmap) {
      newtics++;
    }
    while (newtics--) {
      I_StartTic();
      if (maketic - gametic > BACKUPTICS / 2) {
        break;
      }

      // e6y
      // Eliminating the sudden jump of six frames(BACKUPTICS/2)
      // after change of realtic_clock_rate.
      if (maketic - gametic && gametic <= force_singletics_to &&
        realtic_clock_rate < 200) {
        break;
      }

      G_BuildTiccmd(&netcmds[consoleplayer][maketic % BACKUPTICS]);
      maketic++;
    }
    if (server && maketic > remotesend) { // Send the tics to the server
      int sendtics;
      remotesend -= xtratics;
      if (remotesend < 0) {
        remotesend = 0;
      }
      sendtics = maketic - remotesend;
      {
        size_t pkt_size = sizeof(packet_header_t) + 2 + sendtics *
          sizeof(ticcmd_t);
        packet_header_t *packet = Z_Malloc(pkt_size, PU_STATIC, NULL);

        packet_set(packet, PKT_TICC, maketic - sendtics);
        *(unsigned char *)(packet + 1) = sendtics;
        *(((unsigned char *)(packet + 1)) + 1) = consoleplayer;
        {
          void *tic = ((unsigned char *)(packet + 1)) + 2;
          while (sendtics--) {
            TicToRaw(tic, &netcmds[consoleplayer][remotesend++ % BACKUPTICS]);
            tic = (unsigned char *)tic + sizeof(ticcmd_t);
          }
        }
        I_SendPacket(packet, pkt_size);
        Z_Free(packet);
      }
    }
  }
}

#else  /* ifdef HAVE_NET */

void D_BuildNewTiccmds(void) {
  static int lastmadetic;
  int newtics = I_GetTime() - lastmadetic;

  lastmadetic += newtics;
  while (newtics--) {
    I_StartTic();
    if (maketic - gametic > BACKUPTICS / 2) {
      break;
    }
    G_BuildTiccmd(&localcmds[maketic % BACKUPTICS]);
    maketic++;
  }
}

#endif /* ifdef HAVE_NET */

#ifdef HAVE_NET

/* cph - data passed to this must be in the Doom (little-) endian */
void D_NetSendMisc(netmisctype_t type, size_t len, void *data) {
  if (server) {
    size_t size = sizeof(packet_header_t) + 3 * sizeof(int) + len;
    packet_header_t *packet = Z_Malloc(size, PU_STATIC, NULL);
    int *p = (void *)(packet + 1);

    packet_set(packet, PKT_EXTRA, gametic);
    *p++ = LittleLong(type);
    *p++ = LittleLong(consoleplayer);
    *p++ = LittleLong(len);
    memcpy(p, data, len);
    I_SendPacket(packet, size);

    Z_Free(packet);
  }
}

static void CheckQueuedPackets(void) {
  int i;

  for (i = 0; (unsigned)i < numqueuedpackets; i++) {
    if (doom_ntohl(queuedpacket[i]->tic) <= gametic) {
      switch (queuedpacket[i]->type) {
        case PKT_QUIT: // Player quit the game
        {
          int pn = *(unsigned char *)(queuedpacket[i] + 1);
          playeringame[pn] = false;
          doom_printf("Player %d left the game\n", pn);
        }
        break;
        case PKT_EXTRA:
        {
          int *p = (int *)(queuedpacket[i] + 1);
          size_t len = LittleLong(*(p + 2));
          switch (LittleLong(*p)) {
            case nm_plcolour:
              G_ChangedPlayerColour(LittleLong(*(p + 1)), LittleLong(*(p + 3)));
              break;
            case nm_savegamename:
              if (len < SAVEDESCLEN) {
                memcpy(savedescription, p + 3, len);

                // Force terminating 0 in case
                savedescription[len] = 0;
              }
              break;
          }
        }
        break;
        default:// Should not be queued
          break;
      }
    }
  }

  {             // Requeue remaining packets
    int newnum = 0;
    packet_header_t **newqueue = NULL;

    for (i = 0; (unsigned)i < numqueuedpackets; i++) {
      if (doom_ntohl(queuedpacket[i]->tic) > gametic) {
        newqueue = Z_Realloc(newqueue, ++newnum * sizeof *newqueue,
          PU_STATIC, NULL);
        newqueue[newnum - 1] = queuedpacket[i];
      }
      else {
        Z_Free(queuedpacket[i]);
      }
    }

    Z_Free(queuedpacket);
    numqueuedpackets = newnum;
    queuedpacket = newqueue;
  }
}

#endif // HAVE_NET

void N_TryRunTics(void) {
  int runtics;
  int entertime = I_GetTime();

  // Wait for tics to run
  while (1) {
#ifdef HAVE_NET
    NetUpdate();
#else  /* ifdef HAVE_NET */
    D_BuildNewTiccmds();
#endif /* ifdef HAVE_NET */
    runtics = (server ? remotetic : maketic) - gametic;
    if (!runtics) {
      if (!movement_smooth || !window_focused) {
#ifdef HAVE_NET
        if (server) {
          I_WaitForPacket(ms_to_next_tick);
        }
        else
#endif /* ifdef HAVE_NET */
        I_Sleep(ms_to_next_tick);
      }
      if (I_GetTime() - entertime > 10) {
#ifdef HAVE_NET
        if (server) {
          char buf[sizeof(packet_header_t) + 1];
          remotesend--;
          packet_set((packet_header_t *)buf, PKT_RETRANS, remotetic);
          buf[sizeof(buf) - 1] = consoleplayer;
          I_SendPacket((packet_header_t *)buf, sizeof buf);
        }
#endif /* ifdef HAVE_NET */
        M_Ticker();
        return;
      }

      // if ((displaytime) < (tic_vars.next-SDL_GetTicks()))
      if (gametic > 0) {
        WasRenderedInTryRunTics = true;
        if (movement_smooth && gamestate == wipegamestate) {
          isExtraDDisplay = true;
          D_Display();
          isExtraDDisplay = false;
        }
      }
    }
    else {
      break;
    }
  }

  while (runtics--) {
#ifdef HAVE_NET
    if (server) {
      CheckQueuedPackets();
    }
#endif /* ifdef HAVE_NET */
    if (advancedemo) {
      D_DoAdvanceDemo();
    }
    M_Ticker();
    I_GetTime_SaveMS();
    G_Ticker();
    P_Checksum(gametic);
    gametic++;
#ifdef HAVE_NET
    NetUpdate(); // Keep sending our tics to avoid stalling remote nodes
#endif /* ifdef HAVE_NET */
  }
}

#ifdef HAVE_NET
static void D_QuitNetGame(void) {
  unsigned char buf[1 + sizeof(packet_header_t)];
  packet_header_t *packet = (void *)buf;
  int i;

  if (!server) {
    return;
  }
  buf[sizeof(packet_header_t)] = consoleplayer;
  packet_set(packet, PKT_QUIT, gametic);

  for (i = 0; i < 4; i++) {
    I_SendPacket(packet, 1 + sizeof(packet_header_t));
    I_Sleep(10);
  }
}

#endif /* ifdef HAVE_NET */

/* vi: set et ts=2 sw=2: */
