#include "dict.h"

#include <KCodecs>
#include <KLocalizedString>
#include <QTcpSocket>
#include <QTimer>
#include <QRegExp>
#include <QCryptographicHash>


namespace Dict {

const QString Connection::DefaultHost = "dict.org";
const int Connection::DefaultPort = 2628;
const QString Connection::DefaultDatabase = "!";
const QString Connection::DefaultStrategy = ".";
const QString Connection::AllDatabases = "*";

Connection::Connection(const QString &host, int port)
	: socket(0), state(Disconnected), response(0),
	interauth(false), keepconnect(false), keepalive(true)
{
	dict.host = host;
	dict.port = port;
	setupSocket();
	setupTimers();
	dict.mime = false;
}

void Connection::reset()
{
	delete socket;
	state = Disconnected;
	setupSocket();
}

void Connection::abort()
{
	// Qt before failed to abort the connection attempt
	// reset()
	socket->abort();
}

void Connection::setupSocket()
{
	socket = new QTcpSocket(this);
	connect(socket, SIGNAL(readyRead()), SLOT(read()));
	connect(socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
		SLOT(socketState(QAbstractSocket::SocketState)));
	connect(socket, SIGNAL(error(QAbstractSocket::SocketError)),
		SLOT(socketError(QAbstractSocket::SocketError)));
}

void Connection::setupTimers()
{
	setTimeouts();
	connect(&connectTimer, SIGNAL(timeout()), SLOT(connectTimeout()));
	connect(&readTimer, SIGNAL(timeout()), SLOT(readTimeout()));
	connect(&idleTimer, SIGNAL(timeout()), SLOT(quit()));
}

void Connection::setTimeouts(int connect, int read, int idle)
{
	connectTimer.setInterval(qMax(connect, 20) * 1000);
	readTimer.setInterval(qMax(read, 10) * 1000);
	idleTimer.setInterval(qMax(idle, 240) * 1000);
}

void Connection::connectTimeout()
{
	connectTimer.stop();
	abort(); clear();
	error(i18n("Connect Timeout"), ConnectTimeout);
	status(i18n("Canceled"));
}

void Connection::readTimeout()
{
	readTimer.stop();
	abort(); clear();
	error(i18n("Read Timeout"), ReadTimeout);
	status(i18n("Canceled"));
}

void Connection::quit()
{
	if (state >= Connected)
		send(new Request(Request::Quit, "quit"));
}

void Connection::open()
{
	socket->connectToHost(dict.host, dict.port);
}

void Connection::setHost(const QString &host, int port)
{
	if (dict.host != host || dict.port != port) {
		dict.host = host;
		dict.port = port;
		if (state != Disconnected)
			abort();
		dict.clear();
		clear();
	}
}

void Connection::Info::clear()
{
	ident.clear(); msgid.clear(); capabilities.clear();
	user.clear(); password.clear();
}

void Connection::read()
{
	readTimer.stop();
	int bytes = socket->bytesAvailable();
	if (!bytes) return;

	if (state < Reading) state = Reading;
	status(i18n("Receiving"));
	QByteArray buf = socket->read(bytes);

	// convert from utf only at newline
	// since a multibyte character can get split across packets
	int end = buf.lastIndexOf('\n');
	read(QString::fromUtf8(extra + buf.left(end)).remove('\r'));
	extra = buf.mid(end + 1);
}

void Connection::send(const Request *request)
{
	if (state > Connected) {
		state = Writing;
		socket->write(request->command.toUtf8() + '\n');
		readTimer.start();
		idleTimer.start();
		status(i18n("Sending"));
		recvqueue << request;
	} else {
		if (state == Disconnected)
			open();
		sendqueue << request;
	}
}

inline void clearQueue(Queue &list)
{
	while (!list.isEmpty())
		delete list.takeFirst();
}

inline void removeFromQueue(Queue &list)
{
	if (!list.isEmpty())
		delete list.takeFirst();
}

void Connection::clear()
{
	clearQueue(sendqueue);
	clearQueue(recvqueue);
}

void Connection::cancel()
{
	connectTimer.stop();
	readTimer.stop();
	clear();

	if (keepconnect && state > Connected) {
		state = Canceled;
		idleTimer.start();
	} else if (state != Disconnected)
		abort();

	status(i18n("Canceled"));
}

void Connection::status()
{
	send(new Request(Request::Status, "status"));
}

void Connection::define(const QString &query, const QString &database)
{
	Q_ASSERT(!database.isEmpty());

	QString command = QString("define %1 \"%2\"")
		.arg(database, QString(query).remove('"'));
	send(new Request(Request::Define, command));
}

void Connection::match(const QString &query, const QString &database, const QString &strategy)
{
	Q_ASSERT(!database.isEmpty());
	Q_ASSERT(!strategy.isEmpty());

	QString command = QString("match %1 %2 \"%3\"")
		.arg(database, strategy, QString(query).remove('"'));
	send(new Request(Request::Match, command));
}

void Connection::info(const QString &query)
{
	Request::RequestCommand type;
	if (query == "server")
		type = Request::Server;
	else if (query == "databases")
		type = Request::Databases;
	else if (query == "strategies")
		type = Request::Strategies;
	else
		type = Request::Info;

	send(new Request(type, QString("show %1").arg(query)));
}

void Connection::setOption(Option opt, const QVariant &val)
{
	if (opt == Mime) dict.mime = val.toBool();
	if (opt == Distance) dict.distance = val.toInt();
	quit();
}

void Connection::setClient(const QString &client)
{
	dict.client = QString(client).remove('"');
}

void Connection::setUser(const QString &username, const QString &password)
{
	dict.user = username;
	dict.password = password;
}

void Connection::auth(const QString &username, const QString &password)
{
	setUser(username, password);
	if (dict.user.isEmpty())
		return;

	interauth = true;
	(state >= Connected) ? auth() : open();
}

void Connection::auth()
{
	QCryptographicHash context(QCryptographicHash::Md5);
	context.addData(dict.msgid.toUtf8());
	context.addData(dict.password.toUtf8());

	QString command = QString("auth %1 %2")
		.arg(dict.user, QString(context.result().toHex()));
	if (dict.capabilities.contains("auth"))
		send(new Request(Request::Auth, command));
}

void Connection::client()
{
	QString command = QString("client \"%1\"").arg(dict.client);
	send(new Request(Request::Client, command));
}

void Connection::options()
{
	QString option;
	QStringList commands;

	option = "mime";
	if (dict.mime && dict.capabilities.contains(option))
		commands << QString("option %1").arg(option);
	option = "xlev";
	if (dict.distance && dict.capabilities.contains(option))
		commands << QString("xlev %2").arg(dict.distance);
	foreach (QString command, commands)
		send(new Request(Request::Option, command));
}

void Connection::serverIdent(const QString &text)
{
	QRegExp rx("(.+) <(.+)> (<.+>)");
	if (rx.indexIn(text) >= 0) {
		QStringList list = rx.capturedTexts();
		dict.ident = list[1];
		dict.capabilities = list[2].split('.');
		dict.msgid = list[3];
	} else
		dict.clear();
}

inline Response::ResponseCode responseCode(const QString &line)
{
	return (Response::ResponseCode)
		((line.size() >= 3) ? line.left(3).toInt() : 0);
}

inline QString responseMessage(const QString &line)
{
	return (line.size() > 4) ? line.mid(4) : "";
}

inline bool endMark(const QString &line)
{
	return (line == ".");
}

void Connection::read(const QString &text)
{
	if (state == Reading) state = Parsing;

	foreach (QString line, text.split('\n')) {
		if (state == Parsing) {
			if (!line.isEmpty())
				parse(line);
		} else if (state == Appending) {
			if (endMark(line)) {
				response->end();
				state = Parsing;
			} else
				response->append(line);
		}
	}

	if (state == Parsing) state = Reading;
}

Pair wordpair(const QString &line)
{
	QString key = line.section('"', 2, 2).trimmed();
	QString val = line.section('"', 3, 3);
	return qMakePair(key, val);
}

Pair matchpair(const QString &line)
{
	QString key = line.section(' ', 0, 0);
	QString val = line.section('"', 1, 1);
	return qMakePair(key, val);
}

void ListResponse::append(const QString &line)
{
	if (line.isEmpty()) return;
	Pair pair = matchpair(line);
	if (pair.second.isEmpty())
		pair.second = pair.first;
	list.append(pair);
}

void DefineResponse::parse(const QString &)
{
	if (type == Response::Definitions)
		return;

	Pair pair = wordpair(message);
	if (pair.first != database && !database.isEmpty()) {
		emit processed(*this);
		list.clear();
	}
	text.clear();
	database = pair.first;
	// use database name for empty description
	description = (pair.second.isEmpty()) ? pair.first : pair.second;
}

void MimeResponse::parse(const QString &line)
{
	DefineResponse::parse(line);
	isHeader = true;
	header.clear();
}

void MimeResponse::append(const QString &line)
{
	if (isHeader) {
		if (line.isEmpty()) isHeader = false;
		else header.append(line);
	} else
		DefineResponse::append(line);
}

void MimeResponse::end()
{
	decode();
	DefineResponse::end();
}

void MimeResponse::decode()
{
	QByteArray (*func)(const QByteArray &) = 0;

	if (header.contains("content-transfer-encoding: base64", Qt::CaseInsensitive))
		func = KCodecs::base64Decode;
	else if (header.contains("content-transfer-encoding: quoted-printable", Qt::CaseInsensitive))
		func = KCodecs::quotedPrintableDecode;

	if (func)
		text = QString::fromUtf8(func(text.toUtf8()));
}

void MatchResponse::append(const QString &line)
{
	if (line.isEmpty()) return;
	Pair pair = matchpair(line);
	if (pair.first != database && !database.isEmpty()) {
		emit processed(*this);
		list.clear();
	}
	database = pair.first;
	// skip duplicates
	if (list.isEmpty() || list.last() != pair.second)
		list.append(pair.second);
}

Response *noMatch = new Response(Response::NoMatch);

Response *Connection::newResponse(Response::ResponseCode code)
{
	switch (code) {
	case Response::Definitions:
		if (dict.mime) response = new MimeResponse(code);
		else response = new DefineResponse(code);
		return response;
	case Response::Words:
		if (response) response->type = code;
		else response = noMatch;
		return response;
	case Response::Matches:
		return new MatchResponse(code);
	case Response::NoMatch:
		return noMatch;
	case Response::DatabaseList:
	case Response::StrategyList:
		return new ListResponse(code);
	default:
		return new Response(code);
	}
}

void Connection::delResponse()
{
	if (response == noMatch)
		return;
	delete response;
	response = 0;
}

typedef QHash<Response::ResponseCode, Request::RequestCommand> Map;

class Table
{
public:
	Table();
	const Map &map() const { return table; }

private:
	Map table;
};

Table::Table()
{
	table[Response::Definitions] = Request::Define;
	table[Response::Words] = Request::Define;
	table[Response::Matches] = Request::Match;
	table[Response::DatabaseList] = Request::Databases;
	table[Response::StrategyList] = Request::Strategies;
	table[Response::DatabaseInfo] = Request::Info;
	table[Response::ServerInfo] = Request::Server;
	table[Response::AuthOK] = Request::Auth;
	table[Response::StatusMessage] = Request::Status;
	table[Response::Hello] = Request::None;
	table[Response::Bye] = Request::Quit;
}

const Table table;
const Map &map = table.map();
const Request hello(Request::None, "");

void Connection::parse(const QString &line)
{
	Response::ResponseCode code = responseCode(line);

	const Request *request;
	if (!recvqueue.isEmpty())
		request = recvqueue.first();
	else // dummy request for Hello response
		request = &hello;

	if (!code) {
		qWarning() << "dict: empty response for request" << request->type;
		return;
	} else if (code != Response::OK) {
		response = newResponse(code);
		response->message = responseMessage(line);
		emit responseReceived(*response);
		if (map.contains(code) && map.value(code) != request->type)
			qWarning() << "dict: unexpected response" << code
				<< "for request" << request->type;
		response->parse(line);
	} else if (!response) {
		removeFromQueue(recvqueue);
		return;
	}

	switch (code) {
	case Response::Words:
		state = Appending;
		break;
	case Response::Matches:
		state = Appending;
		// fall thru
	case Response::Definitions:
		connect(response, SIGNAL(processed(const Response &)), SIGNAL(responseProcessed(const Response &)));
		break;
	case Response::OK:
		okResponse();
		break;
	case Response::NoMatch:
		done();
		break;
	case Response::DatabaseList:
	case Response::StrategyList:
	case Response::ServerInfo:
	case Response::DatabaseInfo:
		state = Appending;
		break;
	case Response::Hello:
		helloResponse();
		break;
	case Response::Bye:
		removeFromQueue(recvqueue);
		delResponse();
		break;
	case Response::StatusMessage:
		response->text = i18n("Connected to %1.", dict.host);
		done();
		break;
	case Response::AuthOK:
		emit authenticated(true, dict.user);
		if (interauth) {
			response->text = i18n("Authenticated to %1.", dict.host);
			interauth = false;
			done();
		} else {
			removeFromQueue(recvqueue);
			delResponse();
		}
		break;
	case Response::AuthDenied:
		emit authenticated(false, dict.user);
		if (interauth)
			error(i18n("Authentication failed"), code);
		interauth = false;
		removeFromQueue(recvqueue);
		delResponse();
		break;
	case Response::AccessDenied:
		error(i18n("Access denied"), code);
		break;
	case Response::InvalidDatabase:
		{
		int sect = (request->type != Request::Info) ? 1 : 2;
		error(i18n("Invalid database: %1",
			request->command.section(' ', sect, sect)), code);
		}
		removeFromQueue(recvqueue);
		delResponse();
		break;
	case Response::InvalidStrategy:
		error(i18n("Invalid strategy: %1",
			request->command.section(' ', 2, 2)), code);
		removeFromQueue(recvqueue);
		delResponse();
		break;
	case Response::NoDatabase:
		error(i18n("No databases available"), code);
		break;
	case Response::NoStrategy:
		error(i18n("No strategies available"), code);
		break;
	case Response::ServerUnavailable:
		error(i18n("Server temporarily unavailable"), code);
		break;
	case Response::ServerShutdown:
		error(i18n("Server shutting down"), code);
		break;
	case Response::UnrecognizedCommand:
	case Response::IllegalParameter:
	case Response::UnimplementedCommand:
	case Response::UnimplementedParameter:
		error(i18n("Syntax error"), code);
		break;
	default:
		qWarning() << "dict: unhandled response" << code;
	}
}

void Connection::helloResponse()
{
	serverIdent(response->message);
	clearQueue(recvqueue);

	if (!dict.client.isEmpty()) client();
	if (!dict.user.isEmpty()) auth();
	if (dict.mime || dict.distance) options();

	foreach (const Request *request, sendqueue)
		send(request);
	sendqueue.clear();
	emit identified(!dict.ident.isEmpty(), dict.capabilities);
}

void Connection::okResponse()
{
	switch (response->type) {
	case Response::Words:
	case Response::Matches:
		((MatchResponse *)response)->isDone = true;
		// fall thru
	case Response::DatabaseList:
	case Response::StrategyList:
	case Response::DatabaseInfo:
	case Response::ServerInfo:
		done();
		break;
	case Response::Hello:
	case Response::Bye:
		removeFromQueue(recvqueue);
		delResponse();
		break;
	default:
		qWarning() << "dict: unhandled response" << response->type;
	}
}

void Connection::done()
{
	emit responseProcessed(*response);
//	emit responseText(response->text);
	status(i18n("Done"));
	removeFromQueue(recvqueue);
	delResponse();
	if (!keepalive && recvqueue.isEmpty()) quit();
}

void Connection::status(const QString &message)
{
	emit statusMessage(message);
}

void Connection::error(const QString &message, int type)
{
	emit errorMessage(message, type);
}

void Connection::socketState(QAbstractSocket::SocketState socketState)
{
	switch (socketState) {
	case QAbstractSocket::UnconnectedState:
		readTimer.stop();
		idleTimer.stop();
		state = Disconnected;
		status(i18n("Disconnected"));
		emit connected(false, dict.host);
		break;
	case QAbstractSocket::HostLookupState:
		state = HostLookup;
		status(i18n("Looking for %1...", dict.host));
		break;
	case QAbstractSocket::ConnectingState:
		connectTimer.start();
		state = Connecting;
		status(i18n("Connecting to %1...", dict.host));
		break;
	case QAbstractSocket::ConnectedState:
		connectTimer.stop();
		readTimer.start();
		state = Connected;
		status(i18n("Waiting for %1...", dict.host));
		emit connected(true, dict.host);
		break;
	case QAbstractSocket::ClosingState:
		status(i18n("Disconnecting"));
		break;
	default:
		qWarning() << "dict: unhandled state" << socketState;
	}
}

void Connection::socketError(QAbstractSocket::SocketError errorType)
{
	connectTimer.stop();
	switch (errorType) {
	case QAbstractSocket::RemoteHostClosedError:
		break;
	case QAbstractSocket::ConnectionRefusedError:
		error(i18n("Connection refused by %1", dict.host), ConnectionRefused);
		break;
	case QAbstractSocket::HostNotFoundError:
		error(i18n("Host %1 not found", dict.host), HostNotFound);
		break;
	default:
		QString message = socket->errorString();
		if (message.isEmpty()) return;
		error(message, UnknownError);
// 		qDebug() << "dict error: unhandled error" << error;
	}
}

}
