/*
 *   ORXONOX - the hottest 3D action shooter ever to exist
 *                    > www.orxonox.net <
 *
 *
 *   License notice:
 *
 *   This program 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.
 *
 *   This program 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 this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 *   Author:
 *      Sandro 'smerkli' Merkli
 *   Co-authors:
 *      ...
 *
 */

#include "MasterServer.h"
#include "core/command/ConsoleCommandIncludes.h"
#include "core/CoreIncludes.h"
#include "core/CorePrereqs.h"
#include "core/singleton/ScopedSingletonIncludes.h"
#include "util/Output.h"

namespace orxonox
{
  /*** MACROS ***/
  /* commands for the terminal interface */
  SetConsoleCommand( "ms-listservers", &MasterServer::listServers );
  SetConsoleCommand( "ms-delserver", &MasterServer::delServer );
  //SetConsoleCommand( "ms-serverinfo", &MasterServer::serverInfo );

  /* forward declaration so the linker doesn't complain */
  MasterServer *MasterServer::instance = nullptr;

  /* command: list servers */
  void
  MasterServer::listServers( void )
  {
    /* print list header */
    orxout(user_info) << "List of connected servers" << std::endl;

    /* loop through list elements */
    for( const ServerListElem& elem : MasterServer::getInstance()->mainlist.serverlist )
    {
      orxout(user_info) << "  " << elem.ServerInfo.getServerIP() << std::endl;
    }

    /* display end of list */
    orxout(user_info) << MasterServer::getInstance()->mainlist.serverlist.size() <<
      " servers connected." << std::endl;
  }

  void
  MasterServer::delServer( std::string todeladdr )
  {
    /* tell the user we're now removing the entry from the server list */
    orxout(user_info) << "MS: Deleting server \"" << todeladdr << "\"..."
      << std::endl;

    /* see if we actually have that server on our list */
    ServerListSearchResult shandle =
      MasterServer::getInstance()->mainlist.findServerByAddress(todeladdr);

    if( !shandle.success )
    { orxout(user_info) << "MS: Server not found, not removing." << std::endl;
      return;
    }

    /* force-disconnect the server */
    enet_peer_disconnect( shandle.result.peer, 0 );

    /* actually remove the entry from the server list by address */
    MasterServer::getInstance()->mainlist.delServerByAddress( todeladdr);

    /* tell the user about our success */
    orxout(user_info) << "MS: Server deletion successful." << std::endl;
  }


  /* helpers */
  static void
  helper_output_debug( ENetEvent *event, char *addrconv )
  {
    orxout(verbose, context::master_server)
      << "A packet of length"
      << event->packet->dataLength
      << " containing "
      << (const char*)event->packet->data
      << " was received from "
      << addrconv
      << " on channel "
      << event->channelID << endl;
  }

  void
  MasterServer::helper_sendlist( ENetEvent *event )
  {
    /* packet holder */
    ENetPacket *reply;

    /* loop through list elements */
    for( const ServerListElem& elem : mainlist.serverlist )
    {
      /* send this particular server */
      /* build reply string */
      int packetlen = MSPROTO_SERVERLIST_ITEM_LEN + 1 + elem.ServerInfo.getServerIP().length() + 1 + elem.ServerInfo.getServerName().length() + 1 + sizeof(elem.ServerInfo.getClientNumber()) + 1;
      char *tosend = (char *)calloc(packetlen ,1 );
      if( !tosend )
      { orxout(internal_warning, context::master_server) << "Masterserver.cc: Memory allocation failed." << endl;
        continue;
      }
      sprintf( tosend, "%s %s %s %u", MSPROTO_SERVERLIST_ITEM,
          elem.ServerInfo.getServerIP().c_str(), elem.ServerInfo.getServerName().c_str(), elem.ServerInfo.getClientNumber());

      /* create packet from it */
      reply = enet_packet_create( tosend,
          strlen( tosend ) + 1,
          ENET_PACKET_FLAG_RELIABLE);

      /* Send the reply to the peer over channel id 0. */
      enet_peer_send( event->peer, 0, reply );

      /* One could just use enet_host_service() instead. */
      enet_host_flush( this->server );

      /* free the tosend buffer */
      free( tosend );
    }

    /* create end-of-list packet */
    reply = enet_packet_create( MSPROTO_SERVERLIST_END,
        MSPROTO_SERVERLIST_END_LEN + 1,
        ENET_PACKET_FLAG_RELIABLE );

    /* send end-of-list packet */
    enet_peer_send( event->peer, 0, reply );

    /* One could just use enet_host_service() instead. */
    enet_host_flush( this->server );
  }

  /* maybe the two methods below can be merged into one and
   * made to use ENet's RTT functionality to check for disconnected
   * servers.
   */
  void
  MasterServer::helper_cleanupServers( void )
  {
    if( mainlist.serverlist.size() == 0 )
      return;

    /* loop through list elements */
    for( const ServerListElem& elem : mainlist.serverlist )
    { /* see if we have a disconnected peer */
      if( elem.peer &&
         (elem.peer->state == ENET_PEER_STATE_DISCONNECTED ||
          elem.peer->state == ENET_PEER_STATE_ZOMBIE ))
      {
        /* Remove it from the list */
        orxout(internal_warning) << (char*)elem.peer->data << " timed out.\n";
        mainlist.delServerByName( elem.ServerInfo.getServerName() );

        /* stop iterating, we manipulated the list */
        /* TODO note: this only removes one dead server per loop
         * iteration. not beautiful, but one iteration is ~100ms,
         * so not really relevant for the moment.
         */
        break;
      }
    }

  }




  /***** EVENTS *****/
  /* connect event */
  int
  MasterServer::eventConnect( ENetEvent *event )
  { /* check for bad parameters */
    if( !event )
    { orxout(internal_warning, context::master_server) << "MasterServer::eventConnect: No event given." << endl;
      return -1;
    }

    /* convert address to string. */
    char *addrconv = (char *) calloc( 50, 1 );
    enet_address_get_host_ip( &(event->peer->address), addrconv, 49 );

    /* output debug info */
    orxout(verbose, context::master_server) << "A new client connected from "
      << addrconv
      << " on port "
      << event->peer->address.port << endl;

    /* store string form of address here */
    event->peer->data = addrconv;

    /* all fine. */
    return 0;
  }

  /* disconnect event */
  int
  MasterServer::eventDisconnect( ENetEvent *event )
  { /* check for bad parameters */
    if( !event )
    { orxout(internal_warning, context::master_server) << "No event given." << endl;
      return -1;
    }

    /* output that the disconnect happened */
    orxout(verbose, context::master_server) << (char*)event->peer->data << " disconnected." << endl;

    /* create string from peer data */
    std::string name = std::string( (char*)event->peer->data );

    /* remove the server from the list it belongs to */
    this->mainlist.delServerByName( name );

    /* Reset the peer's client information. */
    if( event->peer->data ) free( event->peer->data );

    /* done */
    return 0;
  }

  /* data event */
  int
  MasterServer::eventData( ENetEvent *event )
  { /* validate packet */
    if( !event || !(event->packet) || !(event->peer) )
    { orxout(internal_warning, context::master_server) << "No complete event given." << endl;
      return -1;
    }

    /* generate address in readable form */
    char *addrconv = (char *) calloc( 50, 1 );
    enet_address_get_host_ip( &(event->peer->address), addrconv, 49 );
    /* convert to string */
    std::string ip = std::string( addrconv );
    /* output debug info about the data that has come */
    helper_output_debug(event, addrconv);
    /* delete addrconv */
    if( addrconv ) free( addrconv );

    /* pointer to full packet data */
    char * packetdata = (char *)event->packet->data;


    /* GAME SERVER OR CLIENT CONNECTION? */
    if( !strncmp(packetdata, MSPROTO_GAME_SERVER, MSPROTO_GAME_SERVER_LEN ) )
    { /* Game server */

      if( !strncmp( packetdata + MSPROTO_GAME_SERVER_LEN+1, MSPROTO_REGISTER_SERVER, MSPROTO_REGISTER_SERVER_LEN ) )
      { /* register new server */
        mainlist.addServer( packet::ServerInformation( event ), event->peer );

        /* tell people we did so */
        orxout(internal_info, context::master_server) << "Added new server to list: " <<
          packet::ServerInformation( event ).getServerIP() << endl;
      }

      else if( !strncmp( packetdata + MSPROTO_GAME_SERVER_LEN+1, MSPROTO_SERVERDC, MSPROTO_SERVERDC_LEN ) )
      { /* disconnect server */

        /* remove the server from the list it belongs to */
        this->mainlist.delServerByAddress( ip );

        /* tell the user */
        orxout(internal_info, context::master_server) << "Removed server " << ip << " from list." << endl;
      }
      /* TODO add hook for disconnect here */

      else if( !strncmp( packetdata + MSPROTO_GAME_SERVER_LEN+1, MSPROTO_SET_NAME, MSPROTO_SET_NAME_LEN ) )
      { /* save server name */
        /* create string from peer data */
        std::string data (event->packet->data,event->packet->data + event->packet->dataLength );
        std::string name = data.substr(MSPROTO_GAME_SERVER_LEN+1 + MSPROTO_SET_NAME_LEN + 1);

        /* remove the server from the list it belongs to */
        this->mainlist.setNameByAddress( ip, name );

        /* tell the user */
        orxout(internal_info, context::master_server) << "Updated server " << ip << " with new name " << name << endl;
      }

      else if( !strncmp( packetdata + MSPROTO_GAME_SERVER_LEN+1, MSPROTO_SET_CLIENTS, MSPROTO_SET_CLIENTS_LEN ) )
      { /* save client count from server */
        /* create string from peer data */
        std::string data (event->packet->data,event->packet->data + event->packet->dataLength );
        std::string textform= data.substr(MSPROTO_GAME_SERVER_LEN + 1 + MSPROTO_SET_CLIENTS_LEN + 1);
        int clientNumber = Ogre::StringConverter::parseInt(textform);

        this->mainlist.setClientsByAddress( ip, clientNumber);

        /* tell the user */
        orxout(internal_info, context::master_server) << "Updated server " << ip << " with new client number " << clientNumber << endl;
      }
    }
    else if( !strncmp( packetdata, MSPROTO_CLIENT, MSPROTO_CLIENT_LEN) )
    { /* client */
      if( !strncmp( packetdata + MSPROTO_CLIENT_LEN+1, MSPROTO_REQ_LIST, MSPROTO_REQ_LIST_LEN ) )
        /* send server list */
        helper_sendlist( event );
    }
    else
    { /* bad message, don't do anything. */ }

    /* Clean up the packet now that we're done using it. */
    enet_packet_destroy( event->packet );
    return 0;
  }


  /**** MAIN ROUTINE *****/
  int
  MasterServer::run()
  {
    /***** ENTER MAIN LOOP *****/
    ENetEvent *event = (ENetEvent *)calloc(1, sizeof(ENetEvent));
    if( event == nullptr )
    {
      orxout(user_error, context::master_server) << "Could not create ENetEvent structure, exiting." << endl;
      exit( EXIT_FAILURE );
    }

    /* check for timed out peers and remove those from * the server list */
    helper_cleanupServers();


    /* create an iterator for the loop */
    enet_host_service( this->server, event, 100 );

    /* check what type of event it is and react accordingly */
    switch (event->type)
    { /* new connection */
      case ENET_EVENT_TYPE_CONNECT:
        eventConnect( event ); break;

        /* disconnect */
      case ENET_EVENT_TYPE_DISCONNECT:
        eventDisconnect( event ); break;

        /* incoming data */
      case ENET_EVENT_TYPE_RECEIVE: eventData( event ); break;
      default: break;
    }

    /* done */
    free(event);
    return 0;
  }

  /* constructor */
  MasterServer::MasterServer()
  {
    /***** INITIALIZE NETWORKING *****/
    if( enet_initialize () != 0)
    { orxout(user_error, context::master_server) << "An error occurred while initializing ENet." << endl;
      exit( EXIT_FAILURE );
    }

    /* register deinitialization */
    atexit( enet_deinitialize );

    /* set the quit flag to false */
    this->quit = false;

    /* Bind the server to the default localhost and port ORX_MSERVER_PORT */
    this->address.host = ENET_HOST_ANY;
    this->address.port = ORX_MSERVER_PORT;

    /* create a host with the above settings (the last two 0 mean: accept
     * any input/output bandwidth */
    this->server = enet_host_create( &this->address, ORX_MSERVER_MAXCONNS,
        ORX_MSERVER_MAXCHANS, 0, 0 );
    assert(this->server);

    /* see if creation worked */
    if( !this->server )
    { orxout(user_error, context::master_server) <<
        "An error occurred while trying to create an ENet server host." << endl;
      exit( EXIT_FAILURE );
    }

    /* set pointer to this instance */
    MasterServer::setInstance( this );

    /* tell people we're now initialized */
    orxout(internal_status, context::master_server) << "MasterServer initialized, waiting for connections." << endl;
  }

  /* destructor */
  MasterServer::~MasterServer()
  {
    /***** CLEANUP PROCESS *****/
    /* terminate all networking connections */
    enet_host_destroy( this->server );

    /* free all used memory */
    /* clear the list of connected game servers */
    /* clear the list of connected game clients */
  }

/* end of namespace */
}
