/* -*- c++ -*-
 *
 * kio_mldonkey.cpp
 *
 * Copyright (C) 2003, 2004 Petter Stokke <ummo@hellokitty.com>
 *
 * 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.
 *
 * 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., 51 Franklin Steet, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

// This is the protocol version that the local code implements.
// Note that this is distinct from what DonkeyProtocol implements.
#define IMPLEMENTED_PROTOCOL_VERSION 26

#include <qcstring.h>
#include <qregexp.h>

#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <time.h>

#include <kdebug.h>
#include <kglobal.h>
#include <kinstance.h>
#include <klocale.h>
#include <kurl.h>
#include <kextendedsocket.h>
#include <ksocks.h>

#include "donkeyprotocol.h"
#include "donkeyhost.h"

#include "kio_mldonkey.h"

using namespace KIO;


MLDonkeyURL::MLDonkeyURL(const KURL& url)
    : original(url)
{
    m_isValid = m_isRoot = m_isHost = m_isPath = m_isFile = false;
    if (url.hasHost() || url.hasUser() || url.hasPass() || url.hasRef() || url.hasSubURL()
	|| !url.queryItems().isEmpty() || !url.hasPath()) return;

    QString path = url.path();

    if (path.isEmpty() || path == "/") {
	m_isValid = m_isRoot = true;
	return;
    }

    QRegExp hre("/([^/]+)/?");
    if (hre.exactMatch(path)) {
	m_isValid = m_isHost = true;
	m_host = hre.cap(1);
	return;
    }

    QRegExp pre("/([^/]+)/([^/]+)/?");
    if (pre.exactMatch(path)) {
	m_isValid = m_isPath = true;
	m_host = pre.cap(1);
	m_path = pre.cap(2);
	return;
    }

    QRegExp fre("/([^/]+)/([^/]+)/(.+)");
    if (fre.exactMatch(path)) {
	m_isValid = m_isFile = true;
	m_host = fre.cap(1);
	m_path = fre.cap(2);
	m_fileName = fre.cap(3);
	return;
    }
}


UDSEntry constructUDSEntry(const QString& name, mode_t type, off_t size, time_t ctime = 0, time_t mtime = 0, time_t atime = 0)
{
    UDSEntry entry;

    UDSAtom a_name;
    a_name.m_uds = KIO::UDS_NAME;
    a_name.m_str = name;
    entry.append(a_name);

    UDSAtom a_type;
    a_type.m_uds = KIO::UDS_FILE_TYPE;
    a_type.m_long = type;
    entry.append(a_type);

    UDSAtom a_size;
    a_size.m_uds = KIO::UDS_SIZE;
    a_size.m_long = size;
    entry.append(a_size);

    UDSAtom a_date;
    a_date.m_uds = KIO::UDS_CREATION_TIME;
    a_date.m_long = ctime;
    entry.append(a_date);

    a_date.m_uds = KIO::UDS_MODIFICATION_TIME;
    a_date.m_long = mtime;
    entry.append(a_date);

    a_date.m_uds = KIO::UDS_ACCESS_TIME;
    a_date.m_long = atime;
    entry.append(a_date);

    return entry;
}

UDSEntry constructUDSEntry(const FileInfo& fi)
{
    return constructUDSEntry(fi.fileName(),
			     S_IFREG,
			     fi.fileSize(),
			     (time_t)fi.fileAge(),
			     (time_t)fi.fileAge(),
			     time(0) - (time_t)fi.fileLastSeen()
	);
}


DonkeyMessage* MLDonkeyProtocol::readMessage()
{
    unsigned char szbuf[4];

    if (KSocks::self()->read(sock->fd(), szbuf, 4) != 4) {
	error(ERR_CONNECTION_BROKEN, QString::null);
	return 0;
    }

    int sz = (int)szbuf[0];
    sz |= ((int)szbuf[1]) << 8;
    sz |= ((int)szbuf[2]) << 16;
    sz |= ((int)szbuf[3]) << 24;

    char* buf = (char*)malloc(sz);
    if (!buf) {
	kdDebug(7166) << "Oops, out of memory!" << endl;
	error(ERR_OUT_OF_MEMORY, QString::null);
	return 0;
    }

    char* p = buf;
    int r, c = 0;
    while (c < sz) {
	r = KSocks::self()->read(sock->fd(), p, sz - c);
	if (r <= 0) {
	    kdDebug(7166) << "Read error." << endl;
	    error(ERR_CONNECTION_BROKEN, QString::null);
	    free(buf);
	    return 0;
	}
	p += r;
	c += r;
    }

    DonkeyMessage* msg = new DonkeyMessage(buf, sz);
    free(buf);

    // kdDebug(7166) << "Received message:" << endl << msg->dumpArray() << endl;

    return msg;
}

bool MLDonkeyProtocol::sendMessage(DonkeyMessage* msg)
{
    unsigned char op[4];
    int sz = msg->size() + 2;
    op[0] = (unsigned char)(sz & 0xff);
    op[1] = (unsigned char)((sz >> 8) & 0xff);
    op[2] = (unsigned char)((sz >> 16) & 0xff);
    op[3] = (unsigned char)((sz >> 24) & 0xff);
    if (KSocks::self()->write(sock->fd(), op, 4) != 4) {
	error(ERR_CONNECTION_BROKEN, QString::null);
	return false;
    }
    op[0] = (unsigned char)(msg->opcode() & 0xff);
    op[1] = (unsigned char)((msg->opcode() >> 8) & 0xff);
    if (KSocks::self()->write(sock->fd(), op, 2) != 2) {
	error(ERR_CONNECTION_BROKEN, QString::null);
	return false;
    }
    if (KSocks::self()->write(sock->fd(), msg->data(), msg->size()) != (signed long int)msg->size()) {
	error(ERR_CONNECTION_BROKEN, QString::null);
	return false;
    }
    return true;
}

bool MLDonkeyProtocol::connectSock(DonkeyHost* host)
{
    kdDebug(7166) << "MLDonkeyProtocol::connectSock(\"" << host->name() << "\")" << endl;

    int on = 1;

    currentHost = QString::null;

    kdDebug(7166) << "Constructing socket..." << endl;

    sock = new KExtendedSocket(host->address(), host->port(), KExtendedSocket::inetSocket);
    if (!sock) {
	error(ERR_OUT_OF_MEMORY, QString::null);
	return false;
    }

    sock->setTimeout(connectTimeout());

    kdDebug(7166) << "Connecting to " << host->address() << ":" << host->port() << "..." << endl;

    if (sock->connect() < 0) {
	if (sock->status() == IO_LookupError) {
	    error(ERR_UNKNOWN_HOST, host->address());
	} else {
	    error(ERR_COULD_NOT_CONNECT, host->address());
	}
	kdDebug(7166) << "Connection failed." << endl;
	delete sock;
	sock = NULL;
	return false;
    }

    int sno = sock->fd();

    if (setsockopt(sno, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on)) == -1) {
	kdDebug() << "Failed to set socket options." << endl;
	delete sock;
	sock = NULL;
	error(ERR_COULD_NOT_CREATE_SOCKET, host->address());
	return false;
    }

    proto = 0;
    bool auth = false;

    kdDebug() << "Socked connected, establishing protocol..." << endl;

    while (!auth) {
	DonkeyMessage *out, *msg = readMessage();
	if (!msg) {
	    disconnectSock();
	    return false;
	}
	switch (msg->opcode()) {
	case DonkeyProtocol::CoreProtocol:
	    proto = msg->readInt32();
	    kdDebug(7166) << "CoreProtocol message, version " << proto << endl;
	    if (proto < MIN_PROTOCOL_VERSION) {
		kdDebug(7166) << "Obsolete protocol version." << endl;
		error(ERR_UPGRADE_REQUIRED, "This MLDonkey is too old!");
		delete msg;
		disconnectSock();
		return false;
	    }
	    out = new DonkeyMessage(DonkeyProtocol::GuiProtocol);
	    out->writeInt32(IMPLEMENTED_PROTOCOL_VERSION);
            if (proto > IMPLEMENTED_PROTOCOL_VERSION)
                proto = IMPLEMENTED_PROTOCOL_VERSION;
	    if (!sendMessage(out)) {
		delete out;
		delete msg;
		disconnectSock();
		return false;
	    }
	    delete out; out = new DonkeyMessage(DonkeyProtocol::GuiExtensions);
	    out->writeInt16(1);
	    out->writeInt32(1);
	    out->writeInt8(1);
	    if (!sendMessage(out)) {
		delete out;
		delete msg;
		disconnectSock();
		return false;
	    }
	    delete out;
	    out = new DonkeyMessage(DonkeyProtocol::Password);
	    out->writeString(host->password());
	    out->writeString(host->username());
	    if (!sendMessage(out)) {
		delete out;
		delete msg;
		disconnectSock();
		return false;
	    }
	    delete out;
	    break;
	case DonkeyProtocol::BadPassword:
	    kdDebug(7166) << "Authentication failure." << endl;
	    delete msg;
	    disconnectSock();
	    error(ERR_ACCESS_DENIED, host->address());
	    return false;
	case DonkeyProtocol::Console:
	    auth = true;
	    kdDebug(7166) << "Authenticated successfully." << endl;
	    break;
	default:
	    break;
	}
	delete msg;
    }

    kdDebug(7166) << "Successfully connected and authenticated." << endl;

    currentHost = host->name();
    return true;
}

void MLDonkeyProtocol::disconnectSock()
{
    if (sock) {
	kdDebug(7166) << "MLDonkeyProtocol::disconnectSock() -> socket closed." << endl;
	delete sock;
	sock = NULL;
	currentHost = QString::null;
    }
}

bool MLDonkeyProtocol::connectDonkey(const QString& hostName)
{
    if (!hostManager->validHostName(hostName)) {
	kdDebug(7166) << "Bad host name \"" << hostName << "\"" << endl;
	error(ERR_DOES_NOT_EXIST, hostName);
	return false;
    }
    if (currentHost == hostName && sock->socketStatus() == KExtendedSocket::connected) {
	kdDebug(7166) << "Reusing connected socket for \"" << currentHost << "\"" << endl;
	return true;
    }
    disconnectSock();
    return connectSock( (DonkeyHost*)hostManager->hostProperties(hostName) );
}

bool MLDonkeyProtocol::readDownloads(const QString& hostName)
{
    kdDebug(7166) << "MLDonkeyProtocol::readDownloads(\"" << hostName << "\")" << endl;

    if (!connectDonkey(hostName)) return false;

    kdDebug(7166) << "readDownloads: connected." << endl;

    bool gotdl = false;

    DonkeyMessage out(DonkeyProtocol::GetDownloadFiles);
    if (!sendMessage(&out)) {
	kdDebug(7166) << "Failed to send GetDownloadFiles message." << endl;
	disconnectSock();
	return false;
    }

    kdDebug(7166) << "readDownloads: waiting for file info." << endl;

    DonkeyMessage* msg;
    while (!gotdl) {
	if (!(msg = readMessage())) {
	    disconnectSock();
	    return false;
	}

	int i, j;

	switch (msg->opcode()) {
	case DonkeyProtocol::DownloadFiles_v4:
	case DonkeyProtocol::DownloadFiles:
	    j = msg->readInt16();
	    for (i=0; i<j; i++) {
		FileInfo fi(msg, proto);
		listEntry(constructUDSEntry(fi), false);
	    }
	    gotdl = true;
	    break;
	default:
	    break;
	}
	delete msg;
    }

    listEntry(UDSEntry(), true);
    disconnectSock();
    return true;
}


bool MLDonkeyProtocol::readComplete(const QString& hostName)
{
    kdDebug(7166) << "MLDonkeyProtocol::readComplete(\"" << hostName << "\")" << endl;

    if (!connectDonkey(hostName)) return false;

    kdDebug(7166) << "readComplete: connected." << endl;

    bool gotdl = false;

    DonkeyMessage out(DonkeyProtocol::GetDownloadedFiles);
    if (!sendMessage(&out)) {
	kdDebug(7166) << "Failed to send GetDownloadedFiles message." << endl;
	disconnectSock();
	return false;
    }

    kdDebug(7166) << "readComplete: waiting for file info." << endl;

    DonkeyMessage* msg;
    while (!gotdl) {
	if (!(msg = readMessage())) {
	    disconnectSock();
	    return false;
	}

	int i, j;

	switch (msg->opcode()) {
	case DonkeyProtocol::DownloadedFiles_v2:
	case DonkeyProtocol::DownloadedFiles:
	    j = msg->readInt16();
	    for (i=0; i<j; i++) {
		FileInfo fi(msg, proto);
		listEntry(constructUDSEntry(fi), false);
	    }
	    gotdl = true;
	    break;
	default:
	    break;
	}
	delete msg;
    }

    listEntry(UDSEntry(), true);
    disconnectSock();
    return true;
}


const FileInfo* MLDonkeyProtocol::statDownload(const MLDonkeyURL& url)
{
    kdDebug(7166) << "MLDonkeyProtocol::statDownload(\"" << url.url().url() << "\")" << endl;

    if (!url.isFile()) {
	error(ERR_DOES_NOT_EXIST, url.url().url());
	return 0;
    }

    if (cachedURL == url.url()) {
	kdDebug(7166) << "statDownload: returned cached instance." << endl;
	return &cachedStat;
    }

    if (!connectDonkey(url.host())) return 0;

    kdDebug(7166) << "statDownload: connected." << endl;

    bool gotdl = false;

    DonkeyMessage out(DonkeyProtocol::GetDownloadFiles);
    if (!sendMessage(&out)) {
	kdDebug(7166) << "Failed to send GetDownloadFiles message." << endl;
	disconnectSock();
	return 0;
    }

    kdDebug(7166) << "statDownload: waiting for file info." << endl;

    DonkeyMessage* msg;
    while (!gotdl) {
	if (!(msg = readMessage())) {
	    disconnectSock();
	    return 0;
	}

	int i, j;

	switch (msg->opcode()) {
	case DonkeyProtocol::DownloadFiles_v4:
	case DonkeyProtocol::DownloadFiles:
	    j = msg->readInt16();
	    for (i=0; i<j; i++) {
		FileInfo fi(msg, proto);
		if (fi.fileName() == url.fileName()) {
		    delete msg;
		    disconnectSock();
		    cachedURL = url.url();
		    cachedStat = fi;
		    return &cachedStat;
		}
	    }
	    gotdl = true;
	    break;
	default:
	    break;
	}
	delete msg;
    }

    disconnectSock();
    error(KIO::ERR_DOES_NOT_EXIST, url.url().url());
    return 0;
}


const FileInfo* MLDonkeyProtocol::statDownloaded(const MLDonkeyURL& url)
{
    kdDebug(7166) << "MLDonkeyProtocol::statDownloaded(\"" << url.url().url() << "\")" << endl;

    if (!url.isFile()) {
	error(ERR_DOES_NOT_EXIST, url.url().url());
	return 0;
    }

    if (cachedURL == url.url()) {
	kdDebug(7166) << "statDownloaded: returned cached instance." << endl;
	return &cachedStat;
    }

    if (!connectDonkey(url.host())) return 0;

    kdDebug(7166) << "statDownloaded: connected." << endl;

    bool gotdl = false;

    DonkeyMessage out(DonkeyProtocol::GetDownloadedFiles);
    if (!sendMessage(&out)) {
	kdDebug(7166) << "Failed to send GetDownloadedFiles message." << endl;
	disconnectSock();
	return 0;
    }

    kdDebug(7166) << "statDownloaded: waiting for file info." << endl;

    DonkeyMessage* msg;
    while (!gotdl) {
	if (!(msg = readMessage())) {
	    disconnectSock();
	    return 0;
	}

	int i, j;

	switch (msg->opcode()) {
	case DonkeyProtocol::DownloadedFiles_v2:
	case DonkeyProtocol::DownloadedFiles:
	    j = msg->readInt16();
	    for (i=0; i<j; i++) {
		FileInfo fi(msg, proto);
		if (fi.fileName() == url.fileName()) {
		    delete msg;
		    disconnectSock();
		    cachedURL = url.url();
		    cachedStat = fi;
		    return &cachedStat;
		}
	    }
	    gotdl = true;
	    break;
	default:
	    break;
	}
	delete msg;
    }

    disconnectSock();
    error(KIO::ERR_DOES_NOT_EXIST, url.url().url());
    return 0;
}


MLDonkeyProtocol::MLDonkeyProtocol(const QCString &pool_socket, const QCString &app_socket)
    : SlaveBase("mldonkey", pool_socket, app_socket)
{
    kdDebug(7166) << "MLDonkeyProtocol::MLDonkeyProtocol()" << endl;
    hostManager = new HostManager();
    sock = 0;
}


MLDonkeyProtocol::~MLDonkeyProtocol()
{
    kdDebug(7166) << "MLDonkeyProtocol::~MLDonkeyProtocol()" << endl;
    disconnectSock();
    delete hostManager;
}


void MLDonkeyProtocol::get(const KURL& url )
{
    kdDebug(7166) << "kio_mldonkey::get(const KURL& url = \"" << url.url() << "\")" << endl ;

    if (url.hasHost()) {
	error(KIO::ERR_UNKNOWN_HOST, url.host());
	return;
    }

    MLDonkeyURL mu(url);
    if (!mu.isValid()) {
	error(KIO::ERR_DOES_NOT_EXIST, url.path());
	return;
    }
    if (!mu.isFile()) {
	error(KIO::ERR_IS_DIRECTORY, url.path());
	return;
    }

    DonkeyHost* host = (DonkeyHost*)hostManager->hostProperties(mu.host());
    const FileInfo* fi = 0;
    if (mu.path() == "downloading")
	fi = statDownload(mu);
    else if (mu.path() == "complete")
	fi = statDownloaded(mu);
    if (!fi) {
	error(KIO::ERR_DOES_NOT_EXIST, url.path());
	return;
    }
    KURL path;
    path.setProtocol("http");
    path.setHost("localhost");
    path.setPort(37435);
    path.setPath("/");
    path.addPath(host->name());
    path.addPath(host->username());
    path.addPath(host->password());
    path.addPath(QString::number(fi->fileNo()));

    kdDebug(7166) << "Redirected path = \"" << path.url() << "\"" << endl;

    redirection(path);
    finished();
}

void MLDonkeyProtocol::stat(const KURL& url )
{
    kdDebug(7166) << "kio_mldonkey::stat(const KURL& url = \"" << url.url() << "\")" << endl ;
    kdDebug(7166) << "Path is \"" << url.path() << "\"" << endl;

    if (url.hasHost()) {
	error(KIO::ERR_UNKNOWN_HOST, url.host());
	return;
    }

    MLDonkeyURL mu(url);
    if (!mu.isValid()) {
	error(KIO::ERR_DOES_NOT_EXIST, url.path());
	return;
    }

    kdDebug(7166) << "Decoded path is \"" << mu.path() << "\"" << endl;

    if (mu.isRoot()) {
	statEntry(constructUDSEntry(QString::null, S_IFDIR, 0));
	finished();
	return;
    }

    if (mu.isHost()) {
	if (!hostManager->validHostName(mu.host())) {
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}
	statEntry(constructUDSEntry(mu.host(), S_IFDIR, 0));
	finished();
	return;
    }

    if (mu.isPath()) {
	if (!hostManager->validHostName(mu.host())) {
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}
	if (mu.path() == "downloading" || mu.path() == "complete") {
	    statEntry(constructUDSEntry(mu.path(), S_IFDIR, 0));
	    finished();
	    return;
	}
	else {
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}
    }

    if (mu.isFile()) {
	if (!hostManager->validHostName(mu.host())) {
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}

	if (mu.path() == "downloading") {
	    const FileInfo* fi = statDownload(mu);
	    if (!fi) return;
	    statEntry(constructUDSEntry(*fi));
	    finished();
	    return;
	} else if (mu.path() == "complete") {
	    const FileInfo* fi = statDownloaded(mu);
	    if (!fi) return;
	    statEntry(constructUDSEntry(*fi));
	    finished();
	    return;
	} else {
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}
   }

    error(KIO::ERR_DOES_NOT_EXIST, url.path());
    return;
}

void MLDonkeyProtocol::listDir(const KURL& url)
{
    kdDebug(7166) << "kio_mldonkey::listDir(const KURL& url = \"" << url.url() << "\")" << endl;
    kdDebug(7166) << "Path is \"" << url.path() << "\"" << endl;

    if (url.hasHost()) {
	error(KIO::ERR_UNKNOWN_HOST, url.host());
	return;
    }

    MLDonkeyURL mu(url);
    if (!mu.isValid()) {
	error(KIO::ERR_DOES_NOT_EXIST, url.path());
	return;
    }
    if (mu.isFile()) {
	error(KIO::ERR_IS_FILE, url.path());
	return;
    }

    if (mu.isRoot()) {
	QStringList hostNames(hostManager->hostList());
	totalSize(hostNames.count());

	QStringList::Iterator it;
	for (it = hostNames.begin(); it != hostNames.end(); ++it)
	    if (hostManager->validHostName(*it))
		listEntry(constructUDSEntry(*it, S_IFDIR, 0), false);

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

    if (mu.isHost()) {
	if (!hostManager->validHostName(mu.host())) {
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}

	listEntry(constructUDSEntry("downloading", S_IFDIR, 0), false);
	listEntry(constructUDSEntry("complete", S_IFDIR, 0), false);
	listEntry(UDSEntry(), true);
	finished();
	return;
    }

    if (mu.isPath()) {
	if (!hostManager->validHostName(mu.host())) {
	    kdDebug(7166) << "Bad host name \"" << mu.host() << "\"" << endl;
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}
	if (mu.path() == "downloading") {
	    kdDebug(7166) << "Reading path \"" << mu.path() << "\"..." << endl;
	    if (!readDownloads(mu.host()))
		return;
	    finished();
	    return;
	}
	if (mu.path() == "complete") {
	    kdDebug(7166) << "Reading path \"" << mu.path() << "\"..." << endl;
	    if (!readComplete(mu.host()))
		return;
	    finished();
	    return;
	}
	else {
	    kdDebug(7166) << "Unknown path \"" << mu.host() << "\" : \"" << mu.path() << "\"" << endl;
	    error(KIO::ERR_DOES_NOT_EXIST, url.path());
	    return;
	}
    }

    error(KIO::ERR_DOES_NOT_EXIST, url.path());
}



extern "C"
{
    int kdemain(int argc, char **argv)
    {
        KInstance instance( "kio_mldonkey" );

        kdDebug(7166) << "*** Starting kio_mldonkey " << endl;

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

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

        kdDebug(7166) << "*** kio_mldonkey Done" << endl;
        return 0;
    }
}
