/*
 * filetransfer.cpp - File Transfer
 * Copyright (C) 2004  Justin Karneges
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "filetransfer.h"

#include <tqtimer.h>
#include <tqptrlist.h>
#include <tqguardedptr.h>
#include <tqfileinfo.h>
#include "xmpp_xmlcommon.h"
#include "s5b.h"

#define SENDBUFSIZE 65536

using namespace XMPP;

// firstChildElement
//
// Get an element's first child element
static TQDomElement firstChildElement(const TQDomElement &e)
{
	for(TQDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
		if(n.isElement())
			return n.toElement();
	}
	return TQDomElement();
}

//----------------------------------------------------------------------------
// FileTransfer
//----------------------------------------------------------------------------
class FileTransfer::Private
{
public:
	FileTransferManager *m;
	JT_FT *ft;
	Jid peer;
	TQString fname;
	TQ_LLONG size;
	TQ_LLONG sent;
	TQString desc;
	bool rangeSupported;
	TQ_LLONG rangeOffset, rangeLength, length;
	TQString streamType;
	bool needStream;
	TQString id, iq_id;
	S5BConnection *c;
	Jid proxy;
	int state;
	bool sender;
};

FileTransfer::FileTransfer(FileTransferManager *m, TQObject *parent)
:TQObject(parent)
{
	d = new Private;
	d->m = m;
	d->ft = 0;
	d->c = 0;
	reset();
}

FileTransfer::~FileTransfer()
{
	reset();
	delete d;
}

void FileTransfer::reset()
{
	d->m->unlink(this);

	delete d->ft;
	d->ft = 0;

	delete d->c;
	d->c = 0;

	d->state = Idle;
	d->needStream = false;
	d->sent = 0;
	d->sender = false;
}

void FileTransfer::setProxy(const Jid &proxy)
{
	d->proxy = proxy;
}

void FileTransfer::sendFile(const Jid &to, const TQString &fname, TQ_LLONG size, const TQString &desc)
{
	d->state = Requesting;
	d->peer = to;
	d->fname = fname;
	d->size = size;
	d->desc = desc;
	d->sender = true;
	d->id = d->m->link(this);

	d->ft = new JT_FT(d->m->client()->rootTask());
	connect(d->ft, TQT_SIGNAL(finished()), TQT_SLOT(ft_finished()));
	TQStringList list;
	list += "http://jabber.org/protocol/bytestreams";
	d->ft->request(to, d->id, fname, size, desc, list);
	d->ft->go(true);
}

int FileTransfer::dataSizeNeeded() const
{
	int pending = d->c->bytesToWrite();
	if(pending >= SENDBUFSIZE)
		return 0;
	TQ_LLONG left = d->length - (d->sent + pending);
	int size = SENDBUFSIZE - pending;
	if((TQ_LLONG)size > left)
		size = (int)left;
	return size;
}

void FileTransfer::writeFileData(const TQByteArray &a)
{
	int pending = d->c->bytesToWrite();
	TQ_LLONG left = d->length - (d->sent + pending);
	if(left == 0)
		return;

	TQByteArray block;
	if((TQ_LLONG)a.size() > left) {
		block = a.copy();
		block.resize((uint)left);
	}
	else
		block = a;
	d->c->write(block);
}

Jid FileTransfer::peer() const
{
	return d->peer;
}

TQString FileTransfer::fileName() const
{
	return d->fname;
}

TQ_LLONG FileTransfer::fileSize() const
{
	return d->size;
}

TQString FileTransfer::description() const
{
	return d->desc;
}

bool FileTransfer::rangeSupported() const
{
	return d->rangeSupported;
}

TQ_LLONG FileTransfer::offset() const
{
	return d->rangeOffset;
}

TQ_LLONG FileTransfer::length() const
{
	return d->length;
}

void FileTransfer::accept(TQ_LLONG offset, TQ_LLONG length)
{
	d->state = Connecting;
	d->rangeOffset = offset;
	d->rangeLength = length;
	if(length > 0)
		d->length = length;
	else
		d->length = d->size;
	d->streamType = "http://jabber.org/protocol/bytestreams";
	d->m->con_accept(this);
}

void FileTransfer::close()
{
	if(d->state == Idle)
		return;
	if(d->state == WaitingForAccept)
		d->m->con_reject(this);
	else if(d->state == Active)
		d->c->close();
	reset();
}

S5BConnection *FileTransfer::s5bConnection() const
{
	return d->c;
}

void FileTransfer::ft_finished()
{
	JT_FT *ft = d->ft;
	d->ft = 0;

	if(ft->success()) {
		d->state = Connecting;
		d->rangeOffset = ft->rangeOffset();
		d->length = ft->rangeLength();
		if(d->length == 0)
			d->length = d->size - d->rangeOffset;
		d->streamType = ft->streamType();
		d->c = d->m->client()->s5bManager()->createConnection();
		connect(d->c, TQT_SIGNAL(connected()), TQT_SLOT(s5b_connected()));
		connect(d->c, TQT_SIGNAL(connectionClosed()), TQT_SLOT(s5b_connectionClosed()));
		connect(d->c, TQT_SIGNAL(bytesWritten(int)), TQT_SLOT(s5b_bytesWritten(int)));
		connect(d->c, TQT_SIGNAL(error(int)), TQT_SLOT(s5b_error(int)));

		if(d->proxy.isValid())
			d->c->setProxy(d->proxy);
		d->c->connectToJid(d->peer, d->id);
		accepted();
	}
	else {
		reset();
		if(ft->statusCode() == 403)
			error(ErrReject);
		else
			error(ErrNeg);
	}
}

void FileTransfer::takeConnection(S5BConnection *c)
{
	d->c = c;
	connect(d->c, TQT_SIGNAL(connected()), TQT_SLOT(s5b_connected()));
	connect(d->c, TQT_SIGNAL(connectionClosed()), TQT_SLOT(s5b_connectionClosed()));
	connect(d->c, TQT_SIGNAL(readyRead()), TQT_SLOT(s5b_readyRead()));
	connect(d->c, TQT_SIGNAL(error(int)), TQT_SLOT(s5b_error(int)));
	if(d->proxy.isValid())
		d->c->setProxy(d->proxy);
	accepted();
	TQTimer::singleShot(0, this, TQT_SLOT(doAccept()));
}

void FileTransfer::s5b_connected()
{
	d->state = Active;
	connected();
}

void FileTransfer::s5b_connectionClosed()
{
	reset();
	error(ErrStream);
}

void FileTransfer::s5b_readyRead()
{
	TQByteArray a = d->c->read();
	TQ_LLONG need = d->length - d->sent;
	if((TQ_LLONG)a.size() > need)
		a.resize((uint)need);
	d->sent += a.size();
	if(d->sent == d->length)
		reset();
	readyRead(a);
}

void FileTransfer::s5b_bytesWritten(int x)
{
	d->sent += x;
	if(d->sent == d->length)
		reset();
	bytesWritten(x);
}

void FileTransfer::s5b_error(int x)
{
	reset();
	if(x == S5BConnection::ErrRefused || x == S5BConnection::ErrConnect)
		error(ErrConnect);
	else if(x == S5BConnection::ErrProxy)
		error(ErrProxy);
	else
		error(ErrStream);
}

void FileTransfer::man_waitForAccept(const FTRequest &req)
{
	d->state = WaitingForAccept;
	d->peer = req.from;
	d->id = req.id;
	d->iq_id = req.iq_id;
	d->fname = req.fname;
	d->size = req.size;
	d->desc = req.desc;
	d->rangeSupported = req.rangeSupported;
}

void FileTransfer::doAccept()
{
	d->c->accept();
}

//----------------------------------------------------------------------------
// FileTransferManager
//----------------------------------------------------------------------------
class FileTransferManager::Private
{
public:
	Client *client;
	TQPtrList<FileTransfer> list, incoming;
	JT_PushFT *pft;
};

FileTransferManager::FileTransferManager(Client *client)
:TQObject(client)
{
	d = new Private;
	d->client = client;

	d->pft = new JT_PushFT(d->client->rootTask());
	connect(d->pft, TQT_SIGNAL(incoming(const FTRequest &)), TQT_SLOT(pft_incoming(const FTRequest &)));
}

FileTransferManager::~FileTransferManager()
{
	d->incoming.setAutoDelete(true);
	d->incoming.clear();
	delete d->pft;
	delete d;
}

Client *FileTransferManager::client() const
{
	return d->client;
}

FileTransfer *FileTransferManager::createTransfer()
{
	FileTransfer *ft = new FileTransfer(this);
	return ft;
}

FileTransfer *FileTransferManager::takeIncoming()
{
	if(d->incoming.isEmpty())
		return 0;

	FileTransfer *ft = d->incoming.getFirst();
	d->incoming.removeRef(ft);

	// move to active list
	d->list.append(ft);
	return ft;
}

void FileTransferManager::pft_incoming(const FTRequest &req)
{
	bool found = false;
	for(TQStringList::ConstIterator it = req.streamTypes.begin(); it != req.streamTypes.end(); ++it) {
		if((*it) == "http://jabber.org/protocol/bytestreams") {
			found = true;
			break;
		}
	}
	if(!found) {
		d->pft->respondError(req.from, req.iq_id, 400, "No valid stream types");
		return;
	}
	if(!d->client->s5bManager()->isAcceptableSID(req.from, req.id)) {
		d->pft->respondError(req.from, req.iq_id, 400, "SID in use");
		return;
	}

	FileTransfer *ft = new FileTransfer(this);
	ft->man_waitForAccept(req);
	d->incoming.append(ft);
	incomingReady();
}

void FileTransferManager::s5b_incomingReady(S5BConnection *c)
{
	TQPtrListIterator<FileTransfer> it(d->list);
	FileTransfer *ft = 0;
	for(FileTransfer *i; (i = it.current()); ++it) {
		if(i->d->needStream && i->d->peer.compare(c->peer()) && i->d->id == c->sid()) {
			ft = i;
			break;
		}
	}
	if(!ft) {
		c->close();
		delete c;
		return;
	}
	ft->takeConnection(c);
}

TQString FileTransferManager::link(FileTransfer *ft)
{
	d->list.append(ft);
	return d->client->s5bManager()->genUniqueSID(ft->d->peer);
}

void FileTransferManager::con_accept(FileTransfer *ft)
{
	ft->d->needStream = true;
	d->pft->respondSuccess(ft->d->peer, ft->d->iq_id, ft->d->rangeOffset, ft->d->rangeLength, ft->d->streamType);
}

void FileTransferManager::con_reject(FileTransfer *ft)
{
	d->pft->respondError(ft->d->peer, ft->d->iq_id, 403, "Declined");
}

void FileTransferManager::unlink(FileTransfer *ft)
{
	d->list.removeRef(ft);
}

//----------------------------------------------------------------------------
// JT_FT
//----------------------------------------------------------------------------
class JT_FT::Private
{
public:
	TQDomElement iq;
	Jid to;
	TQ_LLONG size, rangeOffset, rangeLength;
	TQString streamType;
	TQStringList streamTypes;
};

JT_FT::JT_FT(Task *parent)
:Task(parent)
{
	d = new Private;
}

JT_FT::~JT_FT()
{
	delete d;
}

void JT_FT::request(const Jid &to, const TQString &_id, const TQString &fname, TQ_LLONG size, const TQString &desc, const TQStringList &streamTypes)
{
	TQDomElement iq;
	d->to = to;
	iq = createIQ(doc(), "set", to.full(), id());
	TQDomElement si = doc()->createElement("si");
	si.setAttribute("xmlns", "http://jabber.org/protocol/si");
	si.setAttribute("id", _id);
	si.setAttribute("profile", "http://jabber.org/protocol/si/profile/file-transfer");

	TQDomElement file = doc()->createElement("file");
	file.setAttribute("xmlns", "http://jabber.org/protocol/si/profile/file-transfer");
	file.setAttribute("name", fname);
	file.setAttribute("size", TQString::number(size));
	if(!desc.isEmpty()) {
		TQDomElement de = doc()->createElement("desc");
		de.appendChild(doc()->createTextNode(desc));
		file.appendChild(de);
	}
	TQDomElement range = doc()->createElement("range");
	file.appendChild(range);
	si.appendChild(file);

	TQDomElement feature = doc()->createElement("feature");
	feature.setAttribute("xmlns", "http://jabber.org/protocol/feature-neg");
	TQDomElement x = doc()->createElement("x");
	x.setAttribute("xmlns", "jabber:x:data");
	x.setAttribute("type", "form");

	TQDomElement field = doc()->createElement("field");
	field.setAttribute("var", "stream-method");
	field.setAttribute("type", "list-single");
	for(TQStringList::ConstIterator it = streamTypes.begin(); it != streamTypes.end(); ++it) {
		TQDomElement option = doc()->createElement("option");
		TQDomElement value = doc()->createElement("value");
		value.appendChild(doc()->createTextNode(*it));
		option.appendChild(value);
		field.appendChild(option);
	}

	x.appendChild(field);
	feature.appendChild(x);

	si.appendChild(feature);
	iq.appendChild(si);

	d->streamTypes = streamTypes;
	d->size = size;
	d->iq = iq;
}

TQ_LLONG JT_FT::rangeOffset() const
{
	return d->rangeOffset;
}

TQ_LLONG JT_FT::rangeLength() const
{
	return d->rangeLength;
}

TQString JT_FT::streamType() const
{
	return d->streamType;
}

void JT_FT::onGo()
{
	send(d->iq);
}

bool JT_FT::take(const TQDomElement &x)
{
	if(!iqVerify(x, d->to, id()))
		return false;

	if(x.attribute("type") == "result") {
		TQDomElement si = firstChildElement(x);
		if(si.attribute("xmlns") != "http://jabber.org/protocol/si" || si.tagName() != "si") {
			setError(900, "");
			return true;
		}

		TQString id = si.attribute("id");

		TQ_LLONG range_offset = 0;
		TQ_LLONG range_length = 0;

		TQDomElement file = si.elementsByTagName("file").item(0).toElement();
		if(!file.isNull()) {
			TQDomElement range = file.elementsByTagName("range").item(0).toElement();
			if(!range.isNull()) {
				int x;
				bool ok;
				if(range.hasAttribute("offset")) {
					x = range.attribute("offset").toLongLong(&ok);
					if(!ok || x < 0) {
						setError(900, "");
						return true;
					}
					range_offset = x;
				}
				if(range.hasAttribute("length")) {
					x = range.attribute("length").toLongLong(&ok);
					if(!ok || x < 0) {
						setError(900, "");
						return true;
					}
					range_length = x;
				}
			}
		}

		if(range_offset > d->size || (range_length > (d->size - range_offset))) {
			setError(900, "");
			return true;
		}

		TQString streamtype;
		TQDomElement feature = si.elementsByTagName("feature").item(0).toElement();
		if(!feature.isNull() && feature.attribute("xmlns") == "http://jabber.org/protocol/feature-neg") {
			TQDomElement x = feature.elementsByTagName("x").item(0).toElement();
			if(!x.isNull() && x.attribute("type") == "submit") {
				TQDomElement field = x.elementsByTagName("field").item(0).toElement();
				if(!field.isNull() && field.attribute("var") == "stream-method") {
					TQDomElement value = field.elementsByTagName("value").item(0).toElement();
					if(!value.isNull())
						streamtype = value.text();
				}
			}
		}

		// must be one of the offered streamtypes
		bool found = false;
		for(TQStringList::ConstIterator it = d->streamTypes.begin(); it != d->streamTypes.end(); ++it) {
			if((*it) == streamtype) {
				found = true;
				break;
			}
		}
		if(!found)
			return true;

		d->rangeOffset = range_offset;
		d->rangeLength = range_length;
		d->streamType = streamtype;
		setSuccess();
	}
	else {
		setError(x);
	}

	return true;
}

//----------------------------------------------------------------------------
// JT_PushFT
//----------------------------------------------------------------------------
JT_PushFT::JT_PushFT(Task *parent)
:Task(parent)
{
}

JT_PushFT::~JT_PushFT()
{
}

void JT_PushFT::respondSuccess(const Jid &to, const TQString &id, TQ_LLONG rangeOffset, TQ_LLONG rangeLength, const TQString &streamType)
{
	TQDomElement iq = createIQ(doc(), "result", to.full(), id);
	TQDomElement si = doc()->createElement("si");
	si.setAttribute("xmlns", "http://jabber.org/protocol/si");

	if(rangeOffset != 0 || rangeLength != 0) {
		TQDomElement file = doc()->createElement("file");
		file.setAttribute("xmlns", "http://jabber.org/protocol/si/profile/file-transfer");
		TQDomElement range = doc()->createElement("range");
		if(rangeOffset > 0)
			range.setAttribute("offset", TQString::number(rangeOffset));
		if(rangeLength > 0)
			range.setAttribute("length", TQString::number(rangeLength));
		file.appendChild(range);
		si.appendChild(file);
	}

	TQDomElement feature = doc()->createElement("feature");
	feature.setAttribute("xmlns", "http://jabber.org/protocol/feature-neg");
	TQDomElement x = doc()->createElement("x");
	x.setAttribute("xmlns", "jabber:x:data");
	x.setAttribute("type", "submit");

	TQDomElement field = doc()->createElement("field");
	field.setAttribute("var", "stream-method");
	TQDomElement value = doc()->createElement("value");
	value.appendChild(doc()->createTextNode(streamType));
	field.appendChild(value);

	x.appendChild(field);
	feature.appendChild(x);

	si.appendChild(feature);
	iq.appendChild(si);
	send(iq);
}

void JT_PushFT::respondError(const Jid &to, const TQString &id, int code, const TQString &str)
{
	TQDomElement iq = createIQ(doc(), "error", to.full(), id);
	TQDomElement err = textTag(doc(), "error", str);
	err.setAttribute("code", TQString::number(code));
	iq.appendChild(err);
	send(iq);
}

bool JT_PushFT::take(const TQDomElement &e)
{
	// must be an iq-set tag
	if(e.tagName() != "iq")
		return false;
	if(e.attribute("type") != "set")
		return false;

	TQDomElement si = firstChildElement(e);
	if(si.attribute("xmlns") != "http://jabber.org/protocol/si" || si.tagName() != "si")
		return false;
	if(si.attribute("profile") != "http://jabber.org/protocol/si/profile/file-transfer")
		return false;

	Jid from(e.attribute("from"));
	TQString id = si.attribute("id");

	TQDomElement file = si.elementsByTagName("file").item(0).toElement();
	if(file.isNull())
		return true;

	TQString fname = file.attribute("name");
	if(fname.isEmpty()) {
		respondError(from, id, 400, "Bad file name");
		return true;
	}

	// ensure kosher
	{
		TQFileInfo fi(fname);
		fname = fi.fileName();
	}

	bool ok;
	TQ_LLONG size = file.attribute("size").toLongLong(&ok);
	if(!ok || size < 0) {
		respondError(from, id, 400, "Bad file size");
		return true;
	}

	TQString desc;
	TQDomElement de = file.elementsByTagName("desc").item(0).toElement();
	if(!de.isNull())
		desc = de.text();

	bool rangeSupported = false;
	TQDomElement range = file.elementsByTagName("range").item(0).toElement();
	if(!range.isNull())
		rangeSupported = true;

	TQStringList streamTypes;
	TQDomElement feature = si.elementsByTagName("feature").item(0).toElement();
	if(!feature.isNull() && feature.attribute("xmlns") == "http://jabber.org/protocol/feature-neg") {
		TQDomElement x = feature.elementsByTagName("x").item(0).toElement();
		if(!x.isNull() /*&& x.attribute("type") == "form"*/) {
			TQDomElement field = x.elementsByTagName("field").item(0).toElement();
			if(!field.isNull() && field.attribute("var") == "stream-method" && field.attribute("type") == "list-single") {
				TQDomNodeList nl = field.elementsByTagName("option");
				for(uint n = 0; n < nl.count(); ++n) {
					TQDomElement e = nl.item(n).toElement();
					TQDomElement value = e.elementsByTagName("value").item(0).toElement();
					if(!value.isNull())
						streamTypes += value.text();
				}
			}
		}
	}

	FTRequest r;
	r.from = from;
	r.iq_id = e.attribute("id");
	r.id = id;
	r.fname = fname;
	r.size = size;
	r.desc = desc;
	r.rangeSupported = rangeSupported;
	r.streamTypes = streamTypes;

	incoming(r);
	return true;
}
