/*
 * Copyright 2010-2013 Haiku Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Christophe Huriaux, c.huriaux@gmail.com
 */


#include <HttpForm.h>

#include <cstdlib>
#include <cstring>
#include <ctime>

#include <File.h>
#include <NodeInfo.h>
#include <TypeConstants.h>
#include <Url.h>


static int32 kBoundaryRandomSize = 16;

using namespace std;


// #pragma mark - BHttpFormData


BHttpFormData::BHttpFormData()
	:
	fDataType(B_HTTPFORM_STRING),
	fCopiedBuffer(false),
	fFileMark(false),
	fBufferValue(NULL),
	fBufferSize(0)
{
}


BHttpFormData::BHttpFormData(const BString& name, const BString& value)
	:
	fDataType(B_HTTPFORM_STRING),
	fCopiedBuffer(false),
	fFileMark(false),
	fName(name),
	fStringValue(value),
	fBufferValue(NULL),
	fBufferSize(0)
{
}


BHttpFormData::BHttpFormData(const BString& name, const BPath& file)
	:
	fDataType(B_HTTPFORM_FILE),
	fCopiedBuffer(false),
	fFileMark(false),
	fName(name),
	fPathValue(file),
	fBufferValue(NULL),
	fBufferSize(0)
{
}


BHttpFormData::BHttpFormData(const BString& name, const void* buffer,
	ssize_t size)
	:
	fDataType(B_HTTPFORM_BUFFER),
	fCopiedBuffer(false),
	fFileMark(false),
	fName(name),
	fBufferValue(buffer),
	fBufferSize(size)
{
}


BHttpFormData::BHttpFormData(const BHttpFormData& other)
	:
	fCopiedBuffer(false),
	fFileMark(false),
	fBufferValue(NULL),
	fBufferSize(0)
{
	*this = other;
}


BHttpFormData::~BHttpFormData()
{
	if (fCopiedBuffer)
		delete[] reinterpret_cast<const char*>(fBufferValue);
}


// #pragma mark - Retrieve data informations


bool
BHttpFormData::InitCheck() const
{
	if (fDataType == B_HTTPFORM_BUFFER)
		return fBufferValue != NULL;

	return true;
}


const BString&
BHttpFormData::Name() const
{
	return fName;
}


const BString&
BHttpFormData::String() const
{
	return fStringValue;
}


const BPath&
BHttpFormData::File() const
{
	return fPathValue;
}


const void*
BHttpFormData::Buffer() const
{
	return fBufferValue;
}


ssize_t
BHttpFormData::BufferSize() const
{
	return fBufferSize;
}


bool
BHttpFormData::IsFile() const
{
	return fFileMark;
}


const BString&
BHttpFormData::Filename() const
{
	return fFilename;
}


const BString&
BHttpFormData::MimeType() const
{
	return fMimeType;
}


form_content_type
BHttpFormData::Type() const
{
	return fDataType;
}


// #pragma mark - Change behavior


status_t
BHttpFormData::MarkAsFile(const BString& filename, const BString& mimeType)
{
	if (fDataType == B_HTTPFORM_UNKNOWN || fDataType == B_HTTPFORM_FILE)
		return B_ERROR;

	fFilename = filename;
	fMimeType = mimeType;
	fFileMark = true;

	return B_OK;
}


void
BHttpFormData::UnmarkAsFile()
{
	fFilename.Truncate(0, true);
	fMimeType.Truncate(0, true);
	fFileMark = false;
}


status_t
BHttpFormData::CopyBuffer()
{
	if (fDataType != B_HTTPFORM_BUFFER)
		return B_ERROR;

	char* copiedBuffer = new(std::nothrow) char[fBufferSize];
	if (copiedBuffer == NULL)
		return B_NO_MEMORY;

	memcpy(copiedBuffer, fBufferValue, fBufferSize);
	fBufferValue = copiedBuffer;
	fCopiedBuffer = true;

	return B_OK;
}


BHttpFormData&
BHttpFormData::operator=(const BHttpFormData& other)
{
	fDataType = other.fDataType;
	fCopiedBuffer = false;
	fFileMark = other.fFileMark;
	fName = other.fName;
	fStringValue = other.fStringValue;
	fPathValue = other.fPathValue;
	fBufferValue = other.fBufferValue;
	fBufferSize = other.fBufferSize;
	fFilename = other.fFilename;
	fMimeType = other.fMimeType;

	if (other.fCopiedBuffer)
		CopyBuffer();

	return *this;
}


// #pragma mark - BHttpForm


BHttpForm::BHttpForm()
	:
	fType(B_HTTP_FORM_URL_ENCODED)
{
}


BHttpForm::BHttpForm(const BHttpForm& other)
	:
	fFields(other.fFields),
	fType(other.fType),
	fMultipartBoundary(other.fMultipartBoundary)
{
}


BHttpForm::BHttpForm(const BString& formString)
	:
	fType(B_HTTP_FORM_URL_ENCODED)
{
	ParseString(formString);
}


BHttpForm::~BHttpForm()
{
	Clear();
}


// #pragma mark - Form string parsing


void
BHttpForm::ParseString(const BString& formString)
{
	int32 index = 0;

	while (index < formString.Length())
		_ExtractNameValuePair(formString, &index);
}


BString
BHttpForm::RawData() const
{
	BString result;

	if (fType == B_HTTP_FORM_URL_ENCODED) {
		for (FormStorage::const_iterator it = fFields.begin();
			it != fFields.end(); it++) {
			const BHttpFormData* currentField = &it->second;

			switch (currentField->Type()) {
				case B_HTTPFORM_UNKNOWN:
					break;

				case B_HTTPFORM_STRING:
					result << '&' << BUrl::UrlEncode(currentField->Name())
						<< '=' << BUrl::UrlEncode(currentField->String());
					break;

				case B_HTTPFORM_FILE:
					break;

				case B_HTTPFORM_BUFFER:
					// Send the buffer only if its not marked as a file
					if (!currentField->IsFile()) {
						result << '&' << BUrl::UrlEncode(currentField->Name())
							<< '=';
						result.Append(
							reinterpret_cast<const char*>(currentField->Buffer()),
							currentField->BufferSize());
					}
					break;
			}
		}

		result.Remove(0, 1);
	} else if (fType == B_HTTP_FORM_MULTIPART) {
		//  Very slow and memory consuming method since we're caching the
		// file content, this should be preferably handled by the protocol
		for (FormStorage::const_iterator it = fFields.begin();
			it != fFields.end(); it++) {
			const BHttpFormData* currentField = &it->second;
			result << _GetMultipartHeader(currentField);

			switch (currentField->Type()) {
				case B_HTTPFORM_UNKNOWN:
					break;

				case B_HTTPFORM_STRING:
					result << currentField->String();
					break;

				case B_HTTPFORM_FILE:
				{
					BFile upFile(currentField->File().Path(), B_READ_ONLY);
					char readBuffer[1024];
					ssize_t readSize;

					readSize = upFile.Read(readBuffer, 1024);

					while (readSize > 0) {
						result.Append(readBuffer, readSize);
						readSize = upFile.Read(readBuffer, 1024);
					}
					break;
				}

				case B_HTTPFORM_BUFFER:
					result.Append(
						reinterpret_cast<const char*>(currentField->Buffer()),
						currentField->BufferSize());
					break;
			}

			result << "\r\n";
		}

		result << "--" << fMultipartBoundary << "--\r\n";
	}

	return result;
}


// #pragma mark - Form add


status_t
BHttpForm::AddString(const BString& fieldName, const BString& value)
{
	BHttpFormData formData(fieldName, value);
	if (!formData.InitCheck())
		return B_ERROR;

	fFields.insert(pair<BString, BHttpFormData>(fieldName, formData));
	return B_OK;
}


status_t
BHttpForm::AddInt(const BString& fieldName, int32 value)
{
	BString strValue;
	strValue << value;

	return AddString(fieldName, strValue);
}


status_t
BHttpForm::AddFile(const BString& fieldName, const BPath& file)
{
	BHttpFormData formData(fieldName, file);
	if (!formData.InitCheck())
		return B_ERROR;

	fFields.insert(pair<BString, BHttpFormData>(fieldName, formData));

	if (fType != B_HTTP_FORM_MULTIPART)
		SetFormType(B_HTTP_FORM_MULTIPART);
	return B_OK;
}


status_t
BHttpForm::AddBuffer(const BString& fieldName, const void* buffer,
	ssize_t size)
{
	BHttpFormData formData(fieldName, buffer, size);
	if (!formData.InitCheck())
		return B_ERROR;

	fFields.insert(pair<BString, BHttpFormData>(fieldName, formData));
	return B_OK;
}


status_t
BHttpForm::AddBufferCopy(const BString& fieldName, const void* buffer,
	ssize_t size)
{
	BHttpFormData formData(fieldName, buffer, size);
	if (!formData.InitCheck())
		return B_ERROR;

	// Copy the buffer of the inserted form data copy to
	// avoid an unneeded copy of the buffer upon insertion
	pair<FormStorage::iterator, bool> insertResult
		= fFields.insert(pair<BString, BHttpFormData>(fieldName, formData));

	return insertResult.first->second.CopyBuffer();
}


// #pragma mark - Mark a field as a filename


void
BHttpForm::MarkAsFile(const BString& fieldName, const BString& filename,
	const BString& mimeType)
{
	FormStorage::iterator it = fFields.find(fieldName);

	if (it == fFields.end())
		return;

	it->second.MarkAsFile(filename, mimeType);
	if (fType != B_HTTP_FORM_MULTIPART)
		SetFormType(B_HTTP_FORM_MULTIPART);
}


void
BHttpForm::MarkAsFile(const BString& fieldName, const BString& filename)
{
	MarkAsFile(fieldName, filename, "");
}


void
BHttpForm::UnmarkAsFile(const BString& fieldName)
{
	FormStorage::iterator it = fFields.find(fieldName);

	if (it == fFields.end())
		return;

	it->second.UnmarkAsFile();
}


// #pragma mark - Change form type


void
BHttpForm::SetFormType(form_type type)
{
	fType = type;

	if (fType == B_HTTP_FORM_MULTIPART)
		_GenerateMultipartBoundary();
}


// #pragma mark - Form test


bool
BHttpForm::HasField(const BString& name) const
{
	return (fFields.find(name) != fFields.end());
}


// #pragma mark - Form retrieve


BString
BHttpForm::GetMultipartHeader(const BString& fieldName) const
{
	FormStorage::const_iterator it = fFields.find(fieldName);

	if (it == fFields.end())
		return BString("");

	return _GetMultipartHeader(&it->second);
}


form_type
BHttpForm::GetFormType() const
{
	return fType;
}


const BString&
BHttpForm::GetMultipartBoundary() const
{
	return fMultipartBoundary;
}


BString
BHttpForm::GetMultipartFooter() const
{
	BString result = "--";
	result << fMultipartBoundary << "--\r\n";
	return result;
}


ssize_t
BHttpForm::ContentLength() const
{
	if (fType == B_HTTP_FORM_URL_ENCODED)
		return RawData().Length();

	ssize_t contentLength = 0;

	for (FormStorage::const_iterator it = fFields.begin();
		it != fFields.end(); it++) {
		const BHttpFormData* c = &it->second;
		contentLength += _GetMultipartHeader(c).Length();

		switch (c->Type()) {
			case B_HTTPFORM_UNKNOWN:
				break;

			case B_HTTPFORM_STRING:
				contentLength += c->String().Length();
				break;

			case B_HTTPFORM_FILE:
			{
				BFile upFile(c->File().Path(), B_READ_ONLY);
				upFile.Seek(0, SEEK_END);
				contentLength += upFile.Position();
				break;
			}

			case B_HTTPFORM_BUFFER:
				contentLength += c->BufferSize();
				break;
		}

		contentLength += 2;
	}

	contentLength += fMultipartBoundary.Length() + 6;

	return contentLength;
}


// #pragma mark Form iterator


BHttpForm::Iterator
BHttpForm::GetIterator()
{
	return BHttpForm::Iterator(this);
}


// #pragma mark - Form clear


void
BHttpForm::Clear()
{
	fFields.clear();
}


// #pragma mark - Overloaded operators


BHttpFormData&
BHttpForm::operator[](const BString& name)
{
	if (!HasField(name))
		AddString(name, "");

	return fFields[name];
}


void
BHttpForm::_ExtractNameValuePair(const BString& formString, int32* index)
{
	// Look for a name=value pair
	int16 firstAmpersand = formString.FindFirst("&", *index);
	int16 firstEqual = formString.FindFirst("=", *index);

	BString name;
	BString value;

	if (firstAmpersand == -1) {
		if (firstEqual != -1) {
			formString.CopyInto(name, *index, firstEqual - *index);
			formString.CopyInto(value, firstEqual + 1,
				formString.Length() - firstEqual - 1);
		} else
			formString.CopyInto(value, *index,
				formString.Length() - *index);

		*index = formString.Length() + 1;
	} else {
		if (firstEqual != -1 && firstEqual < firstAmpersand) {
			formString.CopyInto(name, *index, firstEqual - *index);
			formString.CopyInto(value, firstEqual + 1,
				firstAmpersand - firstEqual - 1);
		} else
			formString.CopyInto(value, *index, firstAmpersand - *index);

		*index = firstAmpersand + 1;
	}

	AddString(name, value);
}


void
BHttpForm::_GenerateMultipartBoundary()
{
	fMultipartBoundary = "----------------------------";

	srand(time(NULL));
		// TODO: Maybe a more robust way to seed the random number
		// generator is needed?

	for (int32 i = 0; i < kBoundaryRandomSize; i++)
		fMultipartBoundary << (char)(rand() % 10 + '0');
}


// #pragma mark - Field information access by std iterator


BString
BHttpForm::_GetMultipartHeader(const BHttpFormData* element) const
{
	BString result;
	result << "--" << fMultipartBoundary << "\r\n";
	result << "Content-Disposition: form-data; name=\"" << element->Name()
		<< '"';

	switch (element->Type()) {
		case B_HTTPFORM_UNKNOWN:
			break;

		case B_HTTPFORM_FILE:
		{
			result << "; filename=\"" << element->File().Leaf() << '"';

			BNode fileNode(element->File().Path());
			BNodeInfo fileInfo(&fileNode);

			result << "\r\nContent-Type: ";
			char tempMime[128];
			if (fileInfo.GetType(tempMime) == B_OK)
				result << tempMime;
			else
				result << "application/octet-stream";

			break;
		}

		case B_HTTPFORM_STRING:
		case B_HTTPFORM_BUFFER:
			if (element->IsFile()) {
				result << "; filename=\"" << element->Filename() << '"';

				if (element->MimeType().Length() > 0)
					result << "\r\nContent-Type: " << element->MimeType();
				else
					result << "\r\nContent-Type: text/plain";
			}
			break;
	}

	result << "\r\n\r\n";

	return result;
}


// #pragma mark - Iterator


BHttpForm::Iterator::Iterator(BHttpForm* form)
	:
	fElement(NULL)
{
	fForm = form;
	fStdIterator = form->fFields.begin();
	_FindNext();
}


BHttpForm::Iterator::Iterator(const Iterator& other)
{
	*this = other;
}


bool
BHttpForm::Iterator::HasNext() const
{
	return fStdIterator != fForm->fFields.end();
}


BHttpFormData*
BHttpForm::Iterator::Next()
{
	BHttpFormData* element = fElement;
	_FindNext();
	return element;
}


void
BHttpForm::Iterator::Remove()
{
	fForm->fFields.erase(fStdIterator);
	fElement = NULL;
}


BString
BHttpForm::Iterator::MultipartHeader()
{
	return fForm->_GetMultipartHeader(fPrevElement);
}


BHttpForm::Iterator&
BHttpForm::Iterator::operator=(const Iterator& other)
{
	fForm = other.fForm;
	fStdIterator = other.fStdIterator;
	fElement = other.fElement;
	fPrevElement = other.fPrevElement;

	return *this;
}


void
BHttpForm::Iterator::_FindNext()
{
	fPrevElement = fElement;

	if (fStdIterator != fForm->fFields.end()) {
		fElement = &fStdIterator->second;
		fStdIterator++;
	} else
		fElement = NULL;
}
