/*
 * libjingle
 * Copyright 2004--2005, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products 
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#define TRACK_ARRAY_ALLOC_PROBLEM

#include <vector>
#include <sstream>
#include <algorithm>
#include "talk/xmllite/xmlelement.h"
#include "talk/base/common.h"
#include "talk/xmpp/xmppengineimpl.h"
#include "talk/xmpp/xmpplogintask.h"
#include "talk/xmpp/constants.h"
#include "talk/xmllite/xmlprinter.h"
#include "talk/xmpp/saslhandler.h"
// #include "buzz/saslmechanism.h"

#define new TRACK_NEW

namespace buzz {

static const std::string XMPP_CLIENT_NAMESPACES[] = {
  "stream", "http://etherx.jabber.org/streams",
  "", "jabber:client",
};

static const size_t XMPP_CLIENT_NAMESPACES_LEN = 4;

XmppEngine * XmppEngine::Create() {
  return new XmppEngineImpl();
}


XmppEngineImpl::XmppEngineImpl() :
    stanzaParseHandler_(this),
    stanzaParser_(&stanzaParseHandler_),
    engine_entered_(0),
    user_jid_(JID_EMPTY),
    password_(),
    requested_resource_(STR_EMPTY),
    tls_needed_(true),
    login_task_(new XmppLoginTask(this)),
    next_id_(0),
    bound_jid_(JID_EMPTY),
    state_(STATE_START),
    encrypted_(false),
    error_code_(ERROR_NONE),
    stream_error_(NULL),
    raised_reset_(false),
    output_handler_(NULL),
    session_handler_(NULL),
    iq_entries_(new IqEntryVector()),
    output_(new std::stringstream()),
    sasl_handler_(NULL) {
  for (int i = 0; i < HL_COUNT; i+= 1) {
    stanza_handlers_[i].reset(new StanzaHandlerVector());
  }
}

XmppEngineImpl::~XmppEngineImpl() {
  DeleteIqCookies();
}

XmppReturnStatus
XmppEngineImpl::SetOutputHandler(XmppOutputHandler* output_handler) {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  output_handler_ = output_handler;

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::SetSessionHandler(XmppSessionHandler* session_handler) {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  session_handler_ = session_handler;

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::HandleInput(const char * bytes, size_t len) {
  if (state_ < STATE_OPENING || state_ > STATE_OPEN)
    return XMPP_RETURN_BADSTATE;

  EnterExit ee(this);

  stanzaParser_.Parse(bytes, len, false);

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::ConnectionClosed() {
  if (state_ != STATE_CLOSED) {
    EnterExit ee(this);
    // If told that connection closed and not already closed,
    // then connection was unpexectedly dropped.
    SignalError(ERROR_CONNECTION_CLOSED);
  }
  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::SetUseTls(bool useTls) {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  tls_needed_ = useTls;

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::SetTlsServerDomain(const std::string & tls_server_domain) {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  tls_server_domain_= tls_server_domain;

  return XMPP_RETURN_OK;
}

bool
XmppEngineImpl::GetUseTls() {
  return tls_needed_;
}

XmppReturnStatus
XmppEngineImpl::SetUser(const Jid & jid) {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  user_jid_ = jid;

  return XMPP_RETURN_OK;
}

const Jid &
XmppEngineImpl::GetUser() {
  return user_jid_;
}

XmppReturnStatus
XmppEngineImpl::SetSaslHandler(SaslHandler * sasl_handler) {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  sasl_handler_.reset(sasl_handler);
  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::SetRequestedResource(const std::string & resource) {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  requested_resource_ = resource;

  return XMPP_RETURN_OK;
}

const std::string &
XmppEngineImpl::GetRequestedResource() {
  return requested_resource_;
}

XmppReturnStatus
XmppEngineImpl::AddStanzaHandler(XmppStanzaHandler * stanza_handler,
                                 XmppEngine::HandlerLevel level) {
  if (state_ == STATE_CLOSED)
    return XMPP_RETURN_BADSTATE;

  stanza_handlers_[level]->push_back(stanza_handler);

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::RemoveStanzaHandler(XmppStanzaHandler * stanza_handler) {

  bool found = false;

  for (int level = 0; level < HL_COUNT; level += 1) {
    StanzaHandlerVector::iterator new_end =
      std::remove(stanza_handlers_[level]->begin(),
      stanza_handlers_[level]->end(),
      stanza_handler);

    if (new_end != stanza_handlers_[level]->end()) {
      stanza_handlers_[level]->erase(new_end, stanza_handlers_[level]->end());
      found = true;
    }
  }

  if (!found) {
    return XMPP_RETURN_BADARGUMENT;
  }

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::Connect() {
  if (state_ != STATE_START)
    return XMPP_RETURN_BADSTATE;

  EnterExit ee(this);

  // get the login task started
  state_ = STATE_OPENING;
  if (login_task_.get()) {
    login_task_->IncomingStanza(NULL, false);
    if (login_task_->IsDone())
      login_task_.reset();
  }

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::SendStanza(const XmlElement * element) {
  if (state_ == STATE_CLOSED)
    return XMPP_RETURN_BADSTATE;

  EnterExit ee(this);

  if (login_task_.get()) {
    // still handshaking - then outbound stanzas are queued
    login_task_->OutgoingStanza(element);
  } else {
    // handshake done - send straight through
    InternalSendStanza(element);
  }

  return XMPP_RETURN_OK;
}

XmppReturnStatus
XmppEngineImpl::SendRaw(const std::string & text) {
  if (state_ == STATE_CLOSED || login_task_.get())
    return XMPP_RETURN_BADSTATE;

  EnterExit ee(this);

  (*output_) << text;

  return XMPP_RETURN_OK;
}

std::string
XmppEngineImpl::NextId() {
  std::stringstream ss;
  ss << next_id_++;
  return ss.str();
}

XmppReturnStatus
XmppEngineImpl::Disconnect() {

  if (state_ != STATE_CLOSED) {
    EnterExit ee(this);
    if (state_ == STATE_OPEN)
      *output_ << "</stream:stream>";
    state_ = STATE_CLOSED;
  }

  return XMPP_RETURN_OK;
}

void
XmppEngineImpl::IncomingStart(const XmlElement * pelStart) {
  if (HasError() || raised_reset_)
    return;

  if (login_task_.get()) {
    // start-stream should go to login task
    login_task_->IncomingStanza(pelStart, true);
    if (login_task_->IsDone())
      login_task_.reset();
  }
  else {
    // if not logging in, it's an error to see a start
    SignalError(ERROR_XML);
  }
}

void
XmppEngineImpl::IncomingStanza(const XmlElement * stanza) {
  if (HasError() || raised_reset_)
    return;

  if (stanza->Name() == TQN_STREAM_ERROR) {
    // Explicit XMPP stream error
    SignalStreamError(stanza);
  } else if (login_task_.get()) {
    // Handle login handshake
    login_task_->IncomingStanza(stanza, false);
    if (login_task_->IsDone())
      login_task_.reset();
  } else if (HandleIqResponse(stanza)) {
    // iq is handled by above call
  } else {
    // give every "peek" handler a shot at all stanzas
    for (size_t i = 0; i < stanza_handlers_[HL_PEEK]->size(); i += 1) {
      (*stanza_handlers_[HL_PEEK])[i]->HandleStanza(stanza);
    }

    // give other handlers a shot in precedence order, stopping after handled
    for (int level = HL_SINGLE; level <= HL_ALL; level += 1) {
      for (size_t i = 0; i < stanza_handlers_[level]->size(); i += 1) {
        if ((*stanza_handlers_[level])[i]->HandleStanza(stanza))
          goto Handled;
      }
    }

    // If nobody wants to handle a stanza then send back an error.
    // Only do this for IQ stanzas as messages should probably just be dropped
    // and presence stanzas should certainly be dropped.
    std::string type = stanza->Attr(TQN_TYPE);
    if (stanza->Name() == TQN_IQ && 
        !(type == "error" || type == "result")) {
      SendStanzaError(stanza, XSE_FEATURE_NOT_IMPLEMENTED, STR_EMPTY);
    }
  }
  Handled:
    ; // handled - we're done
}

void
XmppEngineImpl::IncomingEnd(bool isError) {
  if (HasError() || raised_reset_)
    return;

  SignalError(isError ? ERROR_XML : ERROR_DOCUMENT_CLOSED);
}

void
XmppEngineImpl::InternalSendStart(const std::string & to) {
  // send stream-beginning
  // note, we put a \r\n at tne end fo the first line to cause non-XMPP
  // line-oriented servers (e.g., Apache) to reveal themselves more quickly.
  *output_ << "<stream:stream to=\"" << to << "\" version=\"1.0\" "
              "xmlns:stream=\"http://etherx.jabber.org/streams\" "
              "xmlns=\"jabber:client\">\r\n";
}

void
XmppEngineImpl::InternalSendStanza(const XmlElement * element) {
  // It should really never be necessary to set a FROM attribute on a stanza.
  // It is implied by the bind on the stream and if you get it wrong
  // (by flipping from/to on a message?) the server will close the stream.
  ASSERT(!element->HasAttr(TQN_FROM));

  // TODO: consider caching the XmlPrinter
  XmlPrinter::PrintXml(output_.get(), element,
            XMPP_CLIENT_NAMESPACES, XMPP_CLIENT_NAMESPACES_LEN);
}

std::string
XmppEngineImpl::ChooseBestSaslMechanism(const std::vector<std::string> & mechanisms, bool encrypted) {
  return sasl_handler_->ChooseBestSaslMechanism(mechanisms, encrypted);
}

SaslMechanism *
XmppEngineImpl::GetSaslMechanism(const std::string & name) {
  return sasl_handler_->CreateSaslMechanism(name);
}

void
XmppEngineImpl::SignalBound(const Jid & fullJid) {
  if (state_ == STATE_OPENING) {
    bound_jid_ = fullJid;
    state_ = STATE_OPEN;
  }
}

void
XmppEngineImpl::SignalStreamError(const XmlElement * pelStreamError) {
  if (state_ != STATE_CLOSED) {
    stream_error_.reset(new XmlElement(*pelStreamError));
    SignalError(ERROR_STREAM);
  }
}

void
XmppEngineImpl::SignalError(Error errorCode) {
  if (state_ != STATE_CLOSED) {
    error_code_ = errorCode;
    state_ = STATE_CLOSED;
  }
}

bool
XmppEngineImpl::HasError() {
  return error_code_ != ERROR_NONE;
}

void
XmppEngineImpl::StartTls(const std::string & domain) {
  if (output_handler_) {
    output_handler_->StartTls(
      tls_server_domain_.empty() ? domain : tls_server_domain_);
    encrypted_ = true;
  }
}

XmppEngineImpl::EnterExit::EnterExit(XmppEngineImpl* engine)
  : engine_(engine),
  state_(engine->state_),
  error_(engine->error_code_) {
  engine->engine_entered_ += 1;
}

XmppEngineImpl::EnterExit::~EnterExit()  {
 XmppEngineImpl* engine = engine_;

 engine->engine_entered_ -= 1;

 bool closing = (engine->state_ != state_ &&
       engine->state_ == STATE_CLOSED);
 bool flushing = closing || (engine->engine_entered_ == 0);

 if (engine->output_handler_ && flushing) {
   std::string output = engine->output_->str();
   if (output.length() > 0)
     engine->output_handler_->WriteOutput(output.c_str(), output.length());
   engine->output_->str("");

   if (closing) {
     engine->output_handler_->CloseConnection();
     engine->output_handler_ = 0;
   }
 }

 if (engine->engine_entered_)
   return;

 if (engine->raised_reset_) {
   engine->stanzaParser_.Reset();
   engine->raised_reset_ = false;
 }

 if (engine->session_handler_) {
   if (engine->state_ != state_)
     engine->session_handler_->OnStateChange(engine->state_);
     // Note: Handling of OnStateChange(CLOSED) should allow for the
     // deletion of the engine, so no members should be accessed
     // after this line.
 }
}

}
