// Bot.C  -*- C++ -*-
// Copyright (c) 1997, 1998 Etienne BERNARD
// Copyright (C) 2002 Clinton Ebadi

// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.

#include <fstream>
#include <iomanip>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#include "Bot.H"
#include "DCCConnection.H"
#include "StringTokenizer.H"
#include "ServerConnection.H"
#include "Utils.H"

#define DEFAULT_NICKNAME "Bobot"
#define DEFAULT_USERNAME "bobot"
#define DEFAULT_IRCNAME "I'm a bobot++!"
#define DEFAULT_COMMANDCHAR '!'
#define DEFAULT_USERLISTFILENAME "bot.users"
#define DEFAULT_SHITLISTFILENAME "bot.shit"
#define DEFAULT_HELPFILENAME "bot.help"
#define DEFAULT_SCRIPTLOGFILENAME "script.log"
#define DEFAULT_LOGFILENAME "bot.log"
#define DEFAULT_INITFILENAME "bot.init"
#ifdef USESCRIPTS
#define DEFAULT_AUTOEXECFILENAME "bot.autoexec"
#endif

Bot::Bot(String filename, bool debug_on)
  : nickName(DEFAULT_NICKNAME),
    wantedNickName(DEFAULT_NICKNAME),
    userName(DEFAULT_USERNAME),
    ircName(DEFAULT_IRCNAME),
    versionString(VERSION_STRING),
    userHost(""),
    localIP(""),
    commandChar(DEFAULT_COMMANDCHAR),
    configFileName(filename),
    userListFileName(DEFAULT_USERLISTFILENAME),
    shitListFileName(DEFAULT_SHITLISTFILENAME),
    logFileName(DEFAULT_LOGFILENAME),
    helpFileName(DEFAULT_HELPFILENAME),
    initFileName(DEFAULT_INITFILENAME),
#ifdef USESCRIPTS
    scriptLogFileName(DEFAULT_SCRIPTLOGFILENAME),
    autoexecFileName(DEFAULT_AUTOEXECFILENAME),
#endif
    connected(false),
    debug(debug_on), stop(false), sentPing(false),
    startTime(time(NULL)), currentTime(startTime),
    lastNickNameChange(startTime), lastChannelJoin(startTime),
    serverConnection(0), sentUserhostID(0), receivedUserhostID(0)
{
  extern userFunctionsStruct userFunctionsInit[];

#ifdef HAVE_STL_CLEAR
  wantedChannels.clear();
  ignoredUserhosts.clear();
  spyList.clear();
  userhostMap.clear();
#endif

  for (int i = 0; userFunctionsInit[i].name[0] != '\0'; i++) {
    userFunctions.push_back(new
			     userFunction(String(userFunctionsInit[i].name),
					  userFunctionsInit[i].function,
					  userFunctionsInit[i].minLevel,
					  userFunctionsInit[i].needsChannelName));
  }

#if HAVE_IOSBASE
  logFile.open(logFileName, std::ios_base::out | std::ios_base::ate 
	       | std::ios_base::app);
#else
 logFile.open(logFileName, ios::out | ios::ate 
	      | ios::app);
#endif
  logLine("Starting log.");
  channelList = new ChannelList();
  serverList = new ServerList();
  readConfig();
  userList = new UserList(userListFileName);
  shitList = new ShitList(shitListFileName);
  todoList = new TodoList();

  // Let's read the alias file
  std::ifstream initFile(initFileName);

  if (initFile) {
    String temp, alias, command;
    std::list<userFunction *>::iterator it;
    bool found = false;
    userFunction *u;
    int line = 0;
    while (initFile >> temp, temp.length() != 0) {
      line++;
      StringTokenizer st(temp);
      temp = temp.trim();
      if (temp[0]=='#') continue;
      if (st.countTokens(' ') != 2) {
        std::cerr << "Error when reading alias file (" << initFileName 
		  << ") line " << line << "...\n";
        continue;
      }
      alias = st.nextToken().toUpper();
      command = st.nextToken().toUpper();

      // Does the function already exist ?
      found = false;
      for (it = userFunctions.begin(); it != userFunctions.end(); ++it)
        if (alias == (*it)->name) {
          found = true;
          break;
        }
      if (found) continue;

      // Check that the command exists
      found = false;
      for (it = userFunctions.begin(); it != userFunctions.end(); ++it)
        if (command == (*it)->name) {
          found = true;
          u = *it;
          break;
        }
      if (!found) continue;

      userFunctions.push_back (new
				userFunction((char *)(const char *)alias,
                                           u->function,
                                           u->minLevel,
                                           u->needsChannelName));
    }
  }

  std::srand (std::time (0)); // srand for bot-random
#ifdef USESCRIPTS
  botInterp = new BotInterp(this, scriptLogFileName);
  botInterp->LoadScript(autoexecFileName);
#endif
}

Bot::~Bot()
{
  // TODO: is it ok to delete iterators!?!

  Person *p;
  while (spyList.size() != 0) {
    p = (*spyList.begin()).second;
    spyList.erase(spyList.begin());
    delete p;
  }
  DCCConnection *d;
  while (dccConnections.size() != 0) {
    d = *dccConnections.begin();
    dccConnections.erase(dccConnections.begin());
    delete d;
  }
  userFunction *u;
  while (userFunctions.size() != 0) {
    u = *userFunctions.begin();
    userFunctions.erase(userFunctions.begin());
    delete u;
  }
  wantedChannel *w;
  while (wantedChannels.size() != 0) {
    w = (*wantedChannels.begin()).second;
    wantedChannels.erase(wantedChannels.begin());
    delete w;
  }
  userList->save();
  shitList->save();
  delete channelList;
  delete userList;
  delete todoList;
  delete serverList;
  delete shitList;
  delete serverConnection;
  logLine("Stopping log.");
  logFile.close();
}

void
Bot::logLine(String line)
{
  tm *d;
  std::time_t current_time = time(0);

  d = localtime(&current_time);
  logFile << "[" <<  std::setfill('0') << std::setw(2)
          << d->tm_mday << "/" <<  std::setfill('0') << std::setw(2)
          << d->tm_mon + 1 << "/"
          << d->tm_year + 1900 << " - " <<  std::setfill('0') << std::setw(2)
          << d->tm_hour << ":" <<  std::setfill('0') << std::setw(2)
          << d->tm_min << ":" <<  std::setfill('0') << std::setw(2)
          << d->tm_sec << "] "
          << line
          << std::endl;
}

void
Bot::readConfig()
{
  std::ifstream file(configFileName);
  String temp;
  int line = 1;

  if (!file) {
    logLine(String("I cannot find the file ") + configFileName);
    return;
  }
  
  while (!file.eof()) {

    file >> temp;

    if (temp.length() == 0 || temp[0] == '#') {
      line++;
      continue;
    }
    
    StringTokenizer st(temp);
    String command = st.nextToken('=').trim().toUpper();
    String parameters = st.nextToken('=').trim();

    if (command == "NICK" || command == "NICKNAME")
      nickName = wantedNickName = parameters;
    else if (command == "USERNAME")
      userName = parameters;
    else if (command == "IRCNAME" || command == "REALNAME")
      ircName = parameters;
    else if (command == "CMDCHAR" || command == "COMMAND")
      commandChar = parameters[0];
    else if (command == "USERLIST")
      userListFileName = parameters;
    else if (command == "SHITLIST")
      shitListFileName = parameters;
    else if (command == "CHANNEL") {
      if (parameters.indexOf(':') == -1) {
        std::cout << "Warning. The 'channel' syntax has changed."
		  << " Please see the README file for more information."
		  << " I will use compatibility mode, but you're really"
		  << " missing something.\n";
        StringTokenizer st2(parameters);
        String name = st2.nextToken().toLower();
        String key = st2.nextToken();
        wantedChannels[name] = new wantedChannel("", "", key);
      } else {
        StringTokenizer st2(parameters);
        String name = st2.nextToken(':').toLower();
        String mode = st2.nextToken(':');
        String keep = st2.nextToken(':');
        String key = st2.nextToken(':');
        wantedChannels[name] = new wantedChannel(mode, keep, key);
      }
    }
    else if (command == "LOGFILE")
      logFileName = parameters;
#ifdef USESCRIPTS
    else if (command == "SCRIPTLOGFILE")
      scriptLogFileName = parameters;
    else if (command == "AUTOEXECFILE")
      autoexecFileName = parameters;
#endif
    else if (command == "INITFILE")
      initFileName = parameters;
    else if (command == "LOCALIP")
      localIP = parameters;
    else if (command == "SERVER") {
      if (parameters.indexOf(' ') == -1)
        serverList->addServer(new Server(parameters));
      else {
        StringTokenizer st2(parameters);
        String name = st2.nextToken();
        int port = std::atoi(st2.nextToken());
        serverList->addServer(new Server(name,
                                         port,
                                         st2.nextToken()));
      }
    }
    else {
      logLine(String("Syntax error in file ") + configFileName +
              ", line " + String((long)line));
      file.close();
      std::exit(1);
    }

    line++;
  }

  file.close();
}

void
Bot::run()
{
  nextServer();

  while (!stop) {
    waitForInput();  // This is the main event loop
    if (!serverConnection->queue->flush())
      nextServer();
  }
}

void
Bot::waitForInput()
{
#ifdef _HPUX_SOURCE
  int rd;
#else
  fd_set rd;
#endif
  struct timeval timer;

  int sock = serverConnection->getFileDescriptor();
  int maxSocketNumber = sock;

#ifdef _HPUX_SOURCE
  rd = sock;
#else
  FD_ZERO(&rd);
  FD_SET(sock, &rd);
#endif

  for (std::list<DCCConnection *>::iterator it = dccConnections.begin();
       it != dccConnections.end(); ++it) {
    int s = (*it)->getFileDescriptor();
#ifdef _HPUX_SOURCE
    rd |= s;
#else
    FD_SET(s, &rd);
#endif
    if (s > maxSocketNumber)
      maxSocketNumber = s;
  }

  timer.tv_sec = 1;
  timer.tv_usec = 0;

  switch (select(maxSocketNumber + 1, &rd, NULL, NULL, &timer)) {
  case 0: /* timeout */
    break;
  case -1: /* error */
    break;
  default: /* normal */
#ifdef _HPUX_SOURCE
    if (rd & sock)
#else
    if (FD_ISSET(sock, &rd))
#endif
      if (serverConnection->handleInput())
        nextServer();

    std::list<DCCConnection *>::iterator it = dccConnections.begin();
    std::list<DCCConnection *>::iterator it2;

    while (it != dccConnections.end()) {
      it2 = it;
      ++it;
#ifdef _HPUX_SOURCE
      if (rd & (*it2)->getFileDescriptor()) {
#else
      if (FD_ISSET((*it2)->getFileDescriptor(), &rd)) {
#endif
        if ((*it2)->handleInput()) {
          delete *it2;
          dccConnections.erase(it2);
        }
      }
    }
  }

  if (currentTime < std::time(NULL)) { // Actions that we do each second
    currentTime = std::time(NULL);
    for (std::map<String, unsigned int, std::less<String> >::iterator
           it = ignoredUserhosts.begin();
         it != ignoredUserhosts.end(); ++it)
      if ((*it).second > 0)
        (*it).second--;

    String line;
    while ((line = todoList->getNext()) != "") {
      serverConnection->queue->sendChannelMode(line);
    }
#ifdef USESCRIPTS
    botInterp->RunTimers(currentTime);
#endif

#ifdef USESCRIPTS
    tm *thisTime = localtime(&currentTime);
    if (thisTime->tm_sec == 0) {
      char s[6];
      sprintf(s, "%2d:%2d", thisTime->tm_hour, thisTime->tm_min);
      botInterp->RunHooks(Hook::TIMER, String(s),
                          gh_list(Utils::string2SCM(String(s)), SCM_UNDEFINED));
    }
#endif

  }

  if (currentTime >= (time_t)(lastNickNameChange + Bot::NICK_CHANGE) &&
      nickName != wantedNickName) {
    lastNickNameChange = currentTime;
    serverConnection->queue->sendNick(wantedNickName);
  }

  if (currentTime >= (std::time_t)(lastChannelJoin + Bot::CHANNEL_JOIN)) {
    lastChannelJoin = currentTime;
    for (std::map<String, wantedChannel *, std::less<String> >::iterator it =
           wantedChannels.begin(); it != wantedChannels.end();
         ++it)
      if (channelList->getChannel((*it).first) == 0)
         serverConnection->queue->sendJoin((*it).first, (*it).second->key);
  }

  std::list<DCCConnection *>::iterator it2;

  for (std::list<DCCConnection *>::iterator it = dccConnections.begin();
       it != dccConnections.end(); ) {
    it2 = it;
    ++it;
    if ((*it2)->autoRemove && currentTime >= (std::time_t)((*it2)->lastSpoken + Bot::DCC_DELAY)) {
      delete *it2;
      dccConnections.erase(it2);
    }
  }

  if (currentTime >= (std::time_t)(serverConnection->serverLastSpoken + Bot::PING_TIME) && !sentPing) {
    serverConnection->queue->sendPing("Testing connection");
    sentPing = true;
  }

  if (currentTime >= (std::time_t)(serverConnection->serverLastSpoken + Bot::TIMEOUT)) {
    sentPing = false;
    nextServer();
  }
}

// We can change server if we will not lose op on a channel
bool
Bot::canChangeServer()
{
  String channel;
  Channel *c;

  for (std::map<String, Channel *, std::less<String> >::iterator it =
         channelList->begin();
       it != channelList->end(); ++it) {
    channel = (*it).first;
    c = channelList->getChannel(channel);
    if (c->countOp == 1 &&
        c->count > 1 && this->iAmOp(channel))
      return false;
  }
  return true;
}

void
Bot::nextServer()
{
  bool cont = false;

  if (channelList)
    channelList->clear();

  if (serverConnection)
    userList->removeFirst();

  delete serverConnection;

  do {
    Server * s = serverList->nextServer();
    if (!s) {
      std::cout << "No server found. Exiting..." << std::endl;
      std::exit(1);
    }
    serverConnection = new ServerConnection(this, s, localIP);
    if (!serverConnection->connect()) {
      cont = true;
      // We sleep 10 seconds, to avoid connection flood
      sleep(10);
      delete serverConnection;
    } else {
      cont = false;
    }
  } while (cont);
}

void
Bot::reconnect()
{
  if (channelList)
    channelList->clear();

  userList->removeFirst();

  delete serverConnection;

  serverConnection =
    new ServerConnection(this, serverList->currentServer(), localIP);

  serverConnection->connect();
}

void
Bot::connect(int serverNumber)
{
  if (channelList)
    channelList->clear();

  userList->removeFirst();

  delete serverConnection;

  serverConnection =
    new ServerConnection(this, serverList->get(serverNumber), localIP);

  serverConnection->connect();
}

void
Bot::addDCC(Person * from, unsigned long address, int port)
{
  DCCConnection * d = new DCCConnection(this, from->getAddress(),
                                        address, port);

  if (!d->connect())
    return;

  dccConnections.push_back(d);
}

void
Bot::rehash()
{
  for (std::map<String, Channel *, std::less<String> >::iterator it = channelList->begin();
       it != channelList->end(); ++it)
    serverConnection->queue->sendWho((*it).first);
}

String
Bot::getUserhost(String channel, String nick)
{
  Channel *c;

  if (channel == "")
    c = 0;
  else
    c = channelList->getChannel(channel);

  nick = nick.toLower();


  if (c && c->hasNick(nick))
    return c->getUser(nick)->userhost;

  unsigned long num = sentUserhostID++;

  serverConnection->queue->sendUserhost(nick);
  userhostMap[num] = "+";

  while (userhostMap[num] == "+") {
    waitForInput();
    serverConnection->queue->flush();
  }

  // We have got our answer
  String res = userhostMap[num];
  userhostMap.erase(num);

  return res;
}

bool
Bot::iAmOp(String channel)
{
  User * me = channelList->getChannel(channel)->getUser(nickName);
  return (me->mode & User::OP_MODE);
}


syntax highlighted by Code2HTML, v. 0.9.1