/* bzflag
 * Copyright (c) 1993-2025 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

#include "CursesUI.h"


// add this UI to the map
UIAdder CursesUI::uiAdder("curses", &CursesUI::creator);


CursesUI::CursesUI(BZAdminClient& c) :
    BZAdminUI(c), menuWin(NULL),
    menuState(NoMenu), menu(c), client(c), players(c.getPlayers()),
    me(c.getMyId()), maxHistory(20), currentHistory(0),
    maxBufferSize(300), scrollOffset(0)
{

    // initialize ncurses
    initscr();
    start_color();
    use_default_colors();
    init_pair(Default, COLOR_FGDEFAULT, COLOR_BGDEFAULT);
    init_pair(White, COLOR_WHITE, COLOR_BGDEFAULT);
    init_pair(Red, COLOR_RED, COLOR_BGDEFAULT);
    init_pair(Green, COLOR_GREEN, COLOR_BGDEFAULT);
    init_pair(Blue, COLOR_BLUE, COLOR_BGDEFAULT);
    init_pair(Purple, COLOR_MAGENTA, COLOR_BGDEFAULT);
    init_pair(Yellow, COLOR_YELLOW, COLOR_BGDEFAULT);
    init_pair(Cyan, COLOR_CYAN, COLOR_BGDEFAULT);
    nonl();
    cbreak();
    noecho();

    // create main output window
    mainWin = newwin(LINES - 5, 0, 0, 0);
    wsetscrreg(mainWin, 0, LINES - 2);
    scrollok(mainWin, TRUE);
    wrefresh(mainWin);

    // create target window
    targetWin = newwin(1, 0, LINES - 2, 0);
    wattron(targetWin, A_UNDERLINE);
    targetIter = players.begin();
    updateTargetWin();

    // create command window
    cmdWin = newwin(0, 0, LINES - 1, 0);
    keypad(cmdWin, TRUE);
    nodelay(cmdWin, TRUE);
    updateCmdWin();

    // initialize the menu
    menu.setUpdateCallback(initMainMenu);

    // add additional chat targets
    additionalTargets[PlayerId(FirstTeam - ObserverTeam)] = PlayerInfo("teammates");
    additionalTargets[AdminPlayers] = PlayerInfo("admins");
}


CursesUI::~CursesUI()
{
    endwin();
}


void CursesUI::outputMessage(const std::string& msg, ColorCode color)
{
    // add message to the message buffer, remove the oldest message if it's full
    if (msgBuffer.size() == maxBufferSize)
        msgBuffer.erase(msgBuffer.begin());
    std::pair<std::string, ColorCode> p(msg, color);
    msgBuffer.push_back(p);

    // if we have scrolled away from the bottom, don't show the new message
    if (scrollOffset == 0)
    {
        wattron(mainWin, COLOR_PAIR(color));
        waddstr(mainWin, (msg + "\n").c_str());
        wattroff(mainWin, COLOR_PAIR(color));
        wrefresh(mainWin);
    }
}


void CursesUI::handleNewPacket(uint16_t code)
{
    BZAdminUI::handleNewPacket(code);
    menu.handleNewPacket(code);
}


bool CursesUI::checkCommand(std::string& str)
{
    wrefresh(cmdWin);
    str = "";

    // get a character and do checks that are always needed
    int c = wgetch(cmdWin);
    switch (c)
    {
    case KEY_RESIZE:
        handleResize(LINES, COLS);
        return false;
    case KEY_F(2):
        toggleMenu();
        return false;
    case ERR:
        return false;
    }

    // if the menu is active, use the keystrokes for that
    if (menuState == 1)
        return menu.handleKey(c, str);

    // if not, go ahead and parse commands
    switch (c)
    {

    // clear command (21 is Ctrl-U)
    case 21:
        cmd = "";
        updateCmdWin();
        currentHistory = history.size();
        return false;

    // delete last character
    case KEY_BACKSPACE:
    case KEY_DC:
    case 8:
    case 127:
        cmd = cmd.substr(0, cmd.size() - 1);
        updateCmdWin();
        return false;

    // redraw command (12 is ctrl-l)
    case 12:
        wclear(cmdWin);
        wclear(targetWin);
        updateCmdWin();
        updateTargetWin();
        return false;

    // send command
    case '\n': // works with PDCurses
    case 13:   // works with ncurses
        if (history.size() == maxHistory)
            history.erase(history.begin());
        history.push_back(cmd);
        str = cmd;
        cmd = "";
        currentHistory = history.size();
        updateCmdWin();
        return true;

    // scroll main window
    case KEY_NPAGE:
        scrollOffset = (scrollOffset < (unsigned(LINES - 2) / 2) ?
                        0 : scrollOffset - (LINES - 2) / 2);
        updateMainWinFromBuffer(LINES - 2);
        return false;
    case KEY_PPAGE:
        if (msgBuffer.size() < unsigned(LINES - 2))
            scrollOffset = 0;
        else if (scrollOffset > msgBuffer.size() - (LINES - 2) - (LINES - 2) / 2)
            scrollOffset = msgBuffer.size() - (LINES - 2);
        else
            scrollOffset += (LINES - 2) / 2;
        updateMainWinFromBuffer(LINES - 2);
        return false;

    // change target - we have two maps to iterate over, so if we get to
    // the end/beginning of the first one we go to the beginning/end of the
    // second one and vice versa, also the maps should never be empty
    case KEY_LEFT:
        if (targetIter == additionalTargets.begin())
        {
            targetIter = players.begin();
            for (unsigned int j = 0; j < players.size() - 1; j++)
                ++targetIter;
        }
        else if (targetIter == players.begin())
        {
            targetIter = additionalTargets.begin();
            for (unsigned int j = 0; j < additionalTargets.size() - 1; j++)
                ++targetIter;
        }
        else
            --targetIter;
        updateTargetWin();
        return false;
    case KEY_RIGHT:
        ++targetIter;
        if (targetIter == players.end())
            targetIter = additionalTargets.begin();
        else if (targetIter == additionalTargets.end())
            targetIter = players.begin();
        updateTargetWin();
        return false;

    // command history
    case KEY_UP:
        if (currentHistory == 0 || history.empty())
            return false;
        --currentHistory;
        cmd = history[currentHistory];
        updateCmdWin();
        return false;
    case KEY_DOWN:
        if (currentHistory < history.size())
            ++currentHistory;
        if (currentHistory == history.size())
            cmd = "";
        else
            cmd = history[currentHistory];
        updateCmdWin();
        return false;

    // kick target
    case KEY_F(5):
        if (targetIter != players.end() && targetIter->first != me &&
                targetIter->first <= LastRealPlayer)
        {
            if (targetIter->second.isAdmin)
            {
                outputMessage("Warning: Kicking Administrator ("
                              + targetIter->second.name + ")!", Red);
            }
            cmd = "/kick \"";
            cmd += targetIter->second.name;
            cmd += "\"";
            targetIter = players.find(me);
            updateCmdWin();
            updateTargetWin();
        }
        return false;

    // ban target
    case KEY_F(6):
        if (targetIter != players.end() && targetIter->first != me &&
                targetIter->first <= LastRealPlayer)
        {
            if (targetIter->second.ip != "")
            {
                if (targetIter->second.isAdmin)
                {
                    outputMessage("Warning: Banning Administrator ("
                                  + targetIter->second.name + ")!", Red);
                }
                cmd = "/ban ";
                cmd += targetIter->second.ip;
                targetIter = players.find(me);
                updateCmdWin();
                updateTargetWin();
            }
            else
            {
                std::string msg = "--- Can't ban ";
                msg += targetIter->second.name + ", you don't have the IP address";
                outputMessage(msg, White);
            }
        }
        return false;

    // tab - autocomplete
    case '\t':
    {
        std::string matches;
        cmd = comp.complete(cmd, &matches);
        updateCmdWin();
        if (matches.size() > 0)
        {
            outputMessage(matches, White);
            updateTargetWin();
        }
        return false;
    }
    default:
        if (c < 32 || c > 127 || cmd.size() >= CMDLENGTH)
            return false;
        cmd += char(c);
        updateCmdWin();
        return false;
    }
}


void CursesUI::addedPlayer(PlayerId p)
{
    PlayerIdMap::const_iterator iter = players.find(p);
    comp.registerWord(iter->second.name, true /* quote spaces */);
    if (p == me)
        targetIter = iter;
}


void CursesUI::removingPlayer(PlayerId p)
{
    if (targetIter->first == p)
    {
        targetIter = players.find(me);
        updateTargetWin();
    }
    comp.unregisterWord(players.find(p)->second.name);
}


PlayerId CursesUI::getTarget() const
{
    if (targetIter->first == me)
        return AllPlayers;
    return targetIter->first;
}


void CursesUI::handleResize(int lines, int cols)
{
    resizeterm(lines, cols);
    wresize(mainWin, lines - 2, cols);
    updateMainWinFromBuffer(lines - 2);
    mvwin(targetWin, lines - 2, 0);
    wresize(targetWin, 1, cols);
    mvwin(cmdWin, lines - 1, 0);
    wresize(cmdWin, 1, cols);
    updateTargetWin();
    updateCmdWin();
    wrefresh(mainWin);
}


void CursesUI::updateMainWinFromBuffer(unsigned int numberOfMessages)
{
    wclear(mainWin);
    int start = msgBuffer.size() - numberOfMessages - scrollOffset;
    start = (start < 0 ? 0 : start);
    unsigned int end = start + numberOfMessages;
    end = (end >= msgBuffer.size() ? msgBuffer.size() : end);
    for (unsigned int i = start ; i < end; ++i)
    {
        wattron(mainWin, COLOR_PAIR(msgBuffer[i].second));
        waddstr(mainWin, (msgBuffer[i].first + "\n").c_str());
        wattroff(mainWin, COLOR_PAIR(msgBuffer[i].second));
    }
    wrefresh(mainWin);
}


void CursesUI::updateTargetWin()
{
    werase(targetWin);
    wmove(targetWin, 0, 0);
    wmove(targetWin, 1, 1);
    std::string tmp = "Send to ";
    tmp = tmp + (targetIter == players.end() || targetIter->first == me ?
                 "all" : targetIter->second.name) + ":";
    waddstr(targetWin, tmp.c_str());
    wrefresh(targetWin);
}


void CursesUI::updateCmdWin()
{
    werase(cmdWin);
    wmove(cmdWin, 1, 1);
    waddstr(cmdWin, cmd.c_str());
    wmove(cmdWin, 1, 1 + cmd.size());
    wrefresh(cmdWin);
}


void CursesUI::toggleMenu()
{
    if (menuState == NoMenu)
    {
        menuState = VisibleActive;
        curs_set(0);
        const int menuWinSize = (LINES - 2) / 2;
        wresize(mainWin, LINES - 2 - menuWinSize, COLS);
        mvwin(mainWin, menuWinSize, 0);
        updateMainWinFromBuffer(LINES - 2 - menuWinSize);
        menuWin = newwin(menuWinSize, 0, 0, 0);
        menu.setWindow(menuWin);
        menu.showMenu();
    }
    else if (menuState == VisibleActive)
    {
        menuState = VisibleInactive;
        curs_set(1);
    }
    else
    {
        menuState = NoMenu;
        menu.setWindow(NULL);
        delwin(menuWin);
        wresize(mainWin, LINES - 2, COLS);
        mvwin(mainWin, 0, 0);
        updateMainWinFromBuffer(LINES - 2);
    }
    wrefresh(targetWin);
    wrefresh(cmdWin);
}


void CursesUI::initMainMenu(CursesMenu& menu)
{
    menu.setHeader("MAIN MENU");
    menu.clear();
    menu.addItem(new SubmenuCMItem("Show players",
                                   &CursesUI::initPlayerMenu));
    //menu.addItem(new SubmenuCMItem("Edit banlist",
    //                  &CursesUI::initBanMenu));
    menu.addItem(new SubmenuCMItem("Edit server variables",
                                   &CursesUI::initServerVarMenu));
    menu.addItem(new SubmenuCMItem("Edit message filter",
                                   &CursesUI::initFilterMenu));
}


void CursesUI::initPlayerMenu(CursesMenu& menu)
{
    menu.setHeader("PLAYERLIST");
    menu.clear();
    PlayerIdMap::const_iterator it;
    for (it = menu.players.begin(); it != menu.players.end(); ++it)
        menu.addItem(new PlayerCMItem(menu.players, it->first));
    menu.addItem(new SubmenuCMItem("Back to main menu",
                                   &CursesUI::initMainMenu));
    std::map<uint16_t, bool>& updateTypes(menu.getUpdateTypes());
    updateTypes.clear();
    updateTypes[MsgAddPlayer] = true;
    updateTypes[MsgRemovePlayer] = true;
    updateTypes[MsgScore] = false;
    updateTypes[MsgKilled] = false;
    updateTypes[MsgAdminInfo] = false;
}


void CursesUI::initBanMenu(CursesMenu& menu)
{
    menu.setHeader("BANLIST");
    menu.clear();
    menu.addItem(new SubmenuCMItem("Not implemented - go back",
                                   &CursesUI::initMainMenu));
    menu.getUpdateTypes().clear();
}


void CursesUI::initServerVarMenu(CursesMenu& menu)
{
    menu.setHeader("SERVER VARIABLE EDITOR");
    menu.clear();
    BZDB.iterate(&CursesUI::addBZDBCMItem, &menu);
    menu.addItem(new SubmenuCMItem("Back to main menu",
                                   &CursesUI::initMainMenu));
    std::map<uint16_t, bool>& updateTypes(menu.getUpdateTypes());
    updateTypes.clear();
    updateTypes[MsgSetVar] = false;
}


void CursesUI::addBZDBCMItem(const std::string& name, void* menu)
{
    ((CursesMenu*)menu)->addItem(new BZDBCMItem(name));
}


void CursesUI::initFilterMenu(CursesMenu& menu)
{
    menu.setHeader("MESSAGE FILTER EDITOR");
    menu.clear();
    std::map<std::string, uint16_t>::const_iterator iter;
    for (iter = menu.client.getMessageTypeMap().begin();
            iter != menu.client.getMessageTypeMap().end(); ++iter)
        menu.addItem(new FilterCMItem(iter->first, menu.client));
    menu.addItem(new SubmenuCMItem("Back to main menu",
                                   &CursesUI::initMainMenu));
    menu.getUpdateTypes().clear();
}


BZAdminUI* CursesUI::creator(BZAdminClient& client)
{
    return new CursesUI(client);
}

// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
