#include "servers/CNLoginServer.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "PlayerMovement.hpp"
#include "BuiltinCommands.hpp"
#include "Buddies.hpp"
#include "CustomCommands.hpp"
#include "Combat.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Transport.hpp"
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "TableData.hpp"
#include "Groups.hpp"
#include "servers/Monitor.hpp"
#include "Racing.hpp"
#include "Trading.hpp"
#include "Email.hpp"
#include "Vendors.hpp"
#include "Chat.hpp"
#include "Eggs.hpp"
#include "Rand.hpp"

#include "settings.hpp"
#include "sandbox/Sandbox.hpp"

#include "../version.h"

#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
    #include "mingw/mingw.thread.h"
#else
#include <thread>
#endif
#include <string>
#include <chrono>
#include <signal.h>

// HACK
#ifdef __has_feature
#if __has_feature(address_sanitizer)
#define __SANITIZE_ADDRESS__ 1
#endif
#endif

CNShardServer *shardServer = nullptr;
std::thread *shardThread = nullptr;

void startShard(CNShardServer* server) {
    sandbox_thread_start();
    server->start();
}

// terminate gracefully on SIGINT (for gprof & DB saving)
void terminate(int arg) {
    std::cout << "OpenFusion: terminating." << std::endl;

    if (shardServer != nullptr && shardThread != nullptr)
        shardServer->kill();

    Database::close();
    exit(0);
}

#ifdef _WIN32
static BOOL WINAPI winTerminate(DWORD arg) {
    terminate(0);
    return FALSE;
}
#endif

void initsignals() {
#ifdef _WIN32
    if (!SetConsoleCtrlHandler(winTerminate, TRUE)) {
        std::cerr << "[FATAL] Failed to set control handler" << std::endl;
        exit(1);
    }
#else
    struct sigaction act;

    memset((void*)&act, 0, sizeof(act));
    sigemptyset(&act.sa_mask);

    // tell the OS to not kill us if you use a broken pipe, just let us know thru recv() or send()
    act.sa_handler = SIG_IGN;
    if (sigaction(SIGPIPE, &act, NULL) < 0) {
        perror("sigaction");
        exit(1);
    }

    act.sa_handler = terminate;
    if (sigaction(SIGINT, &act, NULL) < 0) {
        perror("sigaction");
        exit(1);
    }
#endif
}

int main() {
    std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
    std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;

#ifdef _WIN32
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
        std::cerr << "OpenFusion: WSAStartup failed" << std::endl;
        exit(EXIT_FAILURE);
    }
#endif

    initsignals();
    settings::init();
    Database::init();
    Rand::init(getTime());
    TableData::init();

    std::cout << "[INFO] Intializing Packet Managers..." << std::endl;

    PlayerManager::init();
    PlayerMovement::init();
    BuiltinCommands::init();
    Buddies::init();
    CustomCommands::init();
    Combat::init();
    Chat::init();
    Items::init();
    Eggs::init();
    Missions::init();
    Nanos::init();
    NPCManager::init();
    Vendors::init();
    Transport::init();
    Buddies::init();
    Email::init();
    Groups::init();
    Racing::init();
    Trading::init();

    Database::open();

    switch (settings::EVENTMODE) {
    case 0: break; // no event
    case 1: std::cout << "[INFO] Event active. Hey, Hey It's Knishmas!" << std::endl; break;
    case 2: std::cout << "[INFO] Event active. Wishing you a spook-tacular Halloween!" << std::endl; break;
    case 3: std::cout << "[INFO] Event active. Have a very hoppy Easter!" << std::endl; break;
    default:
        std::cout << "[FATAL] Unknown event set in config file." << std::endl;
        exit(1);
        /* not reached */
    }

    sandbox_init();

    std::cout << "[INFO] Starting Server Threads..." << std::endl;
    CNLoginServer loginServer(settings::LOGINPORT);
    shardServer = new CNShardServer(settings::SHARDPORT);

    shardThread = new std::thread(startShard, (CNShardServer*)shardServer);

    sandbox_start();
    sandbox_thread_start();

    loginServer.start();

    shardServer->kill();
    shardThread->join();

#ifdef _WIN32
    WSACleanup();
#endif
    return 0;
}

// helper functions

std::string U16toU8(char16_t* src, size_t max) {
    src[max-1] = '\0'; // force a NULL terminator
    try {
        std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
        std::string ret = convert.to_bytes(src);

        if (ret.size() >= max)
            ret.resize(max-2);

        return ret;
    } catch(const std::exception& e) {
        return "";
    }
}

// returns number of char16_t that was written at des
size_t U8toU16(std::string src, char16_t* des, size_t max) {
    std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
    std::u16string tmp = convert.from_bytes(src);

    // copy utf16 string to buffer
    if (sizeof(char16_t) * tmp.length() > max) // make sure we don't write outside the buffer
        memcpy(des, tmp.c_str(), sizeof(char16_t) * max);
    else
        memcpy(des, tmp.c_str(), sizeof(char16_t) * tmp.length());
    des[tmp.length()] = '\0';

    return tmp.length();
}

time_t getTime() {
    using namespace std::chrono;

    milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(steady_clock::now())).time_since_epoch());

    return (time_t)value.count();
}

// returns system time in seconds
time_t getTimestamp() {
    using namespace std::chrono;

    seconds value = duration_cast<seconds>((time_point_cast<seconds>(system_clock::now())).time_since_epoch());

    return (time_t)value.count();
}

// timing safe strcmp implementation for e.g. cookie validation
int timingSafeStrcmp(const char* a, const char* b) {
    int diff = 0;
    while (*a && *b) {
        diff |= *a++ ^ *b++;
    }
    diff |= *a;
    diff |= *b;
    return diff;
}

// convert integer timestamp (in s) to FF systime struct
sSYSTEMTIME timeStampToStruct(uint64_t time) {

    const time_t timeProper = time;
    tm ts = *localtime(&timeProper);

    sSYSTEMTIME systime;
    systime.wMilliseconds = 0;
    systime.wSecond = ts.tm_sec;
    systime.wMinute = ts.tm_min;
    systime.wHour = ts.tm_hour;
    systime.wDay = ts.tm_mday;
    systime.wDayOfWeek = ts.tm_wday + 1;
    systime.wMonth = ts.tm_mon + 1;
    systime.wYear = ts.tm_year + 1900;

    return systime;
}
