/*

  This module manages the !seen command. It's porpuse is to keep record of
when a certain nick was last seen by the bot. To acomplish this, it looks for
quit, part, kick, nick and join events and saves the nicks, along with a
timestamp, in the specified file. That file is NOT meant to be editable.
  So that's the only configuration option to place in the .conf:
"seen somefile.seen"
  If this .seen file doesn't exist (which normally happens when the bot is
being configured for the first time), it will be automaticaly created. Put one
of those for each server, because mbot keeps servers separated.

*/

#include "mbot.h"

#define LEVEL_SEEN 0

#define SEEN_MAX 2000		// maximum nicks on seen list at the same time

#define TIMESTR_SIZE 10
#define FIELD_SIZE (NICK_SIZE+TIMESTR_SIZE+1)

#define DEST s->script.dest
#define BUF s->script.buf
#define SEND_TEXT s->script.send_text

/////////////////////
// class definition
/////////////////////

class CSeen {
public:
  CSeen (CNetServer *, c_char);
  ~CSeen (void);

  int seen_index (c_char);
  time_t seen_time (c_char);
  void update_seen (c_char);

  CString filename;			// file where the database is
  struct seen_type {
    CString nick;			// nick
    time_t time;			// when was it last seen
    seen_type () : nick (NICK_SIZE) {}
  } seen[SEEN_MAX+1];
  int seen_num;				// number of nicks

  bool initialized;
  CNetServer *s;				// server to which belongs

private:

  void setup_seen (void);
  void change_seen (int);
  void write_seen (c_char, int);

  fstream f;
  char buf[BUF_SIZE+1];
};

CList *seen_list;

///////////////
// prototypes
///////////////

extern "C" {

static void seen_cmd_seen (CNetServer *);

time_t server2seentime (CNetServer *, c_char);	// used by alice.cpp
static CSeen *server2seen (CNetServer *);
static void seen_event (CNetServer *);
static void seen_conf (CNetServer *, c_char);
static void seen_stop (CModule *);
static void seen_start (CModule *);

}

////////////////////
// class functions
////////////////////

CSeen::CSeen (CNetServer *server, c_char name)
{
  initialized = 0;
  s = server;
  filename = name;
  seen_num = 0;
  for (int i = 0; i < SEEN_MAX+1; i++)
    seen[i].nick = NULL;
  setup_seen ();
  f.open (filename, ios::in | ios::out);
  if (!f)
    s->write_botlog ("error opening %s: %s\n", name, strerror (errno));
  initialized = 1;
}

CSeen::~CSeen (void)
{
//  for (int i = 0; i < seen_num; i++)
//    my_free (seen[i].nick);
}

// load file to memory
void
CSeen::setup_seen (void)
{
  seen_num = 0;
  ifstream f ((char *)filename);
  if (!f)
    s->write_botlog ("error opening %s: %s", (char *)filename, strerror (errno));

  char nick[NICK_SIZE+1];
  time_t t;
  while (f >> setfill ('\000') >> setw (NICK_SIZE+1) >> nick
           >> setw (TIMESTR_SIZE+1) >> t)
    {
      if (nick[0] == 0)			// eof
        return;
      seen[seen_num].nick = nick;
      seen[seen_num].time = t;
      if (seen_num == SEEN_MAX)
        {
          s->write_botlog ("error reading %s: too many entries (probably you must delete the file)",
                           (char *)filename);
          mbot_exit ();
        }
      seen_num++;
    }
}

// table index for that nick, -1 if nonexistent
int
CSeen::seen_index (c_char nick)
{
  for (int i = 0; i < seen_num; i++)
    if (seen[i].nick |= nick)
      return i;
  return -1;
}

// return time for nick, -1 if nonexistant
time_t
CSeen::seen_time (c_char nick)
{
  int i = seen_index (nick);
  if (i != -1)
    return seen[i].time;
  else
    return -1;
}

// update time for position i
void
CSeen::change_seen (int i)
{
  seen[i].time = s->time_now;
  f.seekp (FIELD_SIZE*i + NICK_SIZE, ios::beg);
  f << setfill ('0') << setw (TIMESTR_SIZE) << setiosflags (ios::right)
    << seen[i].time;
}

// write nick to file and memory, with the current time, at position i
void
CSeen::write_seen (c_char nick, int i)
{
  seen[i].nick = nick;
  seen[i].time = s->time_now;
  f.seekp (FIELD_SIZE*i, ios::beg);
  f << setfill ('\000') << setw (NICK_SIZE) << setiosflags (ios::left)
    << seen[i].nick << setfill ('0') << setw (TIMESTR_SIZE)
    << setiosflags (ios::right) << seen[i].time << '\n';
}

// update nick, change time or add it if nonexistant, possibly overwriting another
void
CSeen::update_seen (c_char nick)
{
  if (strncmp (nick, "Guest", 5) == 0)	// ignore PTnet's "Guest*" nicks
    return;
  int i = seen_index (nick);
  if (i != -1)			// doesn't exist
    {
      change_seen (i);					// change time
      return;
    }
  bool added;
  if (seen_num == SEEN_MAX)			// if on the limit
    {
      time_t t = s->time_now;
      i = 0;
      for (int i2 = 0; i2 < seen_num; i2++)		// seach the oldest
        if (seen[i2].time < t)
          {
            i = i2;
            t = seen[i2].time;
          }
      added = 0;
    }
  else						// else add to the end
    {
      added = 1;
      i = seen_num;
    }
  write_seen (nick, i);				// write
  if (added)
    seen_num++;
}

/////////////
// commands
/////////////

extern "C" {

// !seen nick
static void
seen_cmd_seen (CNetServer *s)
{
  CSeen *seen = server2seen (s);
  if (seen == NULL)
    {
      SEND_TEXT (DEST, "This command is not available.");
      return;
    }
  strsplit (CMD[3], BUF, 2);
  if (BUF[1][0] == 0)
    {
      SEND_TEXT (DEST, "usage: !seen nick");
      return;
    }
  for (int i = 0; i < s->channel_num; i++)	// search in channels
    if (CHANNELS[i]->user_index (BUF[1]) != -1)
      {
        SEND_TEXT (DEST, "%s is currently online.", BUF[1]);
        return;
      }
  time_t t = seen->seen_time (BUF[1]);		// search in seen
  if (t == -1)
    {
      SEND_TEXT (DEST, "Humm, I don't remember that nick.");
      return;
    }
  my_strncpy (BUF[2], asctime (localtime (&t)), MSG_SIZE);
  BUF[2][strlen (BUF[2]) - 1] = 0;
  SEND_TEXT (DEST, "I last saw %s on %s.", BUF[1], BUF[2]);
}

////////////////////
// module managing
////////////////////

time_t
server2seentime (CNetServer *s, c_char nick)
{
  CSeen *seen = server2seen (s);
  if (seen == NULL)
    return -1;
  return seen->seen_time (nick);
}

// return the seen for a given server, NULL if nonexistant
static CSeen *
server2seen (CNetServer *s)
{
  CSeen *a;
  seen_list->rewind ();
  while ((a = (CSeen *)seen_list->next ()) != NULL)
    if (a->s == s)
      return a;
  return NULL;
}

// look for join, quit, kick, part and nick to update seen database
static void
seen_event (CNetServer *s)
{
  if (strcmp (CMD[1], "JOIN") == 0
      || strcmp (CMD[1], "QUIT") == 0
      || strcmp (CMD[1], "KICK") == 0
      || strcmp (CMD[1], "PART") == 0)
    {
      CSeen *seen = server2seen (s);
      if (seen == NULL)
        return;
      mask2nick (CMD[0], BUF[0], NICK_SIZE);
      seen->update_seen (BUF[0]);
    }
  else
    if (strcmp (CMD[1], "NICK") == 0)
      {
        CSeen *seen = server2seen (s);
        if (seen == NULL)
          return;
        mask2nick (CMD[0], BUF[0], NICK_SIZE);
        seen->update_seen (BUF[0]);
        seen->update_seen (CMD[2]);
      }
}

// look for who replies to update seen database
static void
seen_reply (CNetServer *s)
{
  if (strcmp (CMD[1], RPL_WHOREPLY) == 0)
    {
      CSeen *seen = server2seen (s);
      if (seen != NULL)
        seen->update_seen (CMD[7]);
    }
}

// configuration file's local parser
static void
seen_conf (CNetServer *s, c_char bufread)
{
  char buf[3][MSG_SIZE+1];

  strsplit (bufread, buf, 2);

  if (strcasecmp (buf[0], "seen") == 0)
    {
      if (buf[1][0] == 0)
        s->bot->conf_error ("sintax error: use \"seen seenfile\"");
      CSeen *seen = server2seen (s);
      if (seen != NULL)
        s->bot->conf_error ("seen already defined in this server");
      seen = new CSeen (s, buf[1]);
      if (seen == NULL || !seen->initialized)
        s->bot->conf_error ("error initializing seen");
      seen_list->add ((void *)seen);
      s->script.events.add ((void *)seen_event);
      s->script.replies.add ((void *)seen_reply);
      s->script.bind_cmd (seen_cmd_seen, LEVEL_SEEN, "!seen");
    }

}

// module termination
static void
seen_stop (CModule *m)
{
  CSeen *seen;
  seen_list->rewind ();
  while ((seen = (CSeen *)seen_list->next ()) != NULL)
    {
      seen->s->script.events.del ((void *)seen_event);
      seen->s->script.replies.del ((void *)seen_reply);
      seen->s->script.unbind_cmd ("!seen");
      delete seen;
    }
}

// module initialization
static void
seen_start (CModule *m)
{
  seen_list = new CList ();
}

struct CModule::module_type module = {
  MODULE_VERSION,
  "seen",
  seen_start,
  seen_stop,
  seen_conf,
  NULL
};

}
