/**************************************************************************
*   Copyright (C) 2003 - 2004 by Frerich Raabe <raabe@kde.org>            *
*                                Tobias Koenig <tokoe@kde.org>            *
*                                                                         *
*   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     *
*   (at your option) any later version.                                   *
***************************************************************************/

#include <QFile>
#include <QDateTime>
//Added by qt3to4:
#include <QByteArray>

#include <kdebug.h>
#include <kio/job.h>
#include <klocale.h>
#include <kcodecs.h>

#include "debugdialog.h"
#include "xmlrpciface.h"

using namespace KXMLRPC;

namespace KXMLRPC
{
  class Result
  {
      friend class Query;
    public:
      Result()
      { }

      bool success() const
      {
        return m_success;
      }
      int errorCode() const
      {
        return m_errorCode;
      }
      QString errorString() const
      {
        return m_errorString;
      }
      QList<QVariant> data() const
      {
        return m_data;
      }

    private:
      bool m_success;
      int m_errorCode;
      QString m_errorString;
      QList<QVariant> m_data;
  };
}

Query *Query::create( const QVariant &id, QObject *parent, const char *name )
{
  return new Query( id, parent, name );
}

void Query::call( const QString &server, const QString &method,
                  const QList<QVariant> &args, const QString &userAgent )
{
  const QString xmlMarkup = markupCall( method, args );
  DebugDialog::addMessage( xmlMarkup, DebugDialog::Output );

  QByteArray postData;
  QDataStream stream( &postData, QIODevice::WriteOnly );
  stream.writeRawData( xmlMarkup.toUtf8(), xmlMarkup.toUtf8().length() );

  KIO::TransferJob *job = KIO::http_post( KUrl( server ), postData, KIO::HideProgressInfo );
  if ( !job ) {
    kWarning() <<"Unable to create KIO job for" << server;
    return;
  }
  job->addMetaData( "UserAgent", userAgent );
  job->addMetaData( "content-type", "Content-Type: application/xml; charset=utf-8" );
  job->addMetaData( "ConnectTimeout", "50" );

  connect( job, SIGNAL( data( KIO::Job *, const QByteArray & ) ),
           this, SLOT( slotData( KIO::Job *, const QByteArray & ) ) );
  connect( job, SIGNAL( result( KJob * ) ),
           this, SLOT( slotResult( KJob * ) ) );

  m_pendingJobs.append( job );
}

void Query::slotData( KIO::Job *, const QByteArray &data )
{
  unsigned int oldSize = m_buffer.size();
  m_buffer.resize( oldSize + data.size() );
  memcpy( m_buffer.data() + oldSize, data.data(), data.size() );
}

void Query::slotResult( KJob *job )
{
  m_pendingJobs.removeAll( static_cast<KIO::Job*>(job) );

  if ( job->error() != 0 )
  {
    emit fault( job->error(), job->errorString(), m_id );
    emit finished( this );
    return ;
  }

  QString data = QString::fromUtf8( m_buffer.data(), m_buffer.size() );
  DebugDialog::addMessage( data, DebugDialog::Input );

  QDomDocument doc;
  QString errMsg;
  int errLine, errCol;
  if ( !doc.setContent( data, false, &errMsg, &errLine, &errCol  ) )
  {
    emit fault( -1, i18n( "Received invalid XML markup: %1 at %2:%3" ,
                          errMsg, errLine, errCol ), m_id );
    emit finished( this );
    return ;
  }

  m_buffer.truncate( 0 );

  if ( isMessageResponse( doc ) )
    emit message( parseMessageResponse( doc ).data(), m_id );
  else if ( isFaultResponse( doc ) )
  {
    emit fault( parseFaultResponse( doc ).errorCode(), parseFaultResponse( doc ).errorString(), m_id );
  }
  else
  {
    emit fault( 1, i18n( "Unknown type of XML markup received" ), m_id );
  }

  emit finished( this );
}

bool Query::isMessageResponse( const QDomDocument &doc ) const
{
  return doc.documentElement().firstChild().toElement().tagName().toLower() == "params";
}

Result Query::parseMessageResponse( const QDomDocument &doc ) const
{
  Result response;
  response.m_success = true;

  QDomNode paramNode = doc.documentElement().firstChild().firstChild();
  while ( !paramNode.isNull() )
  {
    response.m_data << demarshal( paramNode.firstChild().toElement() );
    paramNode = paramNode.nextSibling();
  }

  return response;
}

bool Query::isFaultResponse( const QDomDocument &doc ) const
{
  return doc.documentElement().firstChild().toElement().tagName().toLower() == "fault";
}

Result Query::parseFaultResponse( const QDomDocument &doc ) const
{
  Result response;
  response.m_success = false;

  QDomNode errorNode = doc.documentElement().firstChild().firstChild();
  const QVariant errorVariant = demarshal( errorNode.toElement() );
  response.m_errorCode = errorVariant.toMap() [ "faultCode" ].toInt();
  response.m_errorString = errorVariant.toMap() [ "faultString" ].toString();

  return response;
}

QString Query::markupCall( const QString &cmd,
                           const QList<QVariant> &args ) const
{
  QString markup = "<?xml version=\"1.0\" ?>\r\n<methodCall>\r\n";

  markup += "<methodName>" + cmd + "</methodName>\r\n";

  if ( !args.isEmpty() )
  {
    markup += "<params>\r\n";
    QList<QVariant>::ConstIterator it = args.constBegin();
    QList<QVariant>::ConstIterator end = args.constEnd();
    for ( ; it != end; ++it )
      markup += "<param>\r\n" + marshal( *it ) + "</param>\r\n";
    markup += "</params>\r\n";
  }

  markup += "</methodCall>\r\n";

  return markup;
}

QString Query::marshal( const QVariant &arg ) const
{
  switch ( arg.type() )
  {
      case QVariant::String:
      {
        QString result = arg.toString();
        result = result.replace( "&", "&amp;" );
        result = result.replace( "\"", "&quot;" );
        result = result.replace( "<", "&lt;" );
        result = result.replace( ">", "&gt;" );
        return "<value><string>" + result + "</string></value>\r\n";
      }
      case QVariant::Int:
        return "<value><int>" + QString::number( arg.toInt() ) + "</int></value>\r\n";
      case QVariant::Double:
        return "<value><double>" + QString::number( arg.toDouble() ) + "</double></value>\r\n";
      case QVariant::Bool:
      {
        QString markup = "<value><boolean>";
        markup += arg.toBool() ? "1" : "0";
        markup += "</boolean></value>\r\n";
        return markup;
      }
      case QVariant::ByteArray:
        return "<value><base64>" + KCodecs::base64Encode( arg.toByteArray() ) + "</base64></value>\r\n";
      case QVariant::DateTime:
        return "<value><datetime.iso8601>" + arg.toDateTime().toString( Qt::ISODate ) + "</datetime.iso8601></value>\r\n";
      case QVariant::List:
      {
        QString markup = "<value><array><data>\r\n";
        const QList<QVariant> args = arg.toList();
        QList<QVariant>::ConstIterator it = args.constBegin();
        QList<QVariant>::ConstIterator end = args.constEnd();
        for ( ; it != end; ++it )
          markup += marshal( *it );
        markup += "</data></array></value>\r\n";
        return markup;
      }
      case QVariant::Map:
      {
        QString markup = "<value><struct>\r\n";
        QMap<QString, QVariant> map = arg.toMap();
        QMap<QString, QVariant>::ConstIterator it = map.constBegin();
        QMap<QString, QVariant>::ConstIterator end = map.constEnd();
        for ( ; it != end; ++it )
        {
          markup += "<member>\r\n";
          markup += "<name>" + it.key() + "</name>\r\n";
          markup += marshal( it.value() );
          markup += "</member>\r\n";
        }
        markup += "</struct></value>\r\n";
        return markup;
      }
      default:
      kWarning() <<"Failed to marshal unknown variant type:" << arg.type();
  };
  return QString();
}

QVariant Query::demarshal( const QDomElement &elem ) const
{
  Q_ASSERT( elem.tagName().toLower() == "value" );

  const QDomElement typeElement = elem.firstChild().toElement();
  const QString typeName = typeElement.tagName().toLower();

  if ( typeName == "string" )
    return QVariant( typeElement.text() );
  else if ( typeName == "i4" || typeName == "int" )
    return QVariant( typeElement.text().toInt() );
  else if ( typeName == "double" )
    return QVariant( typeElement.text().toDouble() );
  else if ( typeName == "boolean" )
  {
    if ( typeElement.text().toLower() == "true" || typeElement.text() == "1" )
      return QVariant( true );
    else
      return QVariant( false );
  }
  else if ( typeName == "base64" )
    return QVariant( KCodecs::base64Decode( typeElement.text().toLatin1() ) );
  else if ( typeName == "datetime" || typeName == "datetime.iso8601" )
    return QVariant( QDateTime::fromString( typeElement.text(), Qt::ISODate ) );
  else if ( typeName == "array" )
  {
    QList<QVariant> values;
    QDomNode valueNode = typeElement.firstChild().firstChild();
    while ( !valueNode.isNull() )
    {
      values << demarshal( valueNode.toElement() );
      valueNode = valueNode.nextSibling();
    }
    return QVariant( values );
  }
  else if ( typeName == "struct" )
  {
    QMap<QString, QVariant> map;
    QDomNode memberNode = typeElement.firstChild();
    while ( !memberNode.isNull() )
    {
      const QString key = memberNode.toElement().elementsByTagName( "name" ).item( 0 ).toElement().text();
      const QVariant data = demarshal( memberNode.toElement().elementsByTagName( "value" ).item( 0 ).toElement() );
      map[ key ] = data;
      memberNode = memberNode.nextSibling();
    }
    return QVariant( map );
  }
  else
    kWarning() <<"Cannot demarshal unknown type" << typeName;

  return QVariant();
}

Query::Query( const QVariant &id, QObject *parent, const char *name )
  : QObject( parent ), m_id( id )
{
  setObjectName( name );
}

Query::~Query()
{
  QList<KIO::Job*>::Iterator it;
  for ( it = m_pendingJobs.begin(); it != m_pendingJobs.end(); ++it )
    (*it)->kill();
}

Server::Server( const KUrl &url, QObject *parent, const char *name )
    : QObject( parent )
{
  setObjectName( name );
  if ( url.isValid() )
    m_url = url;

  m_userAgent = "KDE XMLRPC resources";

  DebugDialog::init();
}

Server::~Server()
{
  QList<Query*>::Iterator it;
  for ( it = mPendingQueries.begin(); it !=mPendingQueries.end(); ++it )
    (*it)->deleteLater();

  mPendingQueries.clear();
}

void Server::queryFinished( Query *query )
{
  mPendingQueries.removeAll( query );
  query->deleteLater();
}

void Server::setUrl( const KUrl &url )
{
  m_url = url.isValid() ? url : KUrl();
}

void Server::call( const QString &method, const QList<QVariant> &args,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot, const QVariant &id )
{
  if ( m_url.isEmpty() )
    kWarning() <<"Cannot execute call to" << method <<": empty server URL";

  Query *query = Query::create( id, this );
  connect( query, SIGNAL( message( const QList<QVariant> &, const QVariant& ) ), msgObj, messageSlot );
  connect( query, SIGNAL( fault( int, const QString&, const QVariant& ) ), faultObj, faultSlot );
  connect( query, SIGNAL( finished( Query* ) ), this, SLOT( queryFinished( Query* ) ) );
  mPendingQueries.append( query );

  query->call( m_url.url(), method, args, m_userAgent );
}

void Server::call( const QString &method, const QVariant &arg,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  args << arg ;
  call( method, args, msgObj, messageSlot, faultObj, faultSlot, id );
}

void Server::call( const QString &method, int arg,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  args << QVariant( arg );
  call( method, args, msgObj, messageSlot, faultObj, faultSlot, id );
}

void Server::call( const QString &method, bool arg,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  args << QVariant( arg );
  call( method, args, msgObj, messageSlot, faultObj, faultSlot, id );
}

void Server::call( const QString &method, double arg ,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  args << QVariant( arg );
  call( method, args, msgObj, messageSlot, faultObj, faultSlot, id );
}

void Server::call( const QString &method, const QString &arg ,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  args << QVariant( arg );
  call( method, args, msgObj, messageSlot, faultObj, faultSlot, id );
}

void Server::call( const QString &method, const QByteArray &arg ,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  args << QVariant( arg );
  call( method, args, faultObj, faultSlot, msgObj, messageSlot, id );
}

void Server::call( const QString &method, const QDateTime &arg,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  args << QVariant( arg );
  call( method, args, msgObj, messageSlot, faultObj, faultSlot, id );
}

void Server::call( const QString &method, const QStringList &arg,
                   QObject* msgObj, const char* messageSlot,
                   QObject* faultObj, const char* faultSlot,
                   const QVariant &id )
{
  QList<QVariant> args;
  QStringList::ConstIterator it = arg.constBegin();
  QStringList::ConstIterator end = arg.constEnd();
  for ( ; it != end; ++it )
    args << QVariant( *it );
  call( method, args, msgObj, messageSlot, faultObj, faultSlot, id );
}

#include "xmlrpciface.moc"
