// imapd/Session.cc
// This file is part of Decimail; see http://decimail.org
// (C) 2004-2007 Philip Endecott
 
// 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>

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

#include <syslog.h>

#include <boost/bind.hpp>

using namespace std;
using namespace pbe;


namespace Imapd {

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


  Session::Session(FileDescriptor& i, FileDescriptor& o):
    num(next_num++), in_fd(i), out_fd(o),
    out_strm(o),
    state(non_auth), unreported_messages(false)
  {
    parser=new CommandParser(*this);
  }


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


  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();

    boost::thread notification_monitor(boost::bind(&Session::monitor_notifications,this));

    try {
      parser->parse(in_fd);
    }
    catch(CloseConnection) {
    }
  }


  void Session::monitor_notifications()
  {
    while (1) {
      wait_until(db.get_fd().readable());  // I hope this will throw when the db connection is closed
                                           // by the main thread terminating and the Session being destroyed.
      mutex_t::scoped_lock l(db_and_output_mutex);
      send_any_unilateral_updates(true);
    }
  }

  
  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) {
      out_fd.set_nonblocking();
    }

    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(boost::lexical_cast<string>(exists)+" EXISTS");
      respond_untagged(boost::lexical_cast<string>(recent)+" RECENT");
      flush();
      unreported_messages=false;
    }
    catch (SysException& E) {
      if (E.get_errno()!=EWOULDBLOCK) {
	throw;
      }
    }

    if (!block) {
      out_fd.set_blocking();
    }

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


  void Session::get_msg_ids(void)
  {
    Query<> q(get_db(), get_mailbox_query()+" order by msg_id");
    Result r = q.runonce();
    seqnum_to_msgid.resize(r.rows+1);
    msgid_to_seqnum.clear();
    for(int seqnum=1; seqnum<=r.rows; ++seqnum) {
      int msgid = r.get<int>(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());
    }
    out_strm << "* " << msg << "\r\n";
  }

  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());
    }
    out_strm << tag << " " << response << " " << msg << "\r\n";
  }

  void Session::send_continuation_request(void)
  {
    out_strm << "+ continue\r\n" << std::flush;
  }

  void Session::flush(void)
  {
    out_strm << std::flush;
  }


  void Session::set_username(string u)
  {
    username=u;
    get_db().create_user_views(u);
  }

  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;

};
