/***************************************************************************
                          sql.cpp  -  description
                             -------------------
    begin                : Sam Nov 24 16:38:12 CET 2001
    copyright            : (C) 2001 by Harald Fernengel
    email                : harry@kdevelop.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 <qcstring.h>
#include <qsocket.h>
#include <qdatetime.h>
#include <qbitarray.h>
#include <qsqldatabase.h>
#include <qsqlquery.h>
#include <qsqlcursor.h>
#include <qsqlrecord.h>
#include <qsqlfield.h>
#include <qregexp.h>

#include <stdlib.h>
#include <math.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netdb.h>

#include <kapplication.h>
#include <kdebug.h>
#include <kmessagebox.h>
#include <kinstance.h>
#include <kglobal.h>
#include <kstandarddirs.h>
#include <klocale.h>
#include <kurl.h>
#include <ksock.h>

#include "sql.h"
#include "sqloutputfactory.h"
#include "sqloutputiface.h"

#if 0
#include "qfile.h"
// for debugging only. Since we have no stdout/err, we dump
// all debug messages into a file
void myMessageHandler( QtMsgType tp, const char* msg )
{
  QFile dbg( "/tmp/sql_io.out" );
  dbg.open( IO_WriteOnly | IO_Append );
  dbg.writeBlock( msg, strlen( msg ) );
  dbg.writeBlock( "\n", 1 );
  dbg.close();

  if ( tp == QtFatalMsg )
    abort();
}
#endif

using namespace KIO;

extern "C" { KDE_EXPORT int kdemain(int argc, char **argv); }

int kdemain(int argc, char **argv)
{
#if 0
  qInstallMsgHandler( myMessageHandler );
#endif
  KApplication app(argc, argv, "kio_sql", false, false);

  if (argc != 4)
    {
      kdDebug(7111) << "Usage: kio_sql  protocol domain-socket1 domain-socket2" << endl;
      exit(-1);
    }

  SqlProtocol slave(argv[2], argv[3]);
  slave.dispatchLoop();

  return 0;
}

SqlProtocol::SqlProtocol(const QCString &pool_socket, const QCString &app_socket)
  : SlaveBase("kio_sql", pool_socket, app_socket)
{
}
/* ---------------------------------------------------------------------------------- */

/** stupid little helper function that
    puts the query of an URL into a QMap */
QUrlQueryMap SqlProtocol::queryToMap( const KURL& url )
{
  QUrlQueryMap map;

  // split the query at the '&' sign, ignoring the first questionmark
  QStringList items = QStringList::split( '&', url.query().mid( 1 ) );
  for ( QStringList::ConstIterator it = items.begin(); it != items.end(); ++it ) {
    if ( (*it).contains( "=" ) == 0 ) {
      kdDebug(7111) << "queryToMap: malformed query" << endl;
      continue;
    }
    QString key = (*it).section( '=', 0, 0 );
    QString val = (*it).section( '=', 1, 1 );
    val = val.replace( "+", " " );
    map[ key ] = KURL::decode_string( val );
  }

  return map;
}

QString SqlProtocol::mapToQuery( const QUrlQueryMap& map )
{
  QString query("?");
  for ( QUrlQueryMap::ConstIterator it = map.begin(); it != map.end(); ++it ) {
    if ( !it.data().isEmpty() ) {
      query += KURL::encode_string_no_slash( it.key() );
      query += "=";
      query += KURL::encode_string_no_slash( it.data() );
      query += "&";
    }
  }
  query.truncate( query.length() - 1 );
  return query;
}

// returns only the field/value pairs from the url query
static QStringList fieldsFromMap( const QUrlQueryMap& map )
{
  QStringList res;
  for ( QUrlQueryMap::ConstIterator it = map.begin(); it != map.end(); ++it ) {
    if ( it.key().startsWith( "_field" ) ) {
      res << it.key() + "=" + it.data();
    }
  }
  return res;
}

SqlProtocol::~SqlProtocol()
{
}

/* ---------------------------------------------------------------------------------- */
void SqlProtocol::put( const KURL&, int, bool, bool )
{
  qDebug( "kio_sql::put" );
}

// holds the connection info based on a url
class ConInfo
{
public:
  ConInfo(KIO::SlaveBase* d, const KURL &url);
  bool checkAuth(KIO::AuthInfo& auth);
  QSqlDatabase* db();

  KURL url;
  QString path;
  QString user;
  QString passwd;
  QString query;
  QString outputFormat;
  QString action;
  QString plugin;
  QString database;
  QString table;
  QString field;
  QString host;
  int port;
  int limit, limitOffset;
  QUrlQueryMap queryMap;

  bool redirect;

private:
  QSqlDatabase* _db;
  QString conName;
  KIO::SlaveBase* q;

  static int conID;
};

int ConInfo::conID = 0;

ConInfo::ConInfo( KIO::SlaveBase* d, const KURL &surl ):
    url(surl), port(-1), limit(0), limitOffset(0), redirect(false), _db(0), q(d)
{
  bool isOK = true;
  url.cleanPath(false);

  queryMap = SqlProtocol::queryToMap( url );

  host = url.host();
  port = url.port();
  if ( port == 0 ) {
    if ( queryMap.contains( "port" ) ) {
      port = queryMap[ "port" ].toInt();
      redirect = true;
    } else {
      // if no port is given, we assume the default
      port = -1;
    }
  }

  path = url.path();
  user = url.user();
  passwd = url.pass();
  query = queryMap[ "query" ];
  outputFormat = queryMap[ "output" ];
  action = queryMap[ "action" ];

  // check whether LIMIT is requested
  limit = queryMap[ "limit" ].toInt( &isOK );
  if ( !isOK )
    limit = -1;
  limitOffset = queryMap[ "offset" ].toInt( &isOK );
  if ( !isOK )
    limitOffset = -1;

  QStringList l = QStringList::split("/", path);
  if (l.count() > 0) {
    plugin = l[0]; // the first part of the path is the plugin
  }
  if (l.count() > 1) {
    database = l[1]; //the second is the database
  }
  if (l.count() > 2) {
    table = l[2]; // tablename
  }
  if (l.count() > 3) {
    field = l[3]; // fieldname
  }

  if ( database.isEmpty() && queryMap.contains( "database" ) ) {
    database = queryMap[ "database" ];
    redirect = true;
  }
  if ( host.isEmpty() && queryMap.contains( "host" ) ) {
    host = queryMap[ "host" ];
    if ( host.contains( ":" ) ) {
      QString prt = host.mid( host.findRev( ":" ) + 1 );
      bool isOK;
      int iprt = prt.toInt( &isOK );
      if ( isOK && iprt >= 0 ) {
        port = iprt;
      }
      host.truncate( host.find( ":" ) );
    }
    redirect = true;
  }
  if ( table.isEmpty()  && queryMap.contains( "table" ) ) {
    table = queryMap[ "table" ];
    redirect = true;
  }
  if ( plugin.isEmpty() && queryMap.contains( "plugin" ) ) {
    plugin = queryMap[ "plugin" ];
    redirect = true;
  }
  if ( url.path().right( 1 ) != "/" ) {
    redirect = true;
  }
  queryMap.erase( "database" );
  queryMap.erase( "host" );
  queryMap.erase( "table" );
  queryMap.erase( "plugin" );
  queryMap.erase( "port" );
}

bool ConInfo::checkAuth(KIO::AuthInfo& auth)
{
    KURL authUrl = KURL( QString( "sql://%1:%2" ).arg( host ).arg( port ) );
    auth.url = authUrl;
    auth.username = user;
    auth.password = passwd;
    auth.realmValue = authUrl.url() + "/" + plugin + "/" + user;
    auth.verifyPath = false;

    if ( !q->checkCachedAuthentication( auth ) ) {
      auth.prompt = i18n("Please enter username and password for %1").arg( host );
      if ( !q->openPassDlg( auth ) ) {
        q->error( ERR_ACCESS_DENIED, path );
        return false;
      }
    }
    user = auth.username;
    passwd = auth.password;
    return true;
}

QSqlDatabase* ConInfo::db()
{
    if (_db)
        return _db;

    QString conName = "SQL_IO_SLAVE_" + QString::number(++conID);
    QSqlDatabase* _db = QSqlDatabase::addDatabase( plugin, conName );

    _db->setDatabaseName( database );
    _db->setHostName( host );
    _db->setPort( port );
    _db->setUserName( user );
    _db->setPassword( passwd );

    return _db;
}

void SqlProtocol::redirect(const ConInfo& info)
{
  KURL newUrl;
  newUrl.setProtocol( info.url.protocol() );
  newUrl.setHost( info.host );
  QString newPath = "/" + info.plugin + "/" + info.database;
  if ( !info.table.isEmpty() )
    newPath += "/" + info.table;
  newPath += "/";
  newUrl.setPath( newPath );
  if ( info.port > 0 )
    newUrl.setPort( info.port );
  newUrl.setQuery( mapToQuery( info.queryMap ) );

  redirection( newUrl );
  finished();
}

void SqlProtocol::get(const KURL& url )
{

#if 0
// just ignore the following code ;)
  qDebug( "kio_sql::get(const KURL& url): " + url.prettyURL() );
  QString dbgStr;
  for ( MetaData::iterator it = mIncomingMetaData.begin(); it != mIncomingMetaData.end(); ++it ) {
    dbgStr += "MetaData: " + it.key() + ": " + it.data() + "\n";
  }
  qDebug( dbgStr );

/*
  dataReq();
  QByteArray bar;
  readData( bar );
  qDebug( bar );
*/
#endif

  ConInfo info(this, url);

  // if no plugin is given we do not need to redirect
  // since the URL would be invalid anyways.
  if ( !info.plugin.isEmpty() && info.redirect ) {
    redirect( info );
    return;
  }

  // return the data
  QString theData;

  SqlOutputIface* output = SqlOutputFactory::getOutputIface( info.outputFormat.local8Bit(), url );
  mimeType( output->mimeType() );

  infoMessage( i18n("Querying %1:%2 ...").arg( info.host ).arg( info.port ) );

  if ( info.plugin.isEmpty() ) {
    theData = output->showStartPage();
    if ( theData.isEmpty() )
      theData = output->lastError();
  } else if ( !QSqlDatabase::drivers().contains( info.plugin ) ) {
    output->error( output->pluginNotFoundStr( info.plugin ) );
    theData = output->lastError();
    errorPage();
  } else {

    KIO::AuthInfo auth;
    if (!info.checkAuth(auth)) {
        finished();
        return;
    }

    QSqlDatabase* db = info.db();
    output->setDatabase( db );

    if ( !db->open() ) {
      output->error( output->cannotOpenDatabase(), db->lastError() );
      theData = output->lastError();
      errorPage();
    } else {
      QString out;
      if ( info.action == "insert" ) {
        QSqlError err;
        infoMessage( i18n("Inserting data") );
        int rows = insertData( info.queryMap, info.table, db, &err );
        if ( rows <= 0 ) {
          output->error( i18n("Could not insert data"), err );
          out = output->lastError();
        } else {
          out = output->success( i18n("Data successfully inserted.") );
        }
      } else if ( info.action == "update" ) {
        QSqlError err;
        infoMessage( i18n("Updating data") );
        int rows = updateData( info.queryMap, info.table, db, &err );
        if ( rows <= 0 ) {
          output->error( i18n("Could not update data"), err );
          out = output->lastError();
        } else {
          out = output->success( i18n("Data successfully updated.") );
        }
      } else if ( info.action == "delete" ) {
        QSqlError err;
        infoMessage( i18n("Deleting data") );
        int rows = deleteData( info.queryMap, info.table, db, &err );
        if ( rows <= 0 ) {
          output->error( i18n("Could not delete data"), err );
          out = output->lastError();
        } else {
          out = output->success( i18n("Data successfully deleted.") );
        }
      } else if ( ( info.action == "showQuery" ) && ( !info.query.isEmpty() ) ) {
        infoMessage( i18n("Showing contents of query") );
        out = output->showQuery( info.query );
      } else if ( ( info.action == "showTable" ) && ( !info.table.isEmpty() ) ) {
        infoMessage( i18n("Displaying table") );
        out = output->showTable( info.table, info.limit, info.limitOffset );
      } else if ( ( info.action == "showMetadata" ) && ( !info.table.isEmpty() ) ) {
        infoMessage( i18n("Displaying metadata") );
        out = output->showMetaData( info.table );
      } else if ( !info.action.isEmpty() ) {
        infoMessage( i18n( "showing %1" ).arg( info.table ) );
        out = output->showSpecial( info.action, info.table, fieldsFromMap( info.queryMap ), info.limit, info.limitOffset );
      } else if ( info.table.isEmpty() ) {
        // Show a list of tables if no table is given
        infoMessage( i18n("Showing tables") );
        out = output->showTables();
      } else {
        infoMessage( i18n("Displaying table") );
        out = output->showTable( info.table, info.limit, info.limitOffset );
      }

      if ( out == QString::null ) {
        theData = output->lastError();
        errorPage();
      } else {
        theData += out;
      }

      cacheAuthentication( auth );
    }

    db->close();
    output->setDatabase( 0 );
    QSqlDatabase::removeDatabase( QSqlDatabase::defaultConnection );
  }

  delete output;
  output = 0;

  data( theData.local8Bit() );
  finished();
}

static void map2record( const QUrlQueryMap& map, const QString& fieldPrefix, QSqlRecord* rec )
{
  if ( !rec )
    return;

  int fpLen = fieldPrefix.length();
  for ( QUrlQueryMap::ConstIterator it = map.begin(); it != map.end(); ++it ) {
    if ( it.key().startsWith( fieldPrefix ) ) {
      rec->setValue( it.key().mid( fpLen ), it.data() );
    }
  }
}

int SqlProtocol::insertData( const QUrlQueryMap& map, const QString& tableName, QSqlDatabase* db, QSqlError* err )
{
  QSqlCursor cur( tableName, TRUE, db );
  map2record( map, "_field", cur.primeInsert() );
  int arows = cur.insert();
  if ( arows <= 0 ) {
    if ( err )
      *err = cur.lastError();
  }

  return arows;
}

int SqlProtocol::updateData( const QUrlQueryMap& map, const QString& tableName, QSqlDatabase* db, QSqlError* err )
{
  QSqlCursor cur( tableName, TRUE, db );

  // get the primary key fields
  QStringList pkFields;
  for ( QUrlQueryMap::ConstIterator it = map.begin(); it != map.end(); ++it ) {
    if ( it.key().startsWith( "_pkfield" ) )
      pkFields << it.key().mid( 8 );
  }

  // set the primary key values
  QSqlIndex idx = cur.index( pkFields );
  map2record( map, "_pkfield", (QSqlRecord*)&cur );
  cur.select( idx, idx );

  // goto the record we want to change
  if ( !cur.next() ) {
    if ( err )
      *err = cur.lastError();
    return 0;
  }

  // update the data
  map2record( map, "_field", cur.primeUpdate() );
  int arows = cur.update();
  if ( arows <= 0 ) {
    if ( err )
      *err = cur.lastError();
  }

  return arows;
}

int SqlProtocol::deleteData( const QUrlQueryMap& map, const QString& tableName, QSqlDatabase* db, QSqlError* err )
{
  QSqlCursor cur( tableName, TRUE, db );

  // get the primary key fields
  QStringList pkFields;
  for ( QUrlQueryMap::ConstIterator it = map.begin(); it != map.end(); ++it ) {
    if ( it.key().startsWith( "_pkfield" ) )
      pkFields << it.key().mid( 8 );
  }

  // set the primary key values
  QSqlIndex idx = cur.index( pkFields );
  map2record( map, "_pkfield", (QSqlRecord*)&cur );
  cur.select( idx, idx );

  // goto the record we want to change
  if ( !cur.next() ) {
    if ( err )
      *err = cur.lastError();
    return 0;
  }

  // update the data
  map2record( map, "_field", cur.primeDelete() );
  int arows = cur.del();
  if ( arows <= 0 ) {
    if ( err )
      *err = cur.lastError();
  }

  return arows;
}

/* ---------------------------------------------------------------------------------- */


void SqlProtocol::mimetype(const KURL & /*url*/)
{
  mimeType("text/html");
  finished();
}

static void stringListToDirs( KIO::SlaveBase *d, const QStringList& list )
{
  for (QStringList::ConstIterator it = list.begin(); it != list.end(); ++it) {
    UDSEntry entry;
    UDSAtom atom;
    atom.m_uds = UDS_NAME;
    atom.m_str = *it;
    entry.append(atom);
    atom.m_uds = UDS_FILE_TYPE;
    atom.m_long = S_IFDIR;
    entry.append(atom);
    d->listEntry(entry, false);
  }
}

void SqlProtocol::stat( const KURL& url )
{
  qDebug( "stat: %s", url.url().ascii() );

//  UDSEntry entry;
//  statEntry( entry );

  finished();
}

void SqlProtocol::listDir( const KURL &url )
{
  qDebug( "kio_sql::listDir for '%s'", url.url().ascii() );

  ConInfo info(this, url);
  int levelCount = info.url.directory().contains("/");
  if (info.url.directory().endsWith("/"))
      --levelCount;
  qDebug("listDir: '%s', count: %d", info.url.directory().ascii(), levelCount);

  if (url.host().isEmpty() || levelCount > 3) {
    error(ERR_MALFORMED_URL, i18n("%1 (usage: sql://[USER:PASSWORD@]HOST/PLUGINNAME/DATABASENAME)").arg(url.prettyURL()));
    finished();
    return;
  }

  // host given, but no plugin
  if (info.plugin.isEmpty() && levelCount == 0) {
    // list all available drivers
    stringListToDirs(this, QSqlDatabase::drivers());
    listEntry(UDSEntry(), true);
    finished();
    return;
  }

  if ( !QSqlDatabase::drivers().contains( info.plugin ) ) {
    error(ERR_MALFORMED_URL, i18n("%1 (No such plugin. Available plugins: %2)").arg(info.plugin).arg(QSqlDatabase::drivers().join(" ")));
    finished();
    return;
  }

  if (info.database.isEmpty()) {
    // find a way to find out existing databases?
    finished();
    return;
  }

  //authorize...
  KIO::AuthInfo auth;
  if (!info.checkAuth(auth)) {
    finished();
    return;
  }

  QSqlDatabase *db = info.db();
  if (!db->open()) {
    error( ERR_CANNOT_OPEN_FOR_READING, i18n("%1\nThe database reported an error:\n %2\n %3" ).arg(url.prettyURL()).arg(db->lastError().driverText()).arg(db->lastError().databaseText() ));
    finished();
    return;
  }

  cacheAuthentication( auth );

  if (!info.database.isEmpty() && levelCount == 1) {
    // list all tables
    stringListToDirs(this, db->tables());
  } else if (!info.table.isEmpty() && levelCount == 2) {
    // list all fields
    const QSqlRecord rec(db->record(info.table));
    if (rec.isEmpty()) {
      error(ERR_CANNOT_OPEN_FOR_READING, i18n("%1 (No such table)").arg(info.table));
      finished();
      return;
    }
    for (int i = 0; i < (int)rec.count(); ++i) {
      UDSEntry entry;
      UDSAtom atom;
      atom.m_uds = UDS_NAME;
      atom.m_str = rec.fieldName(i);
      entry.append(atom);
      atom.m_uds = UDS_FILE_TYPE;
      atom.m_long = S_IFDIR;
      entry.append(atom);
      listEntry(entry, false);
    }
    UDSEntry entry;
    UDSAtom atom;
    atom.m_uds = UDS_NAME;
    atom.m_str = "index.html";
    entry.append(atom);
    atom.m_uds = UDS_FILE_TYPE;
    atom.m_long = S_IFREG;
    entry.append(atom);
    listEntry(entry, false);
  } else if (levelCount == 3) {
    // field attributes
    const QSqlRecordInfo rinf = db->recordInfo(info.table);
    if (!rinf.contains(info.field)) {
      error(ERR_CANNOT_OPEN_FOR_READING, i18n("%1.%2 (No such field)").arg(info.table).arg(info.field));
      finished();
      return;
    }
    const QSqlFieldInfo inf = rinf.find(info.field);
  } else {
    error(ERR_MALFORMED_URL, i18n("%1 (Usage: sql://[USER:PASSWORD@]HOST/PLUGINNAME/DATABASENAME)").arg(url.prettyURL()));
    finished();
    return;
  }

  listEntry( UDSEntry(), true );
  finished();
}

void SqlProtocol::special( const QByteArray& ba )
{
  qDebug( "Special: %s", QCString( ba ).data() );
  finished();
}

/* --------------------------------------------------------------------------- */

