//------------------------------------------------------------------------------
// <copyright file="CurlUtils.cpp" company="LiquidRobotics">
//
// Copyright (c) 2014 Liquid Robotics, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This software is associated with a release.  Liquid Robotics makes no
// representation that the design, interfaces or component distribution will
// remain unchanged in future releases.
//
// </copyright>
//
// <summary>
//   CurlUtils wraps the curl library and provides implementations needed by SV3
//   remoting.
// </summary>
//
// <owner>Mike Cookson</owner>
//------------------------------------------------------------------------------

#include "CurlUtils.h"
#include "LqrSMACByteArray.h"
#include "LqrSMACLogger.h"
#include <sstream>

using namespace std;
using namespace LiquidRobotics::SMACFramework;

namespace LiquidRobotics {
namespace SV3RemotingLib {
namespace CurlUtils {

CurlEasyException::CurlEasyException(CURLcode _code, const std::string& whatArg) throw()
: runtime_error(whatArg)
, code(_code)
{
}

CurlFormException::CurlFormException(CURLFORMcode _code, const std::string& whatArg) throw()
: runtime_error(whatArg)
, code(_code)
{
}

CurlMultiException::CurlMultiException(CURLMcode _code, const std::string& whatArg) throw()
: runtime_error(whatArg)
, code(_code)
{
}

IMPLEMENT_EXCEPTION(ConnectionFailure,std::runtime_error)
IMPLEMENT_EXCEPTION(ServerDidNotRespond,std::runtime_error)

FormAddFileCopyFromString::FormAddFileCopyFromString(curl_httppost*& formpost, curl_httppost*& lastptr, const string& fileName, const string& content, bool contentIsFileName)
{
	ByteArray baData(content.begin(), content.end());
	const string tfname = contentIsFileName ? content : tf.GetName();
	if (!contentIsFileName)
	{
		tf.Write(baData);
		tf.Close();
	}

	ThrowFormAddError(
			curl_formadd(
				&formpost,
				&lastptr,
				CURLFORM_COPYNAME, "file",
				CURLFORM_FILE, tfname.c_str(),
				CURLFORM_END)
				);

	ThrowFormAddError(
			curl_formadd(
				&formpost,
				&lastptr,
				CURLFORM_COPYNAME, "filename",
				CURLFORM_COPYCONTENTS, tfname.c_str(),
				CURLFORM_END)
				);

	ThrowFormAddError(
			curl_formadd(
				&formpost,
				&lastptr,
				CURLFORM_COPYNAME, "submit",
				CURLFORM_COPYCONTENTS, "send",
				CURLFORM_END)
				);
}

CurlOperationBase::CurlOperationBase(const string& _url)
: curl(NULL)
, multi_handle(NULL)
, url(_url)
, pThis(NULL)
{
	pThis = this;
}

CurlOperationBase::~CurlOperationBase()
{
	cleanup();
}

void CurlOperationBase::InitCurl(bool multiInit)
{
	if (!(curl = curl_easy_init()))
		throw runtime_error("curl_easy_init() failed");

	if (multiInit && !(multi_handle = curl_multi_init()))
		throw runtime_error("curl_multi_init() failed");

	SetOpt(CURLOPT_WRITEFUNCTION, &CurlOperationBase::WriteDataFunc);
	SetOpt(CURLOPT_HEADERFUNCTION, &CurlOperationBase::WriteHeaderFunc);
	SetOpt(CURLOPT_WRITEHEADER, &pThis);
	SetOpt(CURLOPT_WRITEDATA, &pThis);
}

void CurlOperationBase::cleanup()
{
	if (multi_handle) {
		curl_multi_cleanup(multi_handle);
		multi_handle = NULL;
	}

	if (curl) {
		curl_easy_cleanup(curl);
		curl = NULL;
	}
}

size_t CurlOperationBase::WriteDataFunc(char* ptr, size_t size, size_t nmemb, void* userdata)
{
	CurlOperationBase* pThis = *((CurlOperationBase**) userdata);
	return pThis->WriteData(ptr, size, nmemb);
}

size_t CurlOperationBase::WriteData(char* ptr, size_t size, size_t nmemb)
{
	const size_t newBytes = size*nmemb;
	size_t result;

	try
	{
		body.insert(body.end(), ptr, ptr+newBytes);
		result = newBytes;
	}
	catch(...)
	{
		result = 0;
	}

	return result;
}

size_t CurlOperationBase::WriteHeaderFunc(void *ptr, size_t size, size_t nmemb, void *userdata)
{
	CurlOperationBase* pThis = *((CurlOperationBase**) userdata);
	return pThis->WriteHeader(ptr, size, nmemb);
}

size_t CurlOperationBase::WriteHeader(void *ptr, size_t size, size_t nmemb)
{
	size_t result;

	try
	{
		const char* p2 = (const char*) ptr;
		const size_t newBytes = size*nmemb;
		const string header(p2, p2+newBytes);

		headers.push_back(header);
		result = newBytes;
	}
	catch(...)
	{
		result = 0;
	}

	return result;
}

PostFile::PostFile(const string& _url, const string& fileName, const string& fileContent, bool contentIsFileName, bool verbose, long curlTimeout)
: CurlOperationBase(url)
, formpost(NULL)
, lastptr(NULL)
, headerlist(NULL)
{
	const bool debugoutput = false;
	const string expect = "Expect:";
	int still_running;
	string tempContent = fileContent;
	tempContent.push_back(0);

	// set up forms for file transfer
	FormAddFileCopyFromString fafcfs(formpost, lastptr, fileName, tempContent, contentIsFileName);

	InitCurl(true);

	// initialize custom header to filter out 100-continue
	//headerlist = curl_slist_append(headerlist, contentType.c_str());
	headerlist = curl_slist_append(headerlist, expect.c_str());

	// set options for file transfer using post
	SetOpt(CURLOPT_URL, url.c_str());
	SetOpt(CURLOPT_NOSIGNAL, 1L);
	SetOpt(CURLOPT_CONNECTTIMEOUT, 10);
	SetOpt(CURLOPT_TIMEOUT, curlTimeout+1);
	SetOpt(CURLOPT_FTP_RESPONSE_TIMEOUT, curlTimeout);
	if (verbose)
		SetOpt(CURLOPT_VERBOSE, 1L);
	SetOpt(CURLOPT_HTTPHEADER, headerlist);
	SetOpt(CURLOPT_HTTPPOST, formpost);

	// perform the transfer
	ThrowMulti(curl_multi_add_handle(multi_handle, curl), "curl_multi_add_handle()");
	ThrowMulti(curl_multi_perform(multi_handle, &still_running), "curl_multi_perform()");

	do
	{
		MyTimeval timeout;
		MyFdSet fdread, fdwrite, fdexcept;
		int maxfd = -1;
		long curltimeout = -1;

		ThrowMulti(curl_multi_timeout(multi_handle, &curltimeout), "curl_multi_timoeut");
		if (debugoutput)
			cout << "to:" << curltimeout << endl;
		timeout.Reset(curltimeout);
		ThrowMulti(curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcept, &maxfd), "curl_multi_fdset()");
		int rc = select(maxfd+1, &fdread, &fdwrite, &fdexcept, &timeout);
		if (debugoutput)
			cout << "rc:" << rc << " r:" << fdread.HasBits() << " w:" << fdwrite.HasBits() << " e:" << fdexcept.HasBits() << endl;
		if (rc == -1) throw runtime_error("select() failed while performing file transfer");
		ThrowMulti(curl_multi_perform(multi_handle, &still_running), "curl_multi_perform()");

		if (!still_running)
		{
			if (debugoutput)
			{
				ThrowMulti(curl_multi_timeout(multi_handle, &curltimeout), "curl_multi_timoeut");
				cout << "to:" << curltimeout << endl;
			}

			double d = -1.0;
			ThrowEasy(curl_easy_getinfo(curl, CURLINFO_CONNECT_TIME, &d), "curl_easy_getinfo()");
			if (debugoutput) cout << "connect: " << d << endl;
			if (d <= 0)
			{
				stringstream msg;
				msg << "Failed to connect to " << url;
				throw ConnectionFailure(msg.str());
			}
		}

	} while(still_running);

	if (headers.size() == 0)
	{
		stringstream msg;
		msg << "No response from " << url;
		throw ServerDidNotRespond(msg.str());
	}
}

PostFile::~PostFile()
{
	if (formpost)		curl_formfree(formpost);
	if (headerlist)		curl_slist_free_all(headerlist);
}

PostFile::MyFdSet::MyFdSet()
{
	FD_ZERO(this);
}

bool PostFile::MyFdSet::HasBits() const
{
	bool result = false;

	for (int fdNum = 0; !result && (fdNum < FD_SETSIZE); ++fdNum)
		if (FD_ISSET(fdNum, this))
			result = true;

	return result;
}


PostFile::MyTimeval::MyTimeval()
{
	tv_usec = 1;
	tv_sec = 0;
}

void PostFile::MyTimeval::Reset(long timeout_value)
{
	*this = MyTimeval();

	if (timeout_value >= 0)
	{
		tv_sec = timeout_value / 1000;
		tv_usec = (timeout_value % 1000) * 1000;
	}
}

PostFile2::PostFile2(const string& _url, const string& fileName, const string& fileContent,
		LiquidRobotics::SMACFramework::Logger& logger, bool contentIsFileName, bool verbose, long curlTimeout)
: CurlOperationBase(_url)
, formpost(NULL)
, lastptr(NULL)
, headerlist(NULL)
{
	Logger::Locked(logger)->Debug << "HTTP POST " << url << endl;
	const string expect = "Expect:";
	string tempContent = fileContent;
	tempContent.push_back(0);

	// set up forms for file transfer
	FormAddFileCopyFromString fafcfs(formpost, lastptr, fileName, tempContent, contentIsFileName);

	InitCurl();

	// initialize custom header to filter out 100-continue
	//headerlist = curl_slist_append(headerlist, contentType.c_str());
	headerlist = curl_slist_append(headerlist, expect.c_str());

	// set options for file transfer using post
	SetOpt(CURLOPT_URL, url.c_str());
	SetOpt(CURLOPT_NOSIGNAL, 1L);
	SetOpt(CURLOPT_CONNECTTIMEOUT, 10);
	SetOpt(CURLOPT_TIMEOUT, curlTimeout+1);
	SetOpt(CURLOPT_FTP_RESPONSE_TIMEOUT, curlTimeout);
	if (verbose)
		SetOpt(CURLOPT_VERBOSE, 1L);
	SetOpt(CURLOPT_HTTPHEADER, headerlist);
	SetOpt(CURLOPT_HTTPPOST, formpost);

	ThrowEasy(curl_easy_perform(curl), "curl_easy_perform");

	double d = -1.0;
	ThrowEasy(curl_easy_getinfo(curl, CURLINFO_CONNECT_TIME, &d), "curl_easy_getinfo()");
	if (d <= 0)
	{
		stringstream msg;
		msg << "Failed to connect to " << url;
		throw ConnectionFailure(msg.str());
	}

	if (headers.size() == 0)
	{
		stringstream msg;
		msg << "No response from " << url;
		throw ServerDidNotRespond(msg.str());
	}
}

PostFile2::~PostFile2()
{
	if (formpost)		curl_formfree(formpost);
	if (headerlist)		curl_slist_free_all(headerlist);
}

HttpGet::HttpGet(const string& _url, LiquidRobotics::SMACFramework::Logger& logger, bool _verbose, long _curlTimeout)
: CurlOperationBase(_url)
, verbose(_verbose)
, curlTimeout(_curlTimeout)
{
	Logger::Locked(logger)->Debug << "HTTP GET " << url << endl;
	InitCurl();

	// set options for http get
	SetOpt(CURLOPT_URL, url.c_str());
	SetOpt(CURLOPT_NOPROGRESS, 1L);
	SetOpt(CURLOPT_NOSIGNAL, 1L);
	SetOpt(CURLOPT_CONNECTTIMEOUT, 10);
	SetOpt(CURLOPT_TIMEOUT, curlTimeout+1);
	SetOpt(CURLOPT_FTP_RESPONSE_TIMEOUT, curlTimeout);
	SetOpt(CURLOPT_VERBOSE, verbose ? 1L : 0L);

	ThrowEasy(curl_easy_perform(curl), "curl_easy_perform");

	double d = -1.0;
	ThrowEasy(curl_easy_getinfo(curl, CURLINFO_CONNECT_TIME, &d), "curl_easy_getinfo()");
	if (d <= 0)
	{
		stringstream msg;
		msg << "Failed to connect to " << url;
		throw ConnectionFailure(msg.str());
	}

	if (headers.size() == 0)
	{
		stringstream msg;
		msg << "No response from " << url;
		throw ServerDidNotRespond(msg.str());
	}
}

HttpGet::~HttpGet()
{
}

void HttpGet::DebugOut(ostream& stm) const
{
	stm << headers.size() << " Headers:" << endl;
	if (headers.size() != 0) stm << endl;
	unsigned hdr = 0;
	for (StringArray::const_iterator ci = headers.begin(); ci != headers.end(); ++ci)
		stm << ++hdr << ") " << *ci << endl;
	stm << endl
		<< body.length() << " Body:" << endl;
	if (body.length()) stm << endl << body << endl;
}

void HttpGet::Close()
{
	if (curl)
	{
		curl_easy_cleanup(curl);
		curl = NULL;
	}
}

SHARED_LIB_PUBLIC void ThrowEasy(CURLcode code, const std::string& func)
{
	if (code != 0)
	{
		stringstream ss;
		ss << func << " returned " << code << " (" << curl_easy_strerror(code) << ")";
		const string msg = ss.str();
		throw CurlEasyException(code, msg);
	}
}

SHARED_LIB_PUBLIC void ThrowFormAddError(CURLFORMcode res)
{
	if (res != 0)
	{
		stringstream ss;
		ss << "curl_formadd() returned " << res;
		const string msg = ss.str();
		throw CurlFormException(res, msg);
	}
}

SHARED_LIB_PUBLIC void ThrowMulti(CURLMcode code, const std::string& func)
{
	if (code != 0)
	{
		stringstream ss;
		ss << func << " returned " << code << " (" << curl_multi_strerror(code) << ")";
		const string msg = ss.str();
		throw CurlMultiException(code, msg);
	}
}

}
}
}
