/*
    libmaus2
    Copyright (C) 2020 German Tischler

    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, see <http://www.gnu.org/licenses/>.
*/
#include <libmaus2/network/CurlObject.hpp>

#if defined(LIBMAUS2_HAVE_LIBCURL)
#include <curl/curl.h>
#endif

libmaus2::network::CurlObject::CurlObject(
	std::string const &
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		url
		#endif
		,
	libmaus2::network::CurlResponseAcceptor *
		racceptor
		,
	uint64_t const
		freelistsize
)
: vcurl(nullptr), AFLalloc(16*1024), sem(), AFL(freelistsize,AFLalloc), outputQueue(),
  response(-1), responseSet(false), lastResponse(-1), headerstr(), acceptor(racceptor)
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	CURL * curl = nullptr;

	if ( !
		(curl = curl_easy_init())
	)
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "CurlObject::CurlObject: curl_easy_init failed" << std::endl;
		lme.finish();
		throw lme;
	}
	for ( uint64_t i = 0; i < freelistsize; ++i )
		sem.post();

	CURLcode const retsetopt = curl_easy_setopt(curl,CURLOPT_WRITEDATA,this);

	if ( retsetopt != CURLE_OK )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "CurlObject::CurlObject: curl_easy_setopt failed: " << curl_easy_strerror(retsetopt) << std::endl;
		lme.finish();
		throw lme;

	}

	CURLcode const retsetoptwrite = curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,write_callback_static);

	if ( retsetoptwrite != CURLE_OK )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "CurlObject::CurlObject: curl_easy_setopt failed: " << curl_easy_strerror(retsetoptwrite) << std::endl;
		lme.finish();
		throw lme;

	}

	CURLcode const retsetseturl = curl_easy_setopt(curl, CURLOPT_URL, url.c_str());

	if ( retsetseturl != CURLE_OK )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "CurlObject::CurlObject: curl_easy_setopt failed: " << curl_easy_strerror(retsetseturl) << std::endl;
		lme.finish();
		throw lme;

	}

	CURLcode const retsetfollow = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1 /* enable */);

	if ( retsetfollow != CURLE_OK )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "CurlObject::CurlObject: curl_easy_setopt failed: " << curl_easy_strerror(retsetfollow) << std::endl;
		lme.finish();
		throw lme;

	}

	CURLcode const retsetoptheaderdata = curl_easy_setopt(curl,CURLOPT_HEADERDATA,this);

	if ( retsetoptheaderdata != CURLE_OK )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "CurlObject::CurlObject: curl_easy_setopt failed: " << curl_easy_strerror(retsetoptheaderdata) << std::endl;
		lme.finish();
		throw lme;

	}

	CURLcode const retsetoptheader = curl_easy_setopt(curl,CURLOPT_HEADERFUNCTION,header_callback_static);

	if ( retsetoptheader != CURLE_OK )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "CurlObject::CurlObject: curl_easy_setopt failed: " << curl_easy_strerror(retsetoptheader) << std::endl;
		lme.finish();
		throw lme;

	}

	vcurl = reinterpret_cast<void *>(curl);
	#else
	libmaus2::exception::LibMausException lme;
	lme.getStream() << "CurlObject::CurlObject: libmaus2 is compiled without support for libcurl" << std::endl;
	lme.finish();
	throw lme;
	#endif
}

std::pair<int,std::string> libmaus2::network::CurlObject::perform()
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	CURL * curl = reinterpret_cast<CURL *>(vcurl);
	CURLcode const res = curl_easy_perform(curl);
	std::string const errs = curl_easy_strerror(res);
	return std::pair<int,std::string>(static_cast<int>(res),errs);
	#else
	return std::pair<int,std::string>(static_cast<int>(-1),std::string("libcurl not enabled in libmaus2"));
	#endif
}

::std::size_t libmaus2::network::CurlObject::read(
	char *
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		p
	#endif
		,
	::std::size_t
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		n
	#endif
)
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	::std::size_t r = 0;

	while ( n )
	{
		try
		{
			CurlOutputBlock OB = outputQueue.deque();

			#if 0
			assert ( static_cast<bool>(responseSet) );
			long const lresponse = static_cast<long>(response);
			#endif

			::std::size_t const av = OB.e - OB.a;
			::std::size_t const use = std::min(n,av);

			std::copy(OB.a,OB.a+use,p);

			p += use;
			n -= use;
			r += use;
			OB.a += use;

			if ( OB.a != OB.e )
			{
				outputQueue.putback(OB);
			}
			else
			{
				if ( OB.aptr )
				{
					AFL.put(OB.aptr);
					sem.post();
				}
			}
		}
		catch(std::exception const &)
		{
			n = 0;
		}
	}

	return r;
	#else
	return 0;
	#endif
}

std::size_t libmaus2::network::CurlObject::write_callback(
	char *
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		ptr
	#endif
		,
	std::size_t
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		size
	#endif
		,
	std::size_t
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		nmemb
	#endif
)
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	CURL * curl = reinterpret_cast<CURL *>(vcurl);

	if ( ! static_cast<bool>(responseSet) )
	{
		long lresponse = -1;
		CURLcode const res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &lresponse);

		if ( res != CURLE_OK )
		{
			outputQueue.terminate();
			return 0;
		}

		response = lresponse;
		lastResponse = lresponse;
		responseSet = true;

		if (
			acceptor
			&&
			!(*acceptor)(lresponse)
		)
		{
			outputQueue.terminate();
			return 0;
		}
	}
	{
		long lresponse = -1;
		CURLcode const res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &lresponse);

		if ( res == CURLE_OK )
			lastResponse = lresponse;

		if (
			acceptor
			&&
			!(*acceptor)(lresponse)
		)
		{
			outputQueue.terminate();
			return 0;
		}
	}

	::std::size_t const n = size * nmemb;

	sem.wait();

	assert ( ! AFL.empty() );
	libmaus2::util::AutoArrayCharBaseTypeInfo::pointer_type aptr = AFL.get();
	assert ( aptr );

	try
	{
		aptr->ensureSize(n);
	}
	catch(...)
	{
		outputQueue.terminate();
		return 0;
	}

	if ( n == 0 )
	{
		outputQueue.terminate();
		return n;
	}

	std::copy(ptr,ptr+n,aptr->get());

	try
	{
		outputQueue.enque(
			CurlOutputBlock(aptr,n)
		);
	}
	catch(...)
	{
		outputQueue.terminate();
		return 0;
	}

	return n;
	#else
	return 0;
	#endif
}

std::size_t libmaus2::network::CurlObject::header_callback(
	char *
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		ptr
	#endif
		,
	std::size_t
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		size
	#endif
		,
	std::size_t
	#if defined(LIBMAUS2_HAVE_LIBCURL)
		nmemb
	#endif
)
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	try
	{
		headerstr.write(ptr,size*nmemb);
		return size*nmemb;
	}
	catch(...)
	{
		return 0;
	}
	#else
	return 0;
	#endif
}

::std::size_t libmaus2::network::CurlObject::write_callback_static(
	char *
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		ptr
		#endif
		,
	std::size_t
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		size
		#endif
		,
	std::size_t
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		nmemb
		#endif
		,
	void *
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		userdata
		#endif
)
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	CurlObject * CO = reinterpret_cast<CurlObject *>(userdata);
	return CO->write_callback(ptr,size,nmemb);
	#else
	return 0;
	#endif
}

::std::size_t libmaus2::network::CurlObject::header_callback_static(
	char *
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		ptr
		#endif
		,
	std::size_t
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		size
		#endif
		,
	std::size_t
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		nmemb
		#endif
		,
	void *
		#if defined(LIBMAUS2_HAVE_LIBCURL)
		userdata
		#endif
)
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	CurlObject * CO = reinterpret_cast<CurlObject *>(userdata);
	assert ( CO );
	return CO->header_callback(ptr,size,nmemb);
	#else
	return 0;
	#endif
}

libmaus2::network::CurlObject::~CurlObject()
{
	#if defined(LIBMAUS2_HAVE_LIBCURL)
	CURL * curl = reinterpret_cast<CURL *>(vcurl);
	curl_easy_cleanup(curl);
	#endif
}
