#include <ProcessPlayer.h>
#include <MCP.h>
#include <SignalHandler.h>

#include <unistd.h>
#include <csignal>
#include <sys/wait.h>
#include <sys/time.h>
#include <stdexcept>
#include <iostream>
#include <string>
#include <cassert>   //assert
#include <algorithm> //find_if
#include <memory> //unique_ptr

std::list<ProcessPlayer*> ProcessPlayer::players;
ProcessPlayer* ProcessPlayer::currentPlayer = nullptr;

rlim_t ProcessPlayer::hardMem = RLIM_INFINITY;
rlim_t ProcessPlayer::softMem = RLIM_INFINITY;

using namespace std::chrono_literals;

ProcessPlayer::ProcessPlayer(std::string binary,
                             std::chrono::microseconds _softTime,
                             std::chrono::microseconds _hardTime) :
    softTime(_softTime), hardTime(_hardTime), state(TimeoutState::None),
    binName(binary), terminated(false)
{
    static SignalHandler child(SIGCHLD,&ProcessPlayer::sigChild,SA_NOCLDSTOP);

    index = players.size();
    players.push_back(this);
    int pipe_out[2], pipe_in[2];
    if (pipe(pipe_out) || pipe(pipe_in)) {
        terminate(ExitReason::INTERNAL_ERROR,"could not establish pipes");
    }
    pid = fork();
    if (pid == 0) { //child
        std::cout << "[MCP] Launching player " << players.size() << " ("
                  << binary << ") as PID " << getpid() << std::endl;
        close(pipe_out[READ]);
        close(pipe_in[WRITE]);
        dup2(pipe_in[READ],INPUT_FD);
        dup2(pipe_out[WRITE],OUTPUT_FD);
        //@TODO! MWi - at moment without Memory Limits (because off Sanitizer incompatibility
        //MWi perhaps without Sanitizer it could work --> @todo test 
        //@todo  Set limits here
        struct rlimit lim = { softMem, hardMem };
        setrlimit(RLIMIT_AS,&lim);
        execl(binary.c_str(), binary.c_str(), nullptr);
        // workaround for mixing address sanitizer with rlimits
        // exit the program as gracefully as possible
        execlp("true", "true", nullptr);
    } else { //parent
        stop();
        close(pipe_out[WRITE]);
        close(pipe_in[READ]);
        from_player = pipe_out[READ];
        to_player = pipe_in[WRITE];
    }
}

ProcessPlayer::~ProcessPlayer() {
    //TODO: Error handling
    if (!terminated) {
        std::cerr << "[MCP] Terminating player " << binName << std::endl;
        ::kill(pid,SIGCONT);
        ::kill(pid,SIGKILL);
        ::waitpid(pid, nullptr, 0);
    }
    //TODO: we should clean up the players, but this is racy in the dtors
    //Further removing may change player numbers. Move to map?
    //players.remove(this);
}

[[noreturn]] void ProcessPlayer::terminate(ExitReason reason, const std::string& msg) const {
    std::cerr << std::endl << "[MCP] Player " << index << " (" << binName << ") "
              << msg << "." << std::endl;
    ::exit(reason | (index << 4));
}

[[noreturn]] void ProcessPlayer::sigChild(siginfo_t* si) {
    for (auto p : ProcessPlayer::players) {
        if (p->pid == si->si_pid) {
            p->terminated = true;
            switch (si->si_code) {
                case CLD_EXITED:
                    p->terminate(ExitReason::EXIT,"left the game");
                case CLD_KILLED:
                    switch (p->state) {
                        case TimeoutState::None:
                            p->terminate(ExitReason::CRASH,"crashed");
                        case TimeoutState::Soft:
                            p->terminate(ExitReason::CRASH,"timed out without handling SIGXCPU");
                        case TimeoutState::Hard:
                            p->terminate(ExitReason::CRASH,"timed out hard");
                    }
                    p->terminate(ExitReason::INTERNAL_ERROR,"unknown kill reason");
                case CLD_DUMPED:
                    p->terminate(ExitReason::CRASH,"dumped a core");
                default:
                    p->terminate(ExitReason::INTERNAL_ERROR,"exited for an unknown reason");
            }
        }
    }
    throw std::runtime_error("Received signal for unknown child!");
}

void ProcessPlayer::resume()  {
    currentPlayer = this;
    if (softTime.count() != 0) {
        softTimeout = Timeout::programTimeout(softTime,[this] {
            kill(pid,SIGXCPU);
            state = TimeoutState::Soft;
        });
    }
    if (hardTime.count() != 0) {
        hardTimeout = Timeout::programTimeout(hardTime,[this] {
            kill(pid,SIGKILL);
            state = TimeoutState::Hard;
        });
    }

    if (kill(pid,SIGCONT) < 0)
        terminate(ExitReason::CRASH,"could not be resumed");
}

void ProcessPlayer::stop() {
    hardTimeout.reset();
    softTimeout.reset();
    if (kill(pid,SIGSTOP) < 0)
        terminate(ExitReason::CRASH,"could not be stopped");

    currentPlayer = nullptr;
}

Player& ProcessPlayer::operator<<(std::string state) {
    resume();
    unsigned pos = 0;
    while (pos < state.length()) {
        auto bytes_written = write(to_player,state.c_str()+pos,state.length()-pos);
        if (bytes_written < 0)
            terminate(ExitReason::INVALID_MOVE,"could not be written to");

        pos += bytes_written;
    }
    return *this;
}

const Player& ProcessPlayer::operator>>(std::string& move) {
    char buf[13] = {};
    move.clear();
    while (true) {
        auto bytes_read = read(from_player, buf, 12);
        if (bytes_read <= 0)
            terminate(ExitReason::INVALID_MOVE,"could not be read from");

        //Is it the end?
        if (buf[bytes_read-1] == '\n') {
            buf[bytes_read-1] = '\0';
            break;
        }
        buf[bytes_read] = '\0';
        move += buf;
    }
    move += buf;
    stop();
    return *this;
}

