// imapd/Session.cc
// This file is part of Decimail; see http://decimail.org
// (C) 2004 Philip Endecott
 
// This is version $Name$
//   (if there is no version (e.g. V0-1) mentioned in the previous line,
//    this is probably a snapshot from between "official" releases.)
 
// 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 "Session.hh"

#include "CommandParser.hh"
#include "Configuration.hh"

#include "utils.hh"
#include "select.hh"

#include <algorithm>
using namespace std;

#ifdef USE_IDENT
#include <ident.h>
#endif

#include <syslog.h>


namespace Imapd {

  void CloseConnection::report(ostream& strm)
  {
    strm << "Connection close requested" << endl;
  }


  Session::Session(int i, int o):
    num(next_num++), in_fd(i), out_fd(o), state(non_auth),
    unreported_messages(false)
  {
    parser=new CommandParser(*this);
    out_f = fdopen(out_fd,"w");  // No C++ equivalent for this !
  }


  Session::~Session()
  {
    delete parser;
    flush();
    for(map<int,Message*>::iterator i=cached_messages.begin();
	i!=cached_messages.end(); ++i) {
      delete i->second;
    }
  }


  bool Session::wait_for_imap_or_db(void)
  {
    // Wait for something to happen on either the IMAP input or the
    // database connection (i.e. a notification).
    // Return TRUE for imap, FALSE for db.

    int db_fd = db.get_fd();

    int readable_fd = select_rr(db_fd,in_fd);
    return readable_fd == in_fd;
  }


  void Session::run(void)
  {
    bool preauth = false;

#ifdef USE_IDENT
    char* ident_cp = ident_id(in_fd,2);
    if (ident_cp) {
      set_username(ident_cp);
      db.listen_for_new_messages(username);
      set_state(auth);
      free(ident_cp);
      respond_untagged("PREAUTH decimail ready ("+username+")");
      preauth = true;
    }
#endif

    if (!preauth) {
      respond_untagged("OK decimail ready");
    }

    flush();

    int rc = fcntl(in_fd,F_SETFL,O_NONBLOCK);
    if (rc==-1) {
      throw SysException("fcntl(in_fd,F_SETFL,O_NONBLOCK)");
    }

    try {
      while (1) {

	// if unreported_messages is true, we should also
	// put "imap_out writable" in the select, so we can
	// retry the unilateral write when this becomes true.
	if (wait_for_imap_or_db()) {
	  // imap input ready
	  parser->parse(in_fd);
	} else {
	  // database notification pending
	  send_any_unilateral_updates(false);
	}
      }
    }
    catch(CloseConnection) {
      return;
    }
  }

  
  void Session::send_any_unilateral_updates(bool block)
  {
    if (state!=selected) {
      db.check_for_new_messages();  // to clear notification
      return;
    }

    if (db.check_for_new_messages()) {
      get_msg_ids();
      unreported_messages=true;
    } else {
      if (!unreported_messages) {
	return;
      }
    }

    int exists = seqnum_to_msgid.size()-1;
    int recent = exists;
    
    if (!block) {
      int rc = fcntl(out_fd,F_SETFL,O_NONBLOCK);
      if (rc==-1) {
	throw SysException("fcntl(out_fd,F_SETFL,O_NONBLOCK)");
      }
    }

    try {
      // Perhaps we should use a single write() for each response;
      // this scheme could give up after filling a buffer with half of
      // a response, which would be messy.
      // (But many responses e.g. fetch responses benefit from the
      // buffering of a stream layer)
      respond_untagged(int_to_string(exists)+" EXISTS");
      respond_untagged(int_to_string(recent)+" RECENT");
      flush();
      unreported_messages=false;
    }
    catch (SysException& E) {
      if (E.get_errno()!=EWOULDBLOCK) {
	throw;
      }
    }

    if (!block) {
      int rc = fcntl(out_fd,F_SETFL,0);
      if (rc==-1) {
	throw SysException("fcntl(out_fd,F_SETFL,0)");
      }
    }

    // other things to worry about: flags changing, messages deleted/expunged
  }


  void Session::get_msg_ids(void)
  {
    DmDatabase::Query q(&get_db());
    q << get_mailbox_query() << " order by msg_id;";
    q.run();
    seqnum_to_msgid.resize(q.get_ntuples()+1);
    msgid_to_seqnum.clear();
    for(int seqnum=1; seqnum<=q.get_ntuples(); ++seqnum) {
      int msgid = q.get_num(seqnum-1,0);
      seqnum_to_msgid[seqnum]=msgid;
      msgid_to_seqnum[msgid]=seqnum;
    }
  }


  class NotAllowedInThisState: public RespondBad {
  public:
    NotAllowedInThisState(void):
      RespondBad("Command not allowed in this state") {}
  };


  void Session::check_state(int allowed_states)
  {
    if (!(allowed_states&state)) {
      throw NotAllowedInThisState();
    }
  }

  void Session::respond_ok(string tag, string msg)
  {
    respond(tag,"OK",msg);
  }

  void Session::respond_no(string tag, string msg)
  {
    respond(tag,"NO",msg);
  }

  void Session::respond_bad(string tag, string msg)
  {
    respond(tag,"BAD",msg);
  }

  void Session::respond_untagged(string msg)
  {
    if (Configuration::singleton().get_bool("imapd_log_imap",false)) {
      syslog(LOG_MAIL|LOG_DEBUG,"{%d} S: * %s",num,msg.c_str());
    }
    fprintf(out_f,"* %s\r\n",msg.c_str());
  }

  void Session::respond(string tag, string response, string msg)
  {
    if (Configuration::singleton().get_bool("imapd_log_imap",false)) {
      syslog(LOG_MAIL|LOG_DEBUG,"{%d} S: %s %s %s",num,tag.c_str(),response.c_str(),msg.c_str());
    }
    fprintf(out_f,"%s %s %s\r\n",tag.c_str(),response.c_str(),msg.c_str());
  }

  void Session::send_continuation_request(void)
  {
    fprintf(out_f,"+ continue\r\n");
    fflush(out_f);
  }

  void Session::flush(void)
  {
    fflush(out_f);
  }


  void Session::set_mailbox(string m)
  {
    mailbox = m;
    mailbox_query = get_db().get_mailbox_query(get_username(),m);
  }

  
  template<typename T, typename U>
  struct contained_in_map {
    const map<T,U>& the_map;
    contained_in_map(const map<T,U>& m): the_map(m) {}
    bool operator()(const T& t) { return the_map.find(t) != the_map.end(); }
  };


  void Session::prefetch_messages(const list<int>& msg_ids)
  {
    list<int> new_msg_ids;
    remove_copy_if(msg_ids.begin(), msg_ids.end(), back_inserter(new_msg_ids),
		   contained_in_map<int,Message*>(cached_messages));

    auto_ptr<list<Message*> > new_msgs = db.get_messages(new_msg_ids);

    for(list<Message*>::const_iterator i=new_msgs->begin();
	i!=new_msgs->end(); ++i) {
      cached_messages[(*i)->get_id()] = (*i);
    }
  }


  int Session::next_num=0;

};
