/*

  This module provides a dictionary-like database implementation. The 
keywords are not case sensitive. There are 6 available commands, they are:

!learn- word num
Delete <num> words from <word>'s definition, starting from the end.

!learn+ word text
Add <text> to <word>'s definition. If <word> doesn't exist, create it.

!learn word [text]
Add or replace <word> with <text>. If <text> if not especified, delete <word>
instead (if it exists).

!forget word
Delete the especified <word>.

!wordcount
Say the number of words defined.

?? word
Search <word> in the database and return its content.

  The options to configure:

"word somefile.word"
which of course contains the database.

"wordnotice"
which makes the "??" queries to be answered with notices instead of privmsg.

"wordnotdef That word is not defined!"
is to change the default message when words not defined are asked for.

"wordlog logname"
uses log <logname> to log succesful !learn commands

*/

#include "mbot.h"

#define LEVEL_LEARN_MINUS 7
#define LEVEL_LEARN_PLUS 7
#define LEVEL_LEARN 7
#define LEVEL_FORGET 7
#define LEVEL_WORDCOUNT 0
#define LEVEL_WORD2TEXT 0

#define HASH_SIZE 1000			// hash table's size
#define WORD_SIZE 30			// dictionary word's size
#define TEXT_SIZE MSG_SIZE		// dictionary definition's size
#define NOTDEF_DEFAULT "Not defined... lousy word anyway :D"

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

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

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

  struct word_type {
    CString word;		// key word
    long index;			// it's position on the file
    word_type *next;
    word_type (c_char w, long i) : word (w, WORD_SIZE), index (i),
      next (NULL) {}
  };
  struct word_type *words[HASH_SIZE+1];

  void word2text (c_char, char *, size_t);
  word_type *get_word (c_char) const;
  void add_word (c_char, c_char);
  bool del_word (c_char);

  CString filename,		// name of the word file
          wordnotdef;		// msg to show when a word is not defined
  int word_num;			// number of words
  bool notice;			// wether to send notice or privmsg
  CLog *log;
  CList server_list;		// servers to which belongs
  CBot *bot;

private:

  void setup_words (void);
  void register_word (c_char, long);
  void unregister_word (c_char);
  void delete_words (void);
  u_int hash (c_char, u_int) const;

  FILE *wfd;
  char buf[BUF_SIZE+1];
};

CList *word_list;

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

extern "C" {

static void word_cmd_learn_minus (CNetServer *);
static void word_cmd_learn_plus (CNetServer *);
static void word_cmd_learn (CNetServer *);
static void word_cmd_forget (CNetServer *);
static void word_cmd_wordcount (CNetServer *);
static void word_cmd_word2text (CNetServer *);

void server2wordtext (CNetServer *, c_char, char *, size_t);

static char *learn_minus_num (char *, int );
static CWord *file2word (c_char);
static CWord *server2word (CNetServer *);
static void word_add (CNetServer *, c_char);
static void word_conf (CNetServer *, c_char);
static void word_stop (CModule *);
static void word_start (CModule *);

}

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

CWord::CWord (CNetServer *server, c_char name) : filename (name, PATH_MAX),
  wordnotdef (NOTDEF_DEFAULT, MSG_SIZE), word_num (0), notice (0),
  log (NULL), bot (server->bot)
{
  server_list.add ((void *)server);
  wfd = fopen (filename, "r+");
  if (wfd == NULL)
    {
      server->write_botlog ("error opening %s: %s", (char *)filename, strerror (errno));
      mbot_exit ();
    }
  setup_words ();
}

CWord::~CWord (void)
{
  delete_words ();
  fclose (wfd);
}

// read the word file into the table
void
CWord::setup_words (void)
{
  int i, f_index;
  for (i = 0; i < HASH_SIZE; i++)
    words[i] = NULL;
  word_num = 0;
  fseek (wfd, 0, SEEK_SET);
  while (1)
    {
      f_index = ftell (wfd);
      if (fgets (buf, BUF_SIZE, wfd) == NULL)
        return;
      buf[num_notspaces(buf)] = 0;		// buf now has the word
      register_word (buf, f_index);
    }
}

// put the word on the table
void
CWord::register_word (c_char word, long f_index)
{
  int i = hash (word, HASH_SIZE);
  word_type *w = words[i], *w_previous = NULL;
  if (w == NULL)			// if the list is empty
    {
      words[i] = new word_type (word, f_index);
      word_num++;
      return;
    }
  while (w != NULL)			// else, find the end of the table
    {
      w_previous = w;
      w = w->next;
    }
  w = new word_type (word, f_index);
  w_previous->next = w;
  word_num++;
}

// delete the word from the table
void
CWord::unregister_word (c_char word)
{
  int i = hash (word, HASH_SIZE);
  word_type *w = words[i], *w_next = NULL;
  if (w == NULL)
    return;
  if (w->word |= word)			// if it's the first
    {
      words[i] = w->next;
      delete w;
      word_num--;
    }
  else
    while (w->next != NULL)		// else find it
      {
        if (strcasecmp (w->next->word, word) == 0)
          {
            w_next = w->next->next;
            delete w->next;
            w->next = w_next;
            word_num--;
            return;
          }
        w = w->next;
      }
}

// delete everything in the word table
void
CWord::delete_words (void)
{
  word_type *w, *w2;
  for (int i = 0; i < HASH_SIZE; i++)
    {
      w = words[i];
      words[i] = NULL;
      while (w != NULL)
        {
          w2 = w->next;
          delete w;
          w = w2;
        }
    }
}

// put in text the definition of word, or \0 if nonexistant
void
CWord::word2text (c_char word, char *text, size_t textlen)
{
  word_type *w = get_word (word);
  if (w != NULL)
    {
      fseek (wfd, w->index, SEEK_SET);
      fgets (buf, BUF_SIZE, wfd);
      strip_crlf (buf);
      my_strncpy (text, buf + strlen (word) + 1, textlen);
      return;
    }
  text[0] = 0;
}

// return pointer to word, NULL if nonexistant
struct CWord::word_type *
CWord::get_word (c_char word) const
{
  word_type *w;
  w = words[hash (word, HASH_SIZE)];
  while (w != NULL)
    {
      if (w->word |= word)
        return w;
      w = w->next;
    }
  return NULL;
}

// create a new word at the end of file
void
CWord::add_word (c_char word, c_char text)
{
  if (text[0] == 0)
    return;
  fseek (wfd, 0, SEEK_END);
  register_word (word, ftell (wfd));
  fprintf (wfd, "%s %s\n", word, text);
  fflush (wfd);
}

// delete a word, return 0 on failure
bool
CWord::del_word (c_char word)
{
  word_type *w = get_word (word);
  if (w == NULL)
    return 0;
  // delete from file
  FILE *tmpfd = tmpfile ();
  if (tmpfd == NULL)
    {
      bot->write_botlog ("error: can't create temporary file in del_word(): %s", strerror (errno));
      return 0;
    }
  fseek (wfd, w->index, SEEK_SET);		// offset of word to delete
  fgets (buf, BUF_SIZE, wfd);			// move a line forward
  while (fgets (buf, BUF_SIZE, wfd) != NULL)	// copy the rest to tmp
    fputs (buf, tmpfd);
  fseek (wfd, w->index, SEEK_SET);
  fseek (tmpfd, 0, SEEK_SET);
  while (fgets (buf, BUF_SIZE, tmpfd) != NULL)
    fputs (buf, wfd);
  fclose (tmpfd);
  ftruncate (fileno (wfd), ftell (wfd));
  fflush (wfd);

  // delete from memory
  unregister_word(word);

  // rearrange other word's index
  fseek (wfd, 0, SEEK_SET);
  int pos = 0, wordread = 0;
  while (wordread < word_num)
    {
      fgets (buf, BUF_SIZE, wfd);
      buf[num_notspaces (buf)] = 0;		// buf now has the word
      w = get_word (buf);
      if (w != NULL)
        w->index = pos;
      pos = ftell (wfd); 
      wordread++;
    }
  return 1;
}

u_int
CWord::hash (c_char st, u_int hash_max) const
{
  size_t i, s = strlen (st), r = 0;
  for (i = 0; i < s; i++)
    r = r + toupper (st[i]);
  return r % hash_max;
}

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

extern "C" {

// !learn- word num
static void
word_cmd_learn_minus (CNetServer *s)
{
  CWord *w = server2word (s);
  if (w == NULL)
    return;
  strsplit (CMD[3], BUF, 3);
  if (BUF[2][0] == 0)
    {
      SEND_TEXT (DEST, "usage: !learn- word num");
      return;
    }
  if (strlen (BUF[1]) > WORD_SIZE)
    {
      SEND_TEXT (DEST, "Word too large.");
      return;
    }
  if (w->log != NULL)
    *w->log << CMD[2] << ' ' << CMD[0] << ' ' << CMD[3] << EOL;
  w->word2text (BUF[1], BUF[3], MSG_SIZE);	// get the previous one
  if (BUF[3][0] != 0)				// if it exists
    {
      w->del_word (BUF[1]);			// delete it
      SEND_TEXT (DEST, "Word changed.");
    }
  else
    SEND_TEXT (DEST, "Word added.");
  snprintf (BUF[4], MSG_SIZE, "%s",
            learn_minus_num (BUF[3], atoi (BUF[2])));
  w->add_word (BUF[1], BUF[4]);
}

// !learn+ word text
static void
word_cmd_learn_plus (CNetServer *s)
{
  CWord *w = server2word (s);
  if (w == NULL)
    return;
  strsplit (CMD[3], BUF, 2);
  if (BUF[2][0] == 0)
    {
      SEND_TEXT (DEST, "usage: !learn+ word text");
      return;
    }
  if (strlen (BUF[1]) > WORD_SIZE)
    {
      SEND_TEXT (DEST, "Word too large.");
      return;
    }
  if (w->log != NULL)
    *w->log << CMD[2] << ' ' << CMD[0] << ' ' << CMD[3] << EOL;
  w->word2text (BUF[1], BUF[3], MSG_SIZE);	// get the previous one
  if (BUF[3][0] != 0)				// if it exists
    {
      w->del_word (BUF[1]);			// delete it
      SEND_TEXT (DEST, "Word changed.");
    }
  else
    SEND_TEXT (DEST, "Word added.");
  snprintf (BUF[4], MSG_SIZE, "%s %s", BUF[3], BUF[2]);
  w->add_word (BUF[1], BUF[4]);
}

// !learn word [text]
static void
word_cmd_learn (CNetServer *s)
{
  CWord *w = server2word (s);
  if (w == NULL)
    return;
  strsplit (CMD[3], BUF, 2);
  if (BUF[1][0] == 0)
    {
      SEND_TEXT (DEST, "usage: !learn word [text]");
      return;
    }
  if (strlen (BUF[1]) > WORD_SIZE)
    {
      SEND_TEXT (DEST, "Word too large.");
      return;
    }
  if (w->log != NULL)
    *w->log << CMD[2] << ' ' << CMD[0] << ' ' << CMD[3] << EOL;
  if (w->get_word (BUF[1]) != NULL)	// if it already exists
    {
      w->del_word (BUF[1]);		// delete it
      if (BUF[2][0] == 0)
        SEND_TEXT (DEST, "Word deleted.");
      else
        SEND_TEXT (DEST, "Word changed.");
    }
  else
    SEND_TEXT (DEST, "Word added.");
  w->add_word (BUF[1], BUF[2]);
}

// !forget word
static void
word_cmd_forget (CNetServer *s)
{
  CWord *w = server2word (s);
  if (w == NULL)
    return;
  strsplit (CMD[3], BUF, 2);
  if (BUF[1][0] == 0)
    {
      SEND_TEXT (DEST, "usage: !forget word");
      return;
    }
  if (!w->del_word (BUF[1]))
    SEND_TEXT (DEST, "Word %s does not exist.", BUF[1]);
  else
    {
      if (w->log != NULL)
        *w->log << CMD[2] << ' ' << CMD[0] << ' ' << CMD[3] << EOL;
      SEND_TEXT (DEST, "Word deleted.");
    }
}

// !wordcount
static void
word_cmd_wordcount (CNetServer *s)
{
  CWord *w = server2word (s);
  if (w != NULL)
    SEND_TEXT (DEST, "There are %d words defined.", w->word_num);
}

// ?? word
static void
word_cmd_word2text (CNetServer *s)
{
  CWord *w = server2word (s);
  if (w == NULL)
    return;
  strsplit (CMD[3], BUF, 2);
  if (BUF[1][0] != 0)
    {
      w->word2text (BUF[1], BUF[2], MSG_SIZE);
      if (BUF[2][0] == 0)
        my_strncpy (BUF[2], w->wordnotdef, MSG_SIZE);
      if (w->notice)		// broken in dcc chat!
        s->irc_notice (DEST, "%s == %s", BUF[1], BUF[2]);
      else
        SEND_TEXT (DEST, "%s", BUF[2]);
    }
}

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

void
server2wordtext (CNetServer *s, c_char word, char *text, size_t textlen)
{
  CWord *w = server2word (s);
  if (w == NULL)
    text[0] = 0;
  else
    w->word2text (word, text, textlen);
}

// Possidon made this one.. so blame him, not me *g*
static char *
learn_minus_num (char *ori, int m)
{
  int i = strlen (ori) - 1, n = 0, l = 0;
  while (i > 0 && n < m)
    {
      if (ori[i] != ' ')
        l = 1;
      while (ori[i] == ' ')
        {
          if (l != 0)
            {
              n++;
              l = 0;
            }
          i--;
        }
      i--;
    }
  if (i > 0)
    ori[i + 2] = '\0';
  return ori;
}

// returns the word object for a given word file, NULL if nonexistant
static CWord *
file2word (c_char filename)
{
  CWord *w;
  word_list->rewind ();
  while ((w = (CWord *)word_list->next ()) != NULL)
    if (strcmp (w->filename, filename) == 0)
      return w;
  return NULL;
}

// returns the word object for a given server, NULL if nonexistant
static CWord *
server2word (CNetServer *s)
{
  CWord *w;
  word_list->rewind ();
  while ((w = (CWord *)word_list->next ()) != NULL)
    if (w->server_list.exist ((void *)s))
      return w;
  return NULL;
}

// adds a server/channel pair to the list
static void
word_add (CNetServer *s, c_char name)
{
  // check if a word using <name> already exists
  CWord *word = file2word (name);
  if (word != NULL)
    {
      word->server_list.add ((void *)s);
      return;
    }
  // if not, create it
  word_list->add ((void *)new CWord (s, name));
}

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

  strsplit (bufread, buf, 2);

  if (strcasecmp (buf[0], "words") == 0)
    {
      if (server2word (s) == NULL)
        {
          word_add (s, buf[1]);
          s->script.bind_cmd (word_cmd_learn_minus, LEVEL_LEARN_MINUS, "!learn-");
          s->script.bind_cmd (word_cmd_learn_plus, LEVEL_LEARN_PLUS, "!learn+");
          s->script.bind_cmd (word_cmd_learn, LEVEL_LEARN, "!learn");
          s->script.bind_cmd (word_cmd_forget, LEVEL_FORGET, "!forget");
          s->script.bind_cmd (word_cmd_wordcount, LEVEL_WORDCOUNT, "!wordcount");
          s->script.bind_cmd (word_cmd_word2text, LEVEL_WORD2TEXT, "??");
        } else
          s->bot->conf_error ("words already defined for this server");
    }
  else if (strcasecmp (buf[0], "wordnotice") == 0)
    {
      CWord *w = server2word (s);
      if (w != NULL)
        w->notice = 1;
      else
        s->bot->conf_error ("words not yet defined for this server");
    }

  if (strcasecmp (buf[0], "wordnotdef") == 0)
    {
      CWord *w = server2word (s);
      if (w != NULL)
        {
          strsplit (bufread, buf, 1);
          w->wordnotdef = buf[1];
        }
      else
        s->bot->conf_error ("words not yet defined for this server");
    }

  if (strcasecmp (buf[0], "wordlog") == 0)
    {
      CWord *w = server2word (s);
      if (w == NULL)
        s->bot->conf_error ("words not yet defined for this server");
      w->log = s->bot->log_get (buf[1]);
      if (w->log == NULL)
        s->bot->conf_error ("inexistant log");
    }
}

// module termination
static void
word_stop (CModule *m)
{
  CWord *w;
  word_list->rewind ();
  while ((w = (CWord *)word_list->next ()) != NULL)
    {
      CNetServer *s;
      w->server_list.rewind ();
      while ((s = (CNetServer *)w->server_list.next ()) != NULL)
        {
          s->script.unbind_cmd ("!learn-");
          s->script.unbind_cmd ("!learn+");
          s->script.unbind_cmd ("!learn");
          s->script.unbind_cmd ("!forget");
          s->script.unbind_cmd ("!wordcount");
          s->script.unbind_cmd ("??");
        }
      delete w;
    }
  delete word_list;
}

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

struct CModule::module_type module = {
  MODULE_VERSION,
  "word",
  word_start,
  word_stop,
  word_conf,
  NULL
};

}
