// nagware.cpp : 'Nagware' plugin to encourage player registration
//

#include "bzfsAPI.h"
#include <stdio.h>

#define NAGWAREPLUG_VER "1.00.03"
#define MAX_PLAYERID    255
#define EVENT_FREQUENCY 15      // number of seconds between checks
#define TIME_FACTOR     60      // number of seconds per minute (useful to decrease for testing)


// TODO: check for msgs > 128 chars and warn ( readConfig() )


struct st_MsgEnt
{
    st_MsgEnt(int t, int r, std::string m): time(t), repeat(r), msg(m) {}
    int time;
    int repeat;
    std::string msg;
};
typedef struct st_MsgEnt MsgEnt;


typedef struct
{
    char permName[31];
    bool enableObs;
    bool countObs;
    int  minPlayers;
    MsgEnt *kickMsg;
    std::vector <MsgEnt *> nagMsgs;
    std::string msgSuffix;
} NagConfig;

NagConfig Config;


typedef struct
{
    bool isValid;
    char callsign[22];
    bz_eTeamType team;
    double  joinTime;
    double  nextEventTime;
    MsgEnt *nextEventMsg;
    bool    isVerified;
} NagPlayer;


NagPlayer Players[MAX_PLAYERID+1];
int       NumPlayers=0;
int       NumObservers=0;
int       MaxUsedID=0;
bool      NagEnabled = true;
double    MatchStartTime = 0;
char      ConfigFilename[256] = "";
float     NextEventTime = 0.0f;


class Nagware : public bz_Plugin, public bz_CustomSlashCommandHandler
{
public:
    virtual const char* Name ()
    {
        return "NAGWARE";
    }
    virtual void Init ( const char* config );
    virtual void Cleanup ();

    virtual void Event ( bz_EventData *eventData );
    virtual bool SlashCommand ( int playerID, bz_ApiString, bz_ApiString, bz_APIStringList* );

protected:

private:
};

BZ_PLUGIN(Nagware)

bool readConfig (char *filename, NagConfig *cfg, int playerID);

double nextRepeat (double playerTime, MsgEnt *m)
{
    if (m->repeat == 0)
        return 0;
    int last = (int)((playerTime - m->time) / m->repeat);
    return (m->time + (m->repeat * (last+1)));
}

void updatePlayerNextEvent (int playerID, double now)
{
    unsigned int idx;
    double playerTime =  now - Players[playerID].joinTime;
    double repeat;

    if (!Players[playerID].isValid || Players[playerID].isVerified)
        return;

    Players[playerID].nextEventTime = -1;
    if (Config.nagMsgs.empty())
        return;

    for (idx=0; idx<Config.nagMsgs.size(); idx++)
    {
        if (Config.nagMsgs[idx]->time > playerTime)
        {
            if (idx > 0 && (repeat = nextRepeat (playerTime, Config.nagMsgs[idx-1])) > 0
                    && repeat < Config.nagMsgs[idx]->time)
            {
                Players[playerID].nextEventTime = Players[playerID].joinTime + repeat;
                Players[playerID].nextEventMsg = Config.nagMsgs[idx-1];
            }
            else
            {
                Players[playerID].nextEventTime = Players[playerID].joinTime + Config.nagMsgs[idx]->time;
                Players[playerID].nextEventMsg = Config.nagMsgs[idx];
            }
            break;
        }
    }

    if (Players[playerID].nextEventTime < 0
            &&  (repeat = nextRepeat (playerTime, Config.nagMsgs[Config.nagMsgs.size()-1])) > 0)
    {
        Players[playerID].nextEventTime = Players[playerID].joinTime + repeat;
        Players[playerID].nextEventMsg = Config.nagMsgs[Config.nagMsgs.size()-1];
    }
}


void sendNagMessage (int who, std::string *msg )
{
    std::string fullMsg = *msg + Config.msgSuffix;
    unsigned int idx=0, x;

    while ((x = (unsigned int)fullMsg.find("\\n", idx)) != (unsigned int)std::string::npos)
    {
        bz_sendTextMessage(BZ_SERVER, who, fullMsg.substr(idx, x-idx).c_str());
        idx = x+2;
    }
    bz_sendTextMessage(BZ_SERVER, who, fullMsg.substr(idx).c_str());
}


void tickEvent (float time)
{
    int x;
    if (time < NextEventTime || !NagEnabled || MatchStartTime!=0.0)
        return;
    for (x=0; x<=MaxUsedID; x++)
    {
        if (Players[x].isValid && !Players[x].isVerified && Players[x].nextEventTime>=0 && time>Players[x].nextEventTime)
        {
            sendNagMessage(x, &Players[x].nextEventMsg->msg);
            updatePlayerNextEvent (x, time);
        }
    }
    x = NumPlayers;
    if (Config.countObs)
        x += NumObservers;
    if (Config.kickMsg && Config.kickMsg->time>0  && x>=Config.minPlayers)    // kick someone !
    {
        double kicktime = Config.kickMsg->time;
        for (x=0; x<=MaxUsedID; x++)
            if (Players[x].isValid && !Players[x].isVerified && time>(Players[x].joinTime+kicktime)
                    &&  (Config.enableObs || Players[x].team!=eObservers))
            {
                bz_kickUser (x, Config.kickMsg->msg.c_str(), true);
                break;
            }
    }
    NextEventTime = time + (float)EVENT_FREQUENCY;
}



void dispNagMsg (int who, const char* label, MsgEnt *m)
{
    char msg[140];

    if (m->repeat)
        sprintf (msg, "%s msg: %d (%d): ", label, m->time, m->repeat);
    else
        sprintf (msg, "%s msg: %d: ", label, m->time);
    strncat (msg, m->msg.c_str(), 130);
    if (strlen (msg) > 124)   // max line len is currently 125 (not 128!)
        strcpy (&msg[122], "...");
    bz_sendTextMessage (BZ_SERVER, who, msg);
}



void nagShowConfig (int who)
{
    unsigned int x;

    bz_sendTextMessage(BZ_SERVER, who, "nagware plugin configuration .........");
    bz_sendTextMessagef(BZ_SERVER, who, "perm name: %s", Config.permName);
    bz_sendTextMessagef(BZ_SERVER, who, "min players: %d %s", Config.minPlayers,
                        Config.countObs?"(including observers)":"");

    if (Config.enableObs)
        bz_sendTextMessage(BZ_SERVER, who, "Observer kick is ENABLED");
    else
        bz_sendTextMessage(BZ_SERVER, who, "Observer kick is DISABLED");
    if (Config.msgSuffix.size() > 0 )
        bz_sendTextMessagef(BZ_SERVER, who, "message suffix: %s", Config.msgSuffix.c_str());
    for (x=0; x<Config.nagMsgs.size(); x++)
        dispNagMsg (who, "nag ", Config.nagMsgs[x]);
    if (Config.kickMsg != NULL)
        dispNagMsg (who, "kick", Config.kickMsg);
    if (NagEnabled)
        bz_sendTextMessage(BZ_SERVER, who, "(plugin is currently ENabled)");
    else
        bz_sendTextMessage(BZ_SERVER, who, "(plugin is currently DISabled)");
}


void nagEnable (bool enable, int who)
{
    NagEnabled = enable;
    bz_sendTextMessage(BZ_SERVER, who, "OK.");
}


void nagList (int who)
{
    int numUnverified = 0;
    int x, timeOn;
    double now = bz_getCurrentTime();

    bz_sendTextMessage (BZ_SERVER, who, "Callsign (unverified)    Time ON");
    for (x=0; x<=MaxUsedID; x++)
    {
        if (Players[x].isValid && !Players[x].isVerified)
        {
            timeOn = (int)(now - Players[x].joinTime);
            bz_sendTextMessagef (BZ_SERVER, who, "%-25.25s %3d:%02d", Players[x].callsign, timeOn/60, timeOn%60);
            ++numUnverified;
        }
    }
    if (numUnverified == 0)
        bz_sendTextMessage (BZ_SERVER, who, "  --- NO unverified players ---");
    bz_sendTextMessagef (BZ_SERVER, who, "Players: %d   Observers:%d   TOTAL: %d", NumPlayers, NumObservers,
                         NumPlayers+NumObservers);
}


void nagReload (int who)
{
    if ( readConfig (ConfigFilename, &Config, who) )
    {
        bz_sendTextMessage(BZ_SERVER, who, "nagware config error, plugin disabled.");
        NagEnabled = false;
    }
    else
    {
        bz_sendTextMessage(BZ_SERVER, who, "nagware config reloaded.");
        // RECALC all player nextevents ...
        double now = bz_getCurrentTime();
        int x;
        for (x=0; x<MaxUsedID; x++)
            if (Players[x].isValid && !Players[x].isVerified)
                updatePlayerNextEvent (x, now);
    }
}



bool listAdd (int playerID, const char *callsign, bz_eTeamType team, bool verified, double time)
{
    if (playerID>MAX_PLAYERID || playerID<0)
        return false;
    Players[playerID].isValid = true;
    Players[playerID].team = team;
    Players[playerID].isVerified = verified;
    strncpy (Players[playerID].callsign, callsign, 20);
    Players[playerID].joinTime = time;
    if (Config.nagMsgs.empty())
        Players[playerID].nextEventTime = -1;
    else
    {
        Players[playerID].nextEventTime = time + (Config.nagMsgs[0]->time);
        Players[playerID].nextEventMsg = Config.nagMsgs[0];
    }

    if (team == eObservers)
        ++NumObservers;
    else
        ++NumPlayers;

    if (playerID > MaxUsedID)
        MaxUsedID = playerID;
    return true;
}

bool listDel (int playerID)
{
    if (playerID>MAX_PLAYERID || playerID<0 || !Players[playerID].isValid)
        return false;
    Players[playerID].isValid = false;
    if (Players[playerID].team == eObservers)
        --NumObservers;
    else
        --NumPlayers;
    return true;
}

void sendHelp (int who)
{
    bz_sendTextMessage(BZ_SERVER, who, "NAG commands: off, on, config, reload, list");
}

bool checkPerms (int playerID, const char *nagCmd, const char *permName)
{
    if (permName==NULL || *permName=='\0')
        permName = "NAG";
    if (bz_hasPerm (playerID, permName))
        return true;
    bz_sendTextMessagef (BZ_SERVER, playerID, "You need \"%s\" permission to do /nag %s", permName, nagCmd);
    return false;
}


/*
 *  Event handlers ....
*/

// handle events
void Nagware::Event ( bz_EventData *eventData )
{
    // player JOIN
    if (eventData->eventType == bz_ePlayerJoinEvent)
    {
        bz_PlayerJoinPartEventData_V1 *joinData = (bz_PlayerJoinPartEventData_V1*)eventData;
        bz_debugMessagef(4, "+++ nagware: Player JOINED (ID:%d, TEAM:%d, CALLSIGN:%s)", joinData->playerID,
                         joinData->record->team, joinData->record->callsign.c_str());
        fflush (stdout);
        listAdd (joinData->playerID, joinData->record->callsign.c_str(), joinData->record->team, joinData->record->verified,
                 joinData->eventTime);

        // player PART
    }
    else if (eventData->eventType == bz_ePlayerPartEvent)
    {
        bz_PlayerJoinPartEventData_V1 *joinData = (bz_PlayerJoinPartEventData_V1*)eventData;
        bz_debugMessagef(4, "+++ nagware: Player PARTED (ID:%d, TEAM:%d, CALLSIGN:%s)", joinData->playerID,
                         joinData->record->team, joinData->record->callsign.c_str());
        fflush (stdout);
        listDel (joinData->playerID);

        // game START
    }
    else if (eventData->eventType == bz_eGameStartEvent)
    {
        bz_GameStartEndEventData_V1 *msgData = (bz_GameStartEndEventData_V1*)eventData;
        bz_debugMessagef(4, "+++ nagware: Game START (%f, %f)", msgData->eventTime, msgData->duration);
        fflush (stdout);
        MatchStartTime = msgData->eventTime;

        // game END
    }
    else if (eventData->eventType == bz_eGameEndEvent)
    {
        bz_GameStartEndEventData_V1 *msgData = (bz_GameStartEndEventData_V1*)eventData;
        bz_debugMessagef(4, "+++ nagware: Game END (%f, %f)", msgData->eventTime, msgData->duration);
        fflush (stdout);
        MatchStartTime = 0.0f;
        // can determine length of match, and adjust event times if needed.

        // tick
    }
    else if (eventData->eventType == bz_eTickEvent)
    {
        bz_TickEventData_V1 *msgData = (bz_TickEventData_V1*)eventData;
        tickEvent ((float)msgData->eventTime);

    }
}

// handle /nag command
bool Nagware::SlashCommand ( int playerID, bz_ApiString cmd, bz_ApiString, bz_APIStringList* cmdParams )
{
    char subCmd[6];
    if (strcasecmp (cmd.c_str(), "nag"))   // is it for me ?
        return false;

    if (cmdParams->get(0).c_str()[0] == '\0')
    {
        sendHelp (playerID);
        return true;
    }

    strncpy (subCmd, cmdParams->get(0).c_str(), 5);
    subCmd[4] = '\0';
    if (strcasecmp (subCmd, "conf") == 0)
    {
        if (checkPerms (playerID, "config", Config.permName))
            nagShowConfig (playerID);
    }
    else if (strcasecmp (subCmd, "off") == 0)
    {
        if (checkPerms (playerID, "off", Config.permName))
            nagEnable (false, playerID);
    }
    else if (strcasecmp (subCmd, "on") == 0)
    {
        if (checkPerms (playerID, "on", Config.permName))
            nagEnable (true, playerID);
    }
    else if (strcasecmp (subCmd, "relo") == 0)
    {
        if (checkPerms (playerID, "reload", Config.permName))
            nagReload (playerID);
    }
    else if (strcasecmp (subCmd, "list") == 0)
    {
        if (checkPerms (playerID, "list", Config.permName))
            nagList (playerID);
    }
    else
        sendHelp (playerID);
    return true;
}




/*
 * Plugin load and unload...
*/

bool commandLineHelp (void)
{
    bz_debugMessage(0, "+++ nagware plugin command-line error.");
    bz_debugMessage(0, "Command line args:  PLUGINNAME,configname");
    bz_debugMessage(0, "nagware plugin NOT loaded!");
    return true;
}


bool parseCommandLine (const char *cmdLine)
{
    if (cmdLine==NULL || *cmdLine=='\0')
        return commandLineHelp ();

    strncpy (ConfigFilename, cmdLine, 255);
    if (readConfig(ConfigFilename, &Config, -1))
    {
        bz_debugMessage (0, "+++ nagware plugin config file error, plugin NOT loaded");
        return true;
    }
    return false;
}

void Nagware::Init(const char* cmdLine)
{
    MaxWaitTime = 1.0f;

    double now = bz_getCurrentTime();

    if (parseCommandLine (cmdLine))
        return;

    // get current list of player indices ...
    bz_APIIntList *playerList = bz_newIntList();
    bz_getPlayerIndexList(playerList);
    for (unsigned int i = 0; i < playerList->size(); i++)
    {
        bz_BasePlayerRecord *playerRecord = bz_getPlayerByIndex(playerList->get(i));
        if (playerRecord != NULL)
        {
            listAdd(playerList->get(i), playerRecord->callsign.c_str(), playerRecord->team, playerRecord->verified, now);
            bz_freePlayerRecord(playerRecord);
        }
    }
    bz_deleteIntList (playerList);

    bz_registerCustomSlashCommand ("nag", this);
    Register(bz_ePlayerJoinEvent);
    Register(bz_ePlayerPartEvent);
    Register(bz_eGameStartEvent);
    Register(bz_eGameEndEvent);
    Register(bz_eTickEvent);

    bz_debugMessagef(0, "+++ nagware plugin loaded - v%s", NAGWAREPLUG_VER);
}

void Nagware::Cleanup(void)
{
    bz_removeCustomSlashCommand ("nag");
    Flush();
    bz_debugMessage(0, "+++ nagware plugin unloaded");
}

/*
 * Read Configuration file...
*/

bool configError (const char *msg, int linenum, int playerID, FILE *fp)
{
    char send[256];
    fclose (fp);
    sprintf (send, "+++ nagware config file error (%s) at line #%d", msg, linenum);
    bz_debugMessagef(0, send);
    if (playerID >=0)
        bz_sendTextMessage(BZ_SERVER, playerID, send);
    return true;
}


char *strtrim (char *s)
{
    char c;
    char *p;
    while (*s == ' ')
        ++s;
    p = strlen(s) + s -1;
    while ( ((c=*p)==' ' || c=='\n') && p>s)
        *p--='\0';
    return s;
}

MsgEnt * parseCfgMessage(char *m)
{
    char *p;
    int time, repeat=0;

    if ((p = strchr (m, ' ')) == NULL)
        return NULL;
    *p = '\0';
    if (strchr (m, ',') != NULL)
    {
        if (sscanf (m, "%d,%d", &time, &repeat) != 2)
            return NULL;
    }
    else
    {
        if (sscanf (m, "%d", &time) != 1)
            return NULL;
    }
    if (time<0 || time > 500 || repeat < 0 || repeat > 1000)
        return NULL;

// TODO: check linelen < 128

    return new MsgEnt (time*TIME_FACTOR, repeat*TIME_FACTOR, p+1);
}


int compareMsgEnt (const void *a, const void *b)
{
    return (*(const MsgEnt* const *)a)->time - (*(const MsgEnt* const *)b)->time;
}


bool readConfig (char *filename, NagConfig *cfg, int playerID)
{
    FILE *cfile = fopen (filename, "r");
    MsgEnt *md;
    int lineNum=0;
    char line[1026];
    char *p, *key, *val;

    if (cfile == NULL)
    {
        sprintf (line, "+++ Error opening nagware config file (%s)", filename);
        bz_debugMessagef(0, line);
        if (playerID >=0)
            bz_sendTextMessage(BZ_SERVER, playerID, line);
        return true;
    }

    // install defaults ...
    strcpy (cfg->permName, "NAG");
    cfg->enableObs = false;
    cfg->countObs = true;
    cfg->minPlayers = 0;
    cfg->msgSuffix = "";
    cfg->nagMsgs.clear();

    while ( fgets (line, 1024, cfile) != NULL )
    {
        ++lineNum;
        if (line[0]=='#' || strlen(line)<2)
            continue;

        if ((p = strchr (line, '=')) == NULL)
            return configError ("no '='", lineNum, playerID, cfile);
        *p = '\0';
        key = strtrim (line);
        val = strtrim (++p);

        if (!strcasecmp (key, "permname"))
            strncpy (cfg->permName, val, 30);

        else if (!strcasecmp (key, "kickobs"))
        {
            if ( !strcasecmp(val, "yes") || !strcasecmp(val, "true") )
                cfg->enableObs = true;
            else
                cfg->enableObs = false;
        }
        else if (!strcasecmp (key, "countobs"))
        {
            if ( !strcasecmp(val, "yes") || !strcasecmp(val, "true") )
                cfg->countObs = true;
            else
                cfg->countObs = false;
        }
        else if (!strcasecmp (key, "minplayers"))
        {
            if (sscanf (val, "%d", &cfg->minPlayers)!=1 || cfg->minPlayers<1 || cfg->minPlayers>100)
                return configError ("Invalid minplayers value", lineNum, playerID, cfile);
        }
        else if (!strcasecmp (key, "messagesuffix"))
            cfg->msgSuffix = std::string (val);

        else if (!strcasecmp (key, "message"))
        {
            if ((md = parseCfgMessage (val)) == NULL)
                return configError ("Invalid message format", lineNum, playerID, cfile);
            cfg->nagMsgs.push_back (md);
        }
        else if (!strcasecmp (key, "kickmessage"))
        {
            if ((md = parseCfgMessage (val)) == NULL)
                return configError ("Invalid kick message format", lineNum, playerID, cfile);
            cfg->kickMsg = md;
        }
        else
            return configError ("unknown tag", lineNum, playerID, cfile);
    }

    // sort the nagmsgs vector by time
    qsort (&cfg->nagMsgs[0], cfg->nagMsgs.size(), sizeof(MsgEnt *), compareMsgEnt);

    fclose (cfile);
    return false;
}


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