#include <EXTERN.h>
#include <perl.h>
#include <XSUB.h>

#include <base/system.h>

// Fails
//#include "e_engine.h"
#include "e_config.h"
#include "e_network.h"
#include "e_protocol.h"
#include "e_server_interface.h"

#include "e_demorec.h"

#include <game/server/perlhooks.h>

// FIXME: #include "e_engine.h" instead
const char* engine_savepath(const char* filename, char* buffer, int max);
void server_start_record(const char* filename);
void server_auth(int cid, int gid);
int client_gid(int cid);

int client_ingame(int client_id);
extern NETSERVER* net;

// defined in game/server/hooks.cpp
void addvote(const char* command);
void delvote(const char* command);

static PerlInterpreter* my_perl = NULL;

XS(get_int) {
 dXSARGS;
 if (items != 1) goto error;

 SV* self = POPs;
 SV** x = av_fetch((AV*)SvRV(self), 0, 0);
 if (!x) goto error;
 int* addr = (int*)SvIV(*x);
 mXPUSHi(*addr);

 XSRETURN(1);

error:
 croak_xs_usage(cv, "config::int");
};

XS(set_int) {
 dXSARGS;
 if (items != 2) goto error;

 int value = POPi;
 AV* self = (AV*)SvRV(POPs);

 SV** x = av_fetch(self, 2, 0);
 if (!x) goto error;
 int min = SvIV(*x);
 x = av_fetch(self, 3, 0);
 if (!x) goto error;
 int max = SvIV(*x);

 if (max > 0) {
  if (value < min || value > max)
   croak("config::int: expected value between %d and %d\n", min, max);
 } else if (value < min)
  croak("config::int: expected value greater than or equal to %d\n", min);

 x = av_fetch(self, 0, 0);
 if (!x) goto error;
 *(int*)SvIV(*x) = value;

 XSRETURN_EMPTY;

error:
 croak_xs_usage(cv, "config::int");
};
 
XS(help_int) {
 dXSARGS;
 if (items != 1) goto error;

 AV* self = (AV*)SvRV(POPs);
 SV** result = av_fetch(self, 1, 0);
 if (!result) goto error;
 XPUSHs(*result);

 XSRETURN(1);

error:
 croak_xs_usage(cv, "config::int");
};

XS(min_int) {
 dXSARGS;
 if (items != 1) goto error;

 AV* self = (AV*)SvRV(POPs);
 SV** result = av_fetch(self, 2, 0);
 if (!result) goto error;
 XPUSHs(*result);

 XSRETURN(1);

error:
 croak_xs_usage(cv, "config::int");
};

XS(max_int) {
 dXSARGS;
 if (items != 1) goto error;

 AV* self = (AV*)SvRV(POPs);
 SV** result = av_fetch(self, 3, 0);
 if (!result) goto error;
 XPUSHs(*result);

 XSRETURN(1);

error:
 croak_xs_usage(cv, "config::int");
};

XS(get_str) {
 dXSARGS;
 if (items != 1) goto error;

 SV* self = POPs;
 SV** x = av_fetch((AV*)SvRV(self), 0, 0);
 if (!x) goto error;
 char* result = (char*)SvUV(*x);
 mXPUSHp(result, strlen(result));
 
 XSRETURN(1);

error:
 croak_xs_usage(cv, "config::str");
};

XS(set_str) {
 dXSARGS;
 if (items != 2) goto error;

 char* value = POPpx;
 AV* self = (AV*)SvRV(POPs);
 SV** x = av_fetch(self, 0, 0);
 if (!x) goto error;
 char* s = (char*)SvUV(*x);
 x = av_fetch(self, 1, 0);
 if (!x) goto error;
 int len = SvUV(*x);

 strncpy(s, value, len);
 s[len-1] = 0;

 XSRETURN_EMPTY;

error:
 croak_xs_usage(cv, "config::str");
};

XS(help_str) {
 dXSARGS;
 if (items != 1) goto error;

 AV* self = (AV*)SvRV(POPs);
 SV** result = av_fetch(self, 2, 0);
 if (!result) goto error;
 XPUSHs(*result);

 XSRETURN(1);

error:
 croak_xs_usage(cv, "config::str");
};

XS(say_rcon) {
 dXSARGS;
 if (items != 2) croak_xs_usage(cv, "client_id, message");

 char* line = POPpx;
 int cid = POPi;

 msg_pack_start_system(NETMSG_RCON_LINE, MSGFLAG_VITAL);
 msg_pack_string(line, 512);
 msg_pack_end();
 server_send_msg(cid);

 XSRETURN_EMPTY;
};

XS(perl_server_clientname) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "client_id");

 int cid = POPi;
 const char* name = server_clientname(cid);
 mXPUSHp(name, strlen(name));

 XSRETURN(1);
};

XS(perl_server_client_team) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "client_id");

 int cid = POPi;
 int team = client_team(cid);
 mXPUSHi(team);

 XSRETURN(1);
};

XS(perl_client_ingame) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "client_id");

 int cid = POPi;
 mXPUSHi(client_ingame(cid));

 XSRETURN(1);
};

XS(perl_client_addr) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "client_id");

 NETADDR addr;
 int cid = POPi;
 netserver_client_addr(net, cid, &addr);

 if (addr.type == NETTYPE_IPV4) {
  mXPUSHi(*(I32*)addr.ip);
  XSRETURN(1);
 };

 croak("Unknown NETADDR type: %d\n", addr.type);
};

XS(perl_kick) {
 dXSARGS;
 if (items < 1 || items > 2) croak_xs_usage(cv, "client_id, reason?");

 if (items == 1)
  server_kick(POPi, "kicked by console");
 else {
  char* reason = POPpx;
  server_kick(POPi, reason);
 };
 
 XSRETURN_EMPTY;
};

XS(perl_ban_add) {
 dXSARGS;
 if (items != 2) croak_xs_usage(cv, "client_id, ban time");

 int time = POPi;
 int cid = POPi;

 if (cid < 0 || cid > MAX_CLIENTS)
  croak("ban_add: invalid client id: %d\n", cid);

 if (!client_ingame(cid)) XSRETURN_EMPTY;

 NETADDR addr;
 netserver_client_addr(net, cid, &addr);
 server_ban_add(addr, time);

 XSRETURN_EMPTY;
};

XS(perl_ban_add_ip) {
 dXSARGS;
 if (items != 2) croak_xs_usage(cv, "ip, ban time");

 int time = POPi;
 I32 ip = POPi;

 NETADDR addr;
 addr.type = NETTYPE_IPV4;
 memcpy(&addr.ip, &ip, sizeof(I32));
 addr.port = 0;

 server_ban_add(addr, time);

 XSRETURN_EMPTY;
};

XS(perl_ban_remove) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "ip");

 I32 ip = POPi;

 NETADDR addr;
 addr.type = NETTYPE_IPV4;
 memcpy(&addr.ip, &ip, sizeof(I32));
 addr.port = 0;

 server_ban_remove(addr);

 XSRETURN_EMPTY;
};

XS(perl_ban_num) {
 dXSARGS;
 if (items > 0) croak_xs_usage(cv, "");

 EXTEND(SP, 1);
 mPUSHi(netserver_ban_num(net));

 XSRETURN(1);
};

XS(perl_ban_get) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "index");

 int index = POPi;

 NETBANINFO info;
 netserver_ban_get(net, index, &info);

 mXPUSHi(*(I32*)info.addr.ip);
 mXPUSHi(info.expires);

 XSRETURN(2);
};

XS(perl_record) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "filename");

 char* filename = POPpx;
 server_start_record(filename);

 XSRETURN_EMPTY;
};

XS(perl_stop_record) {
 dXSARGS;
 if (items != 0) croak_xs_usage(cv, "");

 demorec_record_stop();

 XSRETURN_EMPTY;
};

XS(perl_server_auth) {
 dXSARGS;
 if (items != 2) croak_xs_usage(cv, "client_id, group_id");

 int gid = POPi;
 int cid = POPi;

 server_auth(cid, gid);

 XSRETURN_EMPTY;
};

XS(perl_client_gid) {
 dXSARGS;
 if (items != 1) croak_xs_usage(cv, "client_id");

 int cid = POPi;
 int gid = client_gid(cid);
 mXPUSHu(gid);

 XSRETURN(1);
};

XS(perl_start_vote) {
 dXSARGS;
 if (items != 4)
  croak_xs_usage(cv, "client_id, chat_message, description, command");

 CV* command        = (CV*)POPs;
 char* description  = POPpx;
 char* chat_message = POPpx;
 int   client_id    = POPi;
 SvREFCNT_inc_simple_void(command);

 start_vote(client_id, chat_message, description, command);

 XSRETURN_EMPTY;
};

XS(perl_vote_active) {
 dXSARGS;
 if (items > 0)
  croak_xs_usage(cv, "");

 int result = vote_active();

 mXPUSHi(result);

 XSRETURN(1);
};

XS(perl_vote_enforce) {
 dXSARGS;
 if (items != 1)
  croak_xs_usage(cv, "result");

 int result = POPi;

 vote_enforce(result);

 XSRETURN_EMPTY;
};

XS(perl_send_chat) {
 dXSARGS;
 if (items != 3)
  croak_xs_usage(cv, "chatter_cid, target, text");

 char* text   = POPpx;
 int   target = POPi;
 int   cid    = POPi;

 do_send_chat(cid, target, text);

 XSRETURN_EMPTY;
};

XS(perl_send_broadcast) {
 dXSARGS;
 if (items != 2)
  croak_xs_usage(cv, "cid, text");

 char* text = POPpx;
 int   cid  = POPi;

 send_broadcast(text, cid);

 XSRETURN_EMPTY;
};

XS(perl_addvote) {
 dXSARGS;
 if (items != 1)
  croak_xs_usage(cv, "command");

 char* vote = POPpx;

 addvote(vote);

 XSRETURN_EMPTY;
};

XS(perl_delvote) {
 dXSARGS;
 if (items != 1)
  croak_xs_usage(cv, "command");

 char* vote = POPpx;

 delvote(vote);

 XSRETURN_EMPTY;
};

XS(perl_restart) {
 dXSARGS;
 if (items > 1)
  croak_xs_usage(cv, "[warmup-time]");

 int warmup = items > 0 ? POPi : 0;

 restart(warmup);

 XSRETURN_EMPTY;
};

XS(perl_pause) {
 dXSARGS;
 if (items > 1)
  croak_xs_usage(cv, "[state]");

 int state = items > 0 ? POPi : -1;

 game_pause(state);

 XSRETURN_EMPTY;
};

XS(perl_endround) {
 dXSARGS;
 if (items > 0)
  croak_xs_usage(cv, "");

 endround();

 XSRETURN_EMPTY;
};

XS(perl_set_team) {
 dXSARGS;

 if (items != 2)
  croak_xs_usage(cv, "cid, team");

 int team = POPi;
 int cid  = POPi;

 set_team(cid, team);

 XSRETURN_EMPTY;
};

XS(perl_set_datadir) {
 dXSARGS;
 if (items != 1)
  croak_xs_usage(cv, "path");

 char* path = POPpx;
 engine_set_datadir(path);

 XSRETURN_EMPTY;
};

XS(perl_statistics) {
 dXSARGS;

 if (items != 1)
  croak_xs_usage(cv, "cid");

 int cid = POPi;

 int shots, hits, kills, deaths;
 client_statistics(cid, &shots, &hits, &kills, &deaths);

 mXPUSHi(shots);
 mXPUSHi(hits);
 mXPUSHi(kills);
 mXPUSHi(deaths);

 XSRETURN(4);
};

void boot_DynaLoader(pTHX_ CV* cv);

XS(perl_boot_teeworlds) {
 dXSARGS;

 HV* cfg = get_hv("config", GV_ADD);
 HV* int_package = gv_stashpv("config::int", GV_ADD);
 HV* str_package = gv_stashpv("config::str", GV_ADD);

#define MACRO_CONFIG_INT(name, def, min, max, flags, desc) \
 if ((flags) & CFGFLAG_SERVER) { \
  AV* av = newAV(); \
  av_push(av, newSViv((IV)&config.name)); \
  av_push(av, newSVpvn(desc, sizeof(desc)-1)); \
  av_push(av, newSViv(min)); \
  av_push(av, newSViv(max)); \
  (void)hv_store( \
    cfg, \
    #name, \
    sizeof(#name)-1, \
    sv_bless(newRV_noinc((SV*)av), int_package), \
    0 \
  ); \
 };

#define MACRO_CONFIG_STR(name, len, def, flags, desc) \
 if ((flags) & CFGFLAG_SERVER) { \
  AV* av = newAV(); \
  av_push(av, newSVuv((IV)config.name)); \
  av_push(av, newSVuv(len)); \
  av_push(av, newSVpvn(desc, sizeof(desc)-1)); \
  (void)hv_store( \
    cfg, \
    #name, \
    sizeof(#name)-1, \
    sv_bless(newRV_noinc((SV*)av), str_package), \
    0 \
  ); \
 };

#include "e_config_variables.h"

#undef MACRO_CONFIG_INT
#undef MACRO_CONFIG_STR

 SV* maxclients = get_sv("MAX_CLIENTS", GV_ADD);
 sv_setiv(maxclients, MAX_CLIENTS);

 newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, __FILE__);

 newXS("config::int::get",  get_int,                __FILE__);
 newXS("config::int::set",  set_int,                __FILE__);
 newXS("config::int::help", help_int,               __FILE__);
 newXS("config::int::min",  min_int,                __FILE__);
 newXS("config::int::max",  max_int,                __FILE__);
 newXS("config::str::get",  get_str,                __FILE__);
 newXS("config::str::set",  set_str,                __FILE__);
 newXS("config::str::help", help_str,               __FILE__);
 newXS("say_rcon",          say_rcon,               __FILE__);
 newXS("client_name",       perl_server_clientname, __FILE__);
 newXS("client_team",       perl_server_client_team,__FILE__);
 newXS("client_active",     perl_client_ingame,     __FILE__);
 newXS("client_addr",       perl_client_addr,       __FILE__);
 newXS("server_kick",       perl_kick,              __FILE__);
 newXS("server_ban",        perl_ban_add,           __FILE__);
 newXS("server_ban_ip",     perl_ban_add_ip,        __FILE__);
 newXS("server_unban",      perl_ban_remove,        __FILE__);
 newXS("server_ban_num",    perl_ban_num,           __FILE__);
 newXS("server_ban_get",    perl_ban_get,           __FILE__);
 newXS("record",            perl_record,            __FILE__);
 newXS("stop_record",       perl_stop_record,       __FILE__);
 newXS("server_auth",       perl_server_auth,       __FILE__);
 newXS("client_gid",        perl_client_gid,        __FILE__);
 newXS("server_start_vote", perl_start_vote,        __FILE__);
 newXS("vote_active",       perl_vote_active,       __FILE__);
 newXS("vote_enforce",      perl_vote_enforce,      __FILE__);
 newXS("send_chat",         perl_send_chat,         __FILE__);
 newXS("send_broadcast",    perl_send_broadcast,    __FILE__);
 newXS("addvote",           perl_addvote,           __FILE__);
 newXS("delvote",           perl_delvote,           __FILE__);
 newXS("restart",           perl_restart,           __FILE__);
 newXS("server_pause",      perl_pause,             __FILE__);
 newXS("endround",          perl_endround,          __FILE__);
 newXS("set_team",          perl_set_team,          __FILE__);
 newXS("client_statistics", perl_statistics,        __FILE__);
 newXS("set_datadir",       perl_set_datadir,       __FILE__);

 XSRETURN_EMPTY;
};

int perl_init(int argc, char **argv) {
 my_perl = perl_alloc();
 perl_construct(my_perl);
 PL_exit_flags |= PERL_EXIT_DESTRUCT_END;

 char** perl_argv = calloc(argc + 3, sizeof(char*));
 perl_argv[0] = argv[0];
 perl_argv[1] = "-I";
 perl_argv[2] = malloc(1024);
 engine_savepath("", perl_argv[2], 1024);
 perl_argv[3] = "-Mteeworlds";
 memcpy(perl_argv + 4, argv + 1, sizeof(char*) * (argc - 1));

 if (perl_parse(my_perl, perl_boot_teeworlds, argc + 3, perl_argv, NULL) != 0)
  return -1;
 return perl_run(my_perl);
};

void perl_execute_line(int cid, const char *cmd) {
 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHi(cid);
 mXPUSHp(cmd, strlen(cmd));

 PUTBACK;

 call_pv("execute_line", G_DISCARD | G_EVAL);
 
 FREETMPS;
 LEAVE;
}

void perl_auth(int cid, const char* password) {
 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHi(cid);
 mXPUSHp(password, strlen(password));

 PUTBACK;

 call_pv("auth", G_DISCARD | G_EVAL);

 FREETMPS;
 LEAVE;
};

int perl_cyclemap() {
 CV* sub = get_cv("on_event", 0);
 if (!sub) return 1;

 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHp("cyclemap", 8);

 PUTBACK;
 call_sv((SV*)sub, G_DISCARD | G_EVAL);

 FREETMPS;
 LEAVE;
};

void c_start_vote(int cid, const char* command) {
 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHi(cid);
 mXPUSHp(command, strlen(command));

 PUTBACK;

 call_pv("client_start_vote", G_DISCARD | G_EVAL);

 FREETMPS;
 LEAVE;
};

void perl_execute_cmd(CV* cmd, int keep) {
 dSP;
 PUSHMARK(SP);

 call_sv((SV*)cmd, G_DISCARD | G_NOARGS | G_EVAL);
 if (!keep) SvREFCNT_dec(cmd);
};

void perl_client_enter(int cid) {
 CV* sub = get_cv("on_event", 0);
 if (!sub) return;

 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHp("enter", 5);
 mXPUSHi(cid);

 PUTBACK;

 call_sv((SV*)sub, G_DISCARD | G_EVAL);

 FREETMPS;
 LEAVE;
};

void perl_client_disconnect(int cid) {
 CV* sub = get_cv("on_event", 0);
 if (!sub) return;

 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHp("leave", 5);
 mXPUSHi(cid);

 PUTBACK;

 call_sv((SV*)sub, G_DISCARD | G_EVAL);

 FREETMPS;
 LEAVE;
};

void perl_server_send_chat(int cid, int target, const char* text, int bind) {
 CV* sub = get_cv("server_send_chat", 0);
 if (sub) {
  dSP;
  ENTER;
  SAVETMPS;
  PUSHMARK(SP);

  mXPUSHi(cid);
  mXPUSHi(target);
  mXPUSHp(text, strlen(text));
  mXPUSHi(bind);

  PUTBACK;

  call_sv((SV*)sub, G_DISCARD | G_EVAL);

  FREETMPS;
  LEAVE;
 } else {
  do_send_chat(cid, target, text);
 };
};

void perl_server_set_team(int cid, int team) {
 CV* sub = get_cv("server_set_team", 0);
 if (sub) {
  dSP;
  ENTER;
  SAVETMPS;
  PUSHMARK(SP);

  mXPUSHi(cid);
  mXPUSHi(team);

  PUTBACK;

  call_sv((SV*)sub, G_DISCARD | G_EVAL);

  FREETMPS;
  LEAVE;
 } else {
  set_team(cid, team);
 };
};

void perl_server_start_round() {
 CV* sub = get_cv("on_event", 0);
 if (!sub) return;

 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHp("start round", 11);

 PUTBACK;

 call_sv((SV*)sub, G_DISCARD | G_EVAL);

 FREETMPS;
 LEAVE;
};

void perl_server_end_round() {
 CV* sub = get_cv("on_event", 0);
 if (!sub) return;

 dSP;
 ENTER;
 SAVETMPS;
 PUSHMARK(SP);

 mXPUSHp("end round", 9);

 PUTBACK;

 call_sv((SV*)sub, G_DISCARD | G_EVAL);

 FREETMPS;
 LEAVE;
};
