#include <algorithm>
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>

#include <cstring>

#include <dlfcn.h>
#include <getopt.h>

#include <Game.h>
#include <MCP.h>
#include <ProcessPlayer.h>
#include <Replay.h>
#include <Replayer.h>
#include <SignalHandler.h>

using std::chrono::duration_cast;

[[noreturn]] static void usage() {
    std::cerr << "Usage:\n";
    std::cerr << "\n";
    std::cerr << "   ./mcp [-i <game state>] [-R <file>] [-r <file>]\n";
    std::cerr << "         [-M <megabyte>] [-m <megabyte>] [-T <seconds>]\n";
    std::cerr << "         [-t <seconds>] [-g <file>] <player1> ...\n";
    std::cerr << "\n";
    std::cerr << "Where:\n";
    std::cerr << "\n";
    std::cerr << "   -i <game state>,  --initial-state <game state>\n";
    std::cerr << "     initial game state to start with\n";
    std::cerr << "\n";
    std::cerr << "   -R <file>,  --replay-file <file>\n";
    std::cerr << "     replays a previously recorded game\n";
    std::cerr << "\n";
    std::cerr << "   -r <file>,  --record-file <file>\n";
    std::cerr << "     Record the current game to the file\n";
    std::cerr << "\n";
    std::cerr << "   -M <megabyte>,  --hard-player-mem <megabyte>\n";
    std::cerr << "     Maximum memory limit per player [int]\n";
    std::cerr << "\n";
    std::cerr << "   -m <megabyte>,  --soft-player-mem <megabyte>\n";
    std::cerr << "     Current memory limit per player [int]\n";
    std::cerr << "\n";
    std::cerr << "   -T <seconds>,  --hard-player-time <seconds>\n";
    std::cerr << "     Wallclock time per turn until SIGKILL is sent. [double]\n";
    std::cerr << "\n";
    std::cerr << "   -t <seconds>,  --soft-player-time <seconds>\n";
    std::cerr << "     Wallclock time per turn until SIGXCPU is sent. [double]\n";
    std::cerr << "\n";
    std::cerr << "   -n <playerNumber>,  --no-time-limit <playerNumber>\n";
    std::cerr << "     Don't enforce a time limit on <playerNumber> [0 .. n-1]\n";
    std::cerr << "\n";
    std::cerr << "   -g <file>,  --game-lib <file>\n";
    std::cerr << "     Library to load for game logic (./game.so by default)\n";
    std::cerr << "\n";
    std::cerr << "   -I <playerNumber>,  --retry-moves <playerNumber>\n";
    std::cerr << "     Don't exit on invalid move from player <playerNumber>, retry.\n";
    std::cerr << "\n";
    std::cerr << "   -h,  --help\n";
    std::cerr << "     Displays usage information and exits.\n";
    std::cerr << "\n";
    std::cerr << "   <players>  (accepted multiple times)\n";
    std::cerr << "     (required)  The players for the game\n";
    exit(1);
}

static std::unique_ptr<Game> mcpgame;
static std::vector<std::unique_ptr<Player>> playerList;

[[noreturn]] static void mcpExit(ExitReason reason, int player, const Game &game,
                                 std::string message) {
    std::cerr << "[MCP] ";
    switch (reason) {
        case ExitReason::WIN:
            std::cerr << "Player " << game.playerName(player) << " wins" << std::endl;
            break;
        case ExitReason::DRAW:
            std::cerr << "The game ended in a draw" << std::endl;
            break;
        case ExitReason::INVALID_MOVE:
            std::cerr << "Player " << game.playerName(player) << " returned an "
                      << "invalid move and was terminated"
                      << std::endl;
            break;
        case ExitReason::INTERNAL_ERROR:
            std::cerr << "An internal error occured! Please send a board state "
                      << "which reproduces this error to the game maintainer!"
                      << std::endl;
            break;
        case ExitReason::CRASH:
            std::cerr << "Player " << game.playerName(player) << " crashed" << std::endl;
            break;
        case ExitReason::EXIT:
            std::cerr << "Thats it folks!" << std::endl;
            break;
        case ExitReason::EXEC_FAILED:
            std::cerr << "Exec() failed :/" << std::endl;
            break;
        case ExitReason::RESOURCES_EXCEEDED:
            std::cerr << "Resources exceeded!" << std::endl;
            break;
        default:
            std::cerr << "Invalid status code. This is is an internal error!"
                      << std::endl;
    }
    if (!message.empty())
        std::cerr << "[MCP] " << message << std::endl;
    if (reason == ExitReason::INVALID_MOVE) {
        std::cerr << std::endl;
        game.expectedFormat();
    }
    ::exit(reason | (player << 4));
}



int main(int argc,char* argv[]) try {

    bool recording = false;
    bool replaying = false;
    std::ofstream recordFile;
    std::ifstream replayFile;
	//Parse commandline
	std::stringstream ss;

    std::string gameLib = "./game.so";
    std::string record;
    std::string replay;
    std::string initial;
    std::chrono::microseconds softTime{0}, hardTime{0};
    std::vector<int> noTimeLimit;
    std::vector<int> retryMove;

    int opt;
    const struct option longopts[] = {
        {"help",            no_argument,        nullptr,  'h'},
        {"game-lib",        required_argument,  nullptr,  'g'},
        {"soft-limit-mem",  required_argument,  nullptr,  't'},
        {"hard-limit-mem",  required_argument,  nullptr,  'T'},
        {"no-time-limit",   required_argument,  nullptr,  'n'},
        {"soft-limit-time", required_argument,  nullptr,  'm'},
        {"hard-limit-time", required_argument,  nullptr,  'M'},
        {"record-file",     required_argument,  nullptr,  'r'},
        {"replay-file",     required_argument,  nullptr,  'R'},
        {"initial-state",   required_argument,  nullptr,  'i'},
        {"retry-moves",     required_argument,  nullptr,  'I'},
        {nullptr, 0, nullptr, 0},
    };
    while((opt = getopt_long(argc, argv, "hg:t:T:n:m:M:r:R:i:I:", longopts, nullptr)) != -1) {
        switch(opt) {
            case 'g': gameLib = optarg; break;
            case 't':
              {
                auto t = std::chrono::duration<double>(strtod(optarg, nullptr));
                softTime = duration_cast<std::chrono::milliseconds>(t);
                break;
              }
            case 'T':
              {
                auto t = std::chrono::duration<double>(strtod(optarg, nullptr));
                hardTime = duration_cast<std::chrono::milliseconds>(t);
                break;
              }
            case 'm':
                ProcessPlayer::softMem = strtol(optarg, nullptr, 0) << 20; //MB
                break;
            case 'M':
                ProcessPlayer::hardMem = strtol(optarg, nullptr, 0) << 20;
                break;
            case 'n':
                noTimeLimit.push_back(strtol(optarg, nullptr, 0));
                break;
            case 'I':
                retryMove.push_back(strtol(optarg, nullptr, 0));
                break;
            case 'r': record = optarg; break;
            case 'R': replay = optarg; break;
            case 'i': initial = optarg; break;
            default:
                usage();
        }
    }

    //Load game library
    void *handle = dlopen(gameLib.c_str(),RTLD_LAZY | RTLD_GLOBAL);
    if (!handle) {
        ss << "Cannot load game library: " << dlerror();
        throw std::runtime_error(ss.str());
    }

    replaying = (replay.length() != 0);
    recording = (record.length() != 0);
    if (replaying && recording)
        throw std::runtime_error("Can not replay and record at the same time!");

    //Record a game
    if (recording) {
        recordFile.open(record,std::ofstream::trunc);
        if (!recordFile.is_open())
            throw std::runtime_error(std::string("Could not open record log: ")
                +std::string(strerror(errno)));
    }

    int playerCount = argc - optind;
    Game::create_func func = reinterpret_cast<Game::create_func>(dlsym(handle,"game_create"));
    if (func == nullptr)
        throw std::runtime_error("Unable to find game_create function in game library.");

    //Replay a recorded file
    if (replaying) {
        //Wrap the real game
        mcpgame = std::unique_ptr<Game>(new ReplayGame(func, replay));
        auto pl = static_cast<ReplayGame*>(mcpgame.get())->players();
        for (auto& p : pl)
            playerList.emplace_back(std::move(p));
    } else {
        mcpgame = std::unique_ptr<Game>(func(playerCount, initial));

        if (initial.length() != 0 && recording)
            recordFile << "Init: " << initial << std::endl;

        //Set up signal handling ...
        SignalHandler::ignoreSignal(SIGPIPE);

        //Spawn players
        for(int i = optind; i < argc; ++i) {
            if (recording)
                recordFile << "Player: " << argv[i] << std::endl;
            if (std::find(noTimeLimit.begin(), noTimeLimit.end(), i - optind) != noTimeLimit.end()) {
                playerList.emplace_back(std::make_unique<ProcessPlayer>(
                    argv[i], std::chrono::microseconds(0), std::chrono::microseconds(0)));
            }
            else
                playerList.emplace_back(std::make_unique<ProcessPlayer>(argv[i], softTime, hardTime));
        }
    }

    //Say Hello
    std::cout << "Master Control Program for " << mcpgame->name() << std::endl;
    if (replaying)
        std::cout << "Replaying: " << replay << std::endl;

    int turn = 0;
	while (true) {
        auto player = mcpgame->currentPlayer();
        std::cout << "[MCP] Turn " << ++turn << std::endl;
        if (recording) {
            recordFile << "---" << std::endl;
            recordFile << "Next player: " << player << std::endl;
            recordFile << "Board: " << mcpgame->serialize();
        }
		if (!replaying && player >= playerList.size()) {
            std::cerr << "[MCP] INTERNAL ERROR: Got invalid player from game!"
                      << std::endl;
            return INTERNAL_ERROR;
		}
        Game::MoveResult res = Game::MoveResult::Invalid;
        std::string msg, move;
        while (res == Game::MoveResult::Invalid) {
            std::cout << "[MCP] Player " << mcpgame->playerName(player)
                      << "'s turn" << std::endl;
            mcpgame->renderBoard();
            std::string boardState = mcpgame->serialize();
            std::cout << "[MCP] Board state: " << boardState;
            std::cout.flush();
            *playerList.at(player) << boardState;
            *playerList.at(player) >> move;
            std::cout << "[MCP] Player move: " << move << std::endl;

            std::tie(res,msg) = mcpgame->move(move);
            if (recording && res != Game::MoveResult::Invalid) {
                recordFile << "Move: " << move << std::endl;;
                recordFile.flush();
            }
            switch (res) {
                case Game::MoveResult::End:
                    mcpExit(ExitReason::EXIT,0,*mcpgame,"Replay ended");
                case Game::MoveResult::Won:
                    mcpExit(ExitReason::WIN,player,*mcpgame,msg);
                case Game::MoveResult::Draw:
                    mcpExit(ExitReason::DRAW,player,*mcpgame,msg);
                case Game::MoveResult::Invalid:
                    if (std::find(retryMove.begin(), retryMove.end(), player) == retryMove.end()) {
                        mcpExit(ExitReason::INVALID_MOVE,player,*mcpgame,msg);
                    } else {
                        std::cout << "[MCP] Invalid Move. Retry." << std::endl;
                        std::cout << "[MCP] " << msg << std::endl;
                        mcpgame->expectedFormat();
                    }
               case Game::MoveResult::OK: ;//Just continue
            }
        }
	}

	__builtin_unreachable();
} catch (std::exception &e) {
		std::cerr << "[MCP] internal error: " << e.what() << std::endl;
	    return INTERNAL_ERROR;
}
