//
// VMime library (http://www.vmime.org)
// Copyright (C) 2002 Vincent Richard <vincent@vmime.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 3 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Linking this library statically or dynamically with other modules is making
// a combined work based on this library.  Thus, the terms and conditions of
// the GNU General Public License cover the whole combination.
//

#include "vmime/config.hpp"


#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_POP3


#include "vmime/net/pop3/POP3Response.hpp"
#include "vmime/net/pop3/POP3Connection.hpp"

#include "vmime/platform.hpp"

#include "vmime/utility/stringUtils.hpp"
#include "vmime/utility/filteredStream.hpp"
#include "vmime/utility/stringUtils.hpp"
#include "vmime/utility/inputStreamSocketAdapter.hpp"

#include "vmime/net/socket.hpp"
#include "vmime/net/timeoutHandler.hpp"


namespace vmime {
namespace net {
namespace pop3 {


POP3Response::POP3Response(
	const shared_ptr <socket>& sok,
	const shared_ptr <timeoutHandler>& toh,
	const shared_ptr <tracer>& tracer
)
	: m_socket(sok),
	  m_timeoutHandler(toh),
	  m_tracer(tracer) {

}


// static
shared_ptr <POP3Response> POP3Response::readResponse(
	const shared_ptr <POP3Connection>& conn
) {

	shared_ptr <POP3Response> resp = shared_ptr <POP3Response>(
		new POP3Response(conn->getSocket(), conn->getTimeoutHandler(), conn->getTracer())
	);

	string buffer;
	resp->readResponseImpl(buffer, /* multiLine */ false);

	resp->m_firstLine = buffer;
	resp->m_code = getResponseCode(buffer);
	stripResponseCode(buffer, resp->m_text);

	if (resp->m_tracer) {
		resp->m_tracer->traceReceive(buffer);
	}

	return resp;
}


// static
shared_ptr <POP3Response> POP3Response::readMultilineResponse(
	const shared_ptr <POP3Connection>& conn
) {

	shared_ptr <POP3Response> resp = shared_ptr <POP3Response>(
		new POP3Response(conn->getSocket(), conn->getTimeoutHandler(), conn->getTracer())
	);

	string buffer;
	resp->readResponseImpl(buffer, /* multiLine */ true);

	string firstLine, nextLines;
	stripFirstLine(buffer, nextLines, &firstLine);

	resp->m_firstLine = firstLine;
	resp->m_code = getResponseCode(firstLine);
	stripResponseCode(firstLine, resp->m_text);

	std::istringstream iss(nextLines);
	string line;

	if (resp->m_tracer) {
		resp->m_tracer->traceReceive(firstLine);
	}

	while (std::getline(iss, line, '\n')) {

		line = utility::stringUtils::trim(line);
		resp->m_lines.push_back(line);

		if (resp->m_tracer) {
			resp->m_tracer->traceReceive(line);
		}
	}

	if (resp->m_tracer) {
		resp->m_tracer->traceReceive(".");
	}

	return resp;
}


// static
shared_ptr <POP3Response> POP3Response::readLargeResponse(
	const shared_ptr <POP3Connection>& conn,
	utility::outputStream& os,
	utility::progressListener* progress,
	const size_t predictedSize
) {

	shared_ptr <POP3Response> resp = shared_ptr <POP3Response>(
		new POP3Response(conn->getSocket(), conn->getTimeoutHandler(), conn->getTracer())
	);

	string firstLine;
	const size_t length = resp->readResponseImpl(firstLine, os, progress, predictedSize);

	resp->m_firstLine = firstLine;
	resp->m_code = getResponseCode(firstLine);
	stripResponseCode(firstLine, resp->m_text);

	if (resp->m_tracer) {
		resp->m_tracer->traceReceive(firstLine);
		resp->m_tracer->traceReceiveBytes(length - firstLine.length());
		resp->m_tracer->traceReceive(".");
	}

	return resp;
}


bool POP3Response::isSuccess() const {

	return m_code == CODE_OK;
}


const string POP3Response::getFirstLine() const {

	return m_firstLine;
}


POP3Response::ResponseCode POP3Response::getCode() const {

	return m_code;
}


const string POP3Response::getText() const {

	return m_text;
}


const string POP3Response::getLineAt(const size_t pos) const {

	return m_lines[pos];
}


size_t POP3Response::getLineCount() const {

	return m_lines.size();
}


void POP3Response::readResponseImpl(string& buffer, const bool multiLine) {

	bool foundTerminator = false;

	if (m_timeoutHandler) {
		m_timeoutHandler->resetTimeOut();
	}

	buffer.clear();

	char last1 = '\0', last2 = '\0';

	for ( ; !foundTerminator ; ) {

		// Check whether the time-out delay is elapsed
		if (m_timeoutHandler && m_timeoutHandler->isTimeOut()) {

			if (!m_timeoutHandler->handleTimeOut()) {
				throw exceptions::operation_timed_out();
			}

			m_timeoutHandler->resetTimeOut();
		}

		// Receive data from the socket
		string receiveBuffer;
		m_socket->receive(receiveBuffer);

		if (receiveBuffer.empty()) {  // buffer is empty

			if (m_socket->getStatus() & socket::STATUS_WANT_WRITE) {
				m_socket->waitForWrite();
			} else {
				m_socket->waitForRead();
			}

			continue;
		}

		// We have received data: reset the time-out counter
		if (m_timeoutHandler) {
			m_timeoutHandler->resetTimeOut();
		}

		// Check for transparent characters: '\n..' becomes '\n.'
		const char first = receiveBuffer[0];

		if (first == '.' && last2 == '\n' && last1 == '.') {

			receiveBuffer.erase(receiveBuffer.begin());

		} else if (receiveBuffer.length() >= 2 && first == '.' &&
		           receiveBuffer[1] == '.' && last1 == '\n') {

			receiveBuffer.erase(receiveBuffer.begin());
		}

		for (size_t trans ;
		     string::npos != (trans = receiveBuffer.find("\n..")) ; ) {

			receiveBuffer.replace(trans, 3, "\n.");
		}

		last1 = receiveBuffer[receiveBuffer.length() - 1];
		last2 = static_cast <char>((receiveBuffer.length() >= 2) ? receiveBuffer[receiveBuffer.length() - 2] : 0);

		// Append the data to the response buffer
		buffer += receiveBuffer;

		// Check for terminator string (and strip it if present)
		foundTerminator = checkTerminator(buffer, multiLine);

		// If there is an error (-ERR) when executing a command that
		// requires a multi-line response, the error response will
		// include only one line, so we stop waiting for a multi-line
		// terminator and check for a "normal" one.
		if (multiLine &&
		    !foundTerminator &&
		    buffer.length() >= 4 && buffer[0] == '-') {

			foundTerminator = checkTerminator(buffer, false);
		}
	}
}


size_t POP3Response::readResponseImpl(
	string& firstLine,
	utility::outputStream& os,
	utility::progressListener* progress,
	const size_t predictedSize
) {

	size_t current = 0, total = predictedSize;

	string temp;
	bool codeDone = false;

	if (progress) {
		progress->start(total);
	}

	if (m_timeoutHandler) {
		m_timeoutHandler->resetTimeOut();
	}

	utility::inputStreamSocketAdapter sis(*m_socket);
	utility::stopSequenceFilteredInputStream <5> sfis1(sis, "\r\n.\r\n");
	utility::stopSequenceFilteredInputStream <3> sfis2(sfis1, "\n.\n");
	utility::dotFilteredInputStream dfis(sfis2);   // "\n.." --> "\n."

	utility::inputStream& is = dfis;

	while (!is.eof()) {

		// Check whether the time-out delay is elapsed
		if (m_timeoutHandler && m_timeoutHandler->isTimeOut()) {

			if (!m_timeoutHandler->handleTimeOut()) {
				throw exceptions::operation_timed_out();
			}
		}

		// Receive data from the socket
		byte_t buffer[65536];
		const size_t read = is.read(buffer, sizeof(buffer));

		if (read == 0) {  // buffer is empty

			if (m_socket->getStatus() & socket::STATUS_WANT_WRITE) {
				m_socket->waitForWrite();
			} else if (m_socket->getStatus() & socket::STATUS_WANT_READ) {
				m_socket->waitForRead();
			} else {
				// Input stream needs more bytes to continue, but there
				// is enough data into socket buffer. Do not waitForRead(),
				// just retry read()ing on the stream.
			}

			continue;
		}

		// We have received data: reset the time-out counter
		if (m_timeoutHandler) {
			m_timeoutHandler->resetTimeOut();
		}

		// Notify progress
		current += read;

		if (progress) {
			total = std::max(total, current);
			progress->progress(current, total);
		}

		// If we don't have extracted the response code yet
		if (!codeDone) {

			vmime::utility::stringUtils::appendBytesToString(temp, buffer, read);

			string responseData;

			if (stripFirstLine(temp, responseData, &firstLine) == true) {

				if (getResponseCode(firstLine) != CODE_OK) {
					throw exceptions::command_error("?", firstLine);
				}

				codeDone = true;

				os.write(responseData.data(), responseData.length());
				temp.clear();

				continue;
			}

		} else {

			// Inject the data into the output stream
			os.write(buffer, read);
		}
	}

	if (progress) {
		progress->stop(total);
	}

	return current;
}


// static
bool POP3Response::stripFirstLine(
	const string& buffer,
	string& result,
	string* firstLine
) {

	const size_t end = buffer.find('\n');

	if (end != string::npos) {

		if (firstLine) {
			*firstLine = utility::stringUtils::trim(buffer.substr(0, end));
		}

		result = buffer.substr(end + 1);

		return true;

	} else {

		if (firstLine) {
			*firstLine = utility::stringUtils::trim(buffer);
		}

		result = "";

		return false;
	}
}


// static
POP3Response::ResponseCode POP3Response::getResponseCode(const string& buffer) {

	if (buffer.length() >= 2) {

		// +[space]
		if (buffer[0] == '+' &&
		    (buffer[1] == ' ' || buffer[1] == '\t')) {

			return CODE_READY;
		}

		// +OK
		if (buffer.length() >= 3) {

			if (buffer[0] == '+' &&
			    (buffer[1] == 'O' || buffer[1] == 'o') &&
			    (buffer[2] == 'K' || buffer[1] == 'k')) {

				return CODE_OK;
			}
		}
	}

	// -ERR or whatever
	return CODE_ERR;
}


// static
void POP3Response::stripResponseCode(const string& buffer, string& result) {

	const size_t pos = buffer.find_first_of(" \t");

	if (pos != string::npos) {
		result = buffer.substr(pos + 1);
	} else {
		result = buffer;
	}
}


// static
bool POP3Response::checkTerminator(string& buffer, const bool multiLine) {

	// Multi-line response
	if (multiLine) {

		static const string term1("\r\n.\r\n");
		static const string term2("\n.\n");

		return checkOneTerminator(buffer, term1) ||
		       checkOneTerminator(buffer, term2);

	// Normal response
	} else {

		static const string term1("\r\n");
		static const string term2("\n");

		return checkOneTerminator(buffer, term1) ||
		       checkOneTerminator(buffer, term2);
	}

	return false;
}


// static
bool POP3Response::checkOneTerminator(string& buffer, const string& term) {

	if (buffer.length() >= term.length() &&
		std::equal(buffer.end() - term.length(), buffer.end(), term.begin())) {

		buffer.erase(buffer.end() - term.length(), buffer.end());
		return true;
	}

	return false;
}


} // pop3
} // net
} // vmime


#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_POP3
