/* --------------------------------------------------------------------------

   libmusicbrainz5 - Client library to access MusicBrainz

   Copyright (C) 2012 Andrew Hawkins

   This file is part of libmusicbrainz5.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   libmusicbrainz5 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this library.  If not, see <http://www.gnu.org/licenses/>.

     $Id$

----------------------------------------------------------------------------*/

//http://musicbrainz.org/doc/XML_Web_Service/Version_2
//http://bugs.musicbrainz.org/browser/mmd-schema/trunk/schema/musicbrainz_mmd-2.0.rng
//http://wiki.musicbrainz.org/Picard_Tag_Mapping

#include "config.h"
#include "musicbrainz5/defines.h"

#include "musicbrainz5/Query.h"

#include <sstream>
#include <iostream>
#include <cstdlib>

#include <string.h>
#include <unistd.h>
#include <sys/time.h>

#include <ne_uri.h>

#include "musicbrainz5/HTTPFetch.h"
#include "musicbrainz5/Disc.h"
#include "musicbrainz5/Message.h"
#include "musicbrainz5/ReleaseList.h"
#include "musicbrainz5/Release.h"

class MusicBrainz5::CQueryPrivate
{
	public:
		CQueryPrivate()
		:	m_Port(80),
			m_ProxyPort(0),
			m_LastResult(CQuery::eQuery_Success),
			m_LastHTTPCode(200)
		{
		}

		std::string m_UserAgent;
		std::string m_Server;
		int m_Port;
		std::string m_UserName;
		std::string m_Password;
		std::string m_ProxyHost;
		int m_ProxyPort;
		std::string m_ProxyUserName;
		std::string m_ProxyPassword;
		CQuery::tQueryResult m_LastResult;
		int m_LastHTTPCode;
		std::string m_LastErrorMessage;
};

MusicBrainz5::CQuery::CQuery(const std::string& UserAgent, const std::string& Server, int Port)
:	m_d(new CQueryPrivate)
{
	m_d->m_UserAgent=UserAgent;
	m_d->m_Server=Server;
	m_d->m_Port=Port;
}

MusicBrainz5::CQuery::~CQuery()
{
	delete m_d;
}

void MusicBrainz5::CQuery::SetUserName(const std::string& UserName)
{
	m_d->m_UserName=UserName;
}

void MusicBrainz5::CQuery::SetPassword(const std::string& Password)
{
	m_d->m_Password=Password;
}

void MusicBrainz5::CQuery::SetProxyHost(const std::string& ProxyHost)
{
	m_d->m_ProxyHost=ProxyHost;
}

void MusicBrainz5::CQuery::SetProxyPort(int ProxyPort)
{
	m_d->m_ProxyPort=ProxyPort;
}

void MusicBrainz5::CQuery::SetProxyUserName(const std::string& ProxyUserName)
{
	m_d->m_ProxyUserName=ProxyUserName;
}

void MusicBrainz5::CQuery::SetProxyPassword(const std::string& ProxyPassword)
{
	m_d->m_ProxyPassword=ProxyPassword;
}

MusicBrainz5::CMetadata MusicBrainz5::CQuery::PerformQuery(const std::string& Query)
{
	WaitRequest();

	CMetadata Metadata;

	CHTTPFetch Fetch(UserAgent(),m_d->m_Server,m_d->m_Port);

	if (!m_d->m_UserName.empty())
		Fetch.SetUserName(m_d->m_UserName);

	if (!m_d->m_Password.empty())
		Fetch.SetPassword(m_d->m_Password);

	if (!m_d->m_ProxyHost.empty())
		Fetch.SetProxyHost(m_d->m_ProxyHost);

	if (0!=m_d->m_ProxyPort)
		Fetch.SetProxyPort(m_d->m_ProxyPort);

	if (!m_d->m_ProxyUserName.empty())
		Fetch.SetProxyUserName(m_d->m_ProxyUserName);

	if (!m_d->m_ProxyPassword.empty())
		Fetch.SetProxyPassword(m_d->m_ProxyPassword);

	try
	{
		int Ret=Fetch.Fetch(Query);

#ifdef _MB5_DEBUG_
		std::cerr << "Ret: " << Ret << std::endl;
#endif

		if (Ret>0)
		{
			std::vector<unsigned char> Data=Fetch.Data();
			std::string strData(Data.begin(),Data.end());

#ifdef _MB5_DEBUG_
			std::cerr << "Ret is '" << strData << "'" << std::endl;
#endif

			XMLResults Results;
			XMLNode *TopNode = XMLRootNode::parseString(strData, &Results);
			if (Results.code==eXMLErrorNone)
			{
				XMLNode MetadataNode=*TopNode;
				if (!MetadataNode.isEmpty())
				{
					Metadata=CMetadata(MetadataNode);
				}
			}
			delete TopNode;
		}
	}

	catch (CConnectionError& Error)
	{
		m_d->m_LastResult=CQuery::eQuery_ConnectionError;
		m_d->m_LastHTTPCode=Fetch.Status();
		m_d->m_LastErrorMessage=Fetch.ErrorMessage();

		throw;
	}

	catch (CTimeoutError& Error)
	{
		m_d->m_LastResult=CQuery::eQuery_Timeout;
		m_d->m_LastHTTPCode=Fetch.Status();
		m_d->m_LastErrorMessage=Fetch.ErrorMessage();

		throw;
	}

	catch (CAuthenticationError& Error)
	{
		m_d->m_LastResult=CQuery::eQuery_AuthenticationError;
		m_d->m_LastHTTPCode=Fetch.Status();
		m_d->m_LastErrorMessage=Fetch.ErrorMessage();

		throw;
	}

	catch (CFetchError& Error)
	{
		m_d->m_LastResult=CQuery::eQuery_FetchError;
		m_d->m_LastHTTPCode=Fetch.Status();
		m_d->m_LastErrorMessage=Fetch.ErrorMessage();

		throw;
	}

	catch (CRequestError& Error)
	{
		m_d->m_LastResult=CQuery::eQuery_RequestError;
		m_d->m_LastHTTPCode=Fetch.Status();
		m_d->m_LastErrorMessage=Fetch.ErrorMessage();

		throw;
	}

	catch (CResourceNotFoundError& Error)
	{
		m_d->m_LastResult=CQuery::eQuery_ResourceNotFound;
		m_d->m_LastHTTPCode=Fetch.Status();
		m_d->m_LastErrorMessage=Fetch.ErrorMessage();

		throw;
	}

	return Metadata;
}

MusicBrainz5::CMetadata MusicBrainz5::CQuery::Query(const std::string& Entity, const std::string& ID, const std::string& Resource, const tParamMap& Params)
{
	std::stringstream os;

	os << "/ws/2/" << Entity;

	if (!ID.empty())
	{
		os << "/" << ID;

		if (!Resource.empty())
			os << "/" << Resource;
	}

	if (!Params.empty())
		os << "?" << URLEncode(Params);

#ifdef _MB5_DEBUG_
	std::cerr << "Query is '" << os.str() << "'" << std::endl;
#endif

	return PerformQuery(os.str());
}

MusicBrainz5::CReleaseList MusicBrainz5::CQuery::LookupDiscID(const std::string& DiscID)
{
	//Will this work soon (and return disc IDs as well)?
	//http://musicbrainz.org/ws/2/discid/arIS30RPWowvwNEqsqdDnZzDGhk-?inc=artists+labels+recordings+release-groups+artist-credits

	MusicBrainz5::CReleaseList ReleaseList;

	CMetadata Metadata=Query("discid",DiscID);

	CDisc *Disc=Metadata.Disc();
	if (Disc && Disc->ReleaseList())
		ReleaseList=*Disc->ReleaseList();

	return ReleaseList;
}

MusicBrainz5::CRelease MusicBrainz5::CQuery::LookupRelease(const std::string& ReleaseID)
{
	MusicBrainz5::CRelease Release;

	tParamMap Params;
	Params["inc"]="artists labels recordings release-groups url-rels discids artist-credits";

	CMetadata Metadata=Query("release",ReleaseID,"",Params);
	if (Metadata.Release())
		Release=*Metadata.Release();

	return Release;
}

void MusicBrainz5::CQuery::WaitRequest() const
{
	if (m_d->m_Server.find("musicbrainz.org")!=std::string::npos)
	{
		static struct timeval LastRequest;
		const int TimeBetweenRequests=2;

		struct timeval TimeNow;
		gettimeofday(&TimeNow,0);

		if (LastRequest.tv_sec!=0 || LastRequest.tv_usec!=0)
		{
			struct timeval Diff;

			do
			{
				gettimeofday(&TimeNow,0);
				timersub(&TimeNow,&LastRequest,&Diff);

				if (Diff.tv_sec<TimeBetweenRequests)
					usleep(100000);
			}	while (Diff.tv_sec<TimeBetweenRequests);
		}

		memcpy(&LastRequest,&TimeNow,sizeof(LastRequest));
	}
}

bool MusicBrainz5::CQuery::AddCollectionEntries(const std::string& CollectionID, const std::vector<std::string>& Entries)
{
	return EditCollection(CollectionID,Entries,"PUT");
}

bool MusicBrainz5::CQuery::DeleteCollectionEntries(const std::string& CollectionID, const std::vector<std::string>& Entries)
{
	return EditCollection(CollectionID,Entries,"DELETE");
}

bool MusicBrainz5::CQuery::EditCollection(const std::string& CollectionID, const std::vector<std::string>& Entries, const std::string& Action)
{
	bool RetVal=false;

	std::vector<std::string> ToProcess=Entries;

	while (!ToProcess.empty())
	{
		std::string Query;

		Query="/ws/2/collection/"+CollectionID+"/releases/";

		std::vector<std::string> ThisBatch;
		while (!ToProcess.empty() && ThisBatch.size()<25)
		{
			ThisBatch.push_back(ToProcess.back());
			ToProcess.pop_back();
		}

		std::vector<std::string>::const_iterator ThisRelease=ThisBatch.begin();
		while(ThisRelease!=ThisBatch.end())
		{
			if (ThisRelease!=ThisBatch.begin())
				Query+=";";

			Query+=*ThisRelease;

			++ThisRelease;
		}

		Query+="?client="+m_d->m_UserAgent;

		CHTTPFetch Fetch(UserAgent(),m_d->m_Server,m_d->m_Port);

		if (!m_d->m_UserName.empty())
			Fetch.SetUserName(m_d->m_UserName);

		if (!m_d->m_Password.empty())
			Fetch.SetPassword(m_d->m_Password);

		if (!m_d->m_ProxyHost.empty())
			Fetch.SetProxyHost(m_d->m_ProxyHost);

		if (0!=m_d->m_ProxyPort)
			Fetch.SetProxyPort(m_d->m_ProxyPort);

		if (!m_d->m_ProxyUserName.empty())
			Fetch.SetProxyUserName(m_d->m_ProxyUserName);

		if (!m_d->m_ProxyPassword.empty())
			Fetch.SetProxyPassword(m_d->m_ProxyPassword);

		try
		{
#ifdef _MB5_DEBUG_
			std::cerr << "Collection " << Action << " Query is '" << Query << "'" << std::endl;
#endif

			int Ret=Fetch.Fetch(Query,Action);

#ifdef _MB5_DEBUG_
			std::cerr << "Collection Ret: " << Ret << std::endl;
#endif

			if (Ret>0)
			{
				std::vector<unsigned char> Data=Fetch.Data();
				std::string strData(Data.begin(),Data.end());

#ifdef _MB5_DEBUG_
				std::cerr << "Collection " << Action << " ret is '" << strData << "'" << std::endl;
#endif

				XMLResults Results;
				XMLNode *TopNode = XMLRootNode::parseString(strData, &Results);
				if (Results.code==eXMLErrorNone)
				{
					XMLNode MetadataNode=*TopNode;
					if (!MetadataNode.isEmpty())
					{
						CMetadata Metadata(MetadataNode);

						if (Metadata.Message() && Metadata.Message()->Text()=="OK")
							RetVal=RetVal && true;
					}
				}
				delete TopNode;
			}
		}

		catch (CConnectionError& Error)
		{
			m_d->m_LastResult=CQuery::eQuery_ConnectionError;
			m_d->m_LastHTTPCode=Fetch.Status();
			m_d->m_LastErrorMessage=Fetch.ErrorMessage();

			throw;
		}

		catch (CTimeoutError& Error)
		{
			m_d->m_LastResult=CQuery::eQuery_Timeout;
			m_d->m_LastHTTPCode=Fetch.Status();
			m_d->m_LastErrorMessage=Fetch.ErrorMessage();

			throw;
		}

		catch (CAuthenticationError& Error)
		{
			m_d->m_LastResult=CQuery::eQuery_AuthenticationError;
			m_d->m_LastHTTPCode=Fetch.Status();
			m_d->m_LastErrorMessage=Fetch.ErrorMessage();

			throw;
		}

		catch (CFetchError& Error)
		{
			m_d->m_LastResult=CQuery::eQuery_FetchError;
			m_d->m_LastHTTPCode=Fetch.Status();
			m_d->m_LastErrorMessage=Fetch.ErrorMessage();

			throw;
		}

		catch (CRequestError& Error)
		{
			m_d->m_LastResult=CQuery::eQuery_RequestError;
			m_d->m_LastHTTPCode=Fetch.Status();
			m_d->m_LastErrorMessage=Fetch.ErrorMessage();

			throw;
		}

		catch (CResourceNotFoundError& Error)
		{
			m_d->m_LastResult=CQuery::eQuery_ResourceNotFound;
			m_d->m_LastHTTPCode=Fetch.Status();
			m_d->m_LastErrorMessage=Fetch.ErrorMessage();

			throw;
		}
	}

	return RetVal;
}

std::string MusicBrainz5::CQuery::UserAgent() const
{
	std::string UserAgent=m_d->m_UserAgent;
	if (!UserAgent.empty())
		UserAgent+=" ";
	UserAgent+=PACKAGE "/v" VERSION;

	return UserAgent;
}

std::string MusicBrainz5::CQuery::URIEscape(const std::string &URI)
{
	char *EscURIStr = ne_path_escape(URI.c_str());
	std::string EscURI((const char *)EscURIStr);
	free(EscURIStr);
	return EscURI;
}

std::string MusicBrainz5::CQuery::URLEncode(const std::map<std::string,std::string>& Params)
{
	std::string EncodedStr;

	for (std::map<std::string,std::string>::const_iterator i = Params.begin(); i != Params.end(); i++)
	{
		std::string Name = (*i).first;
		std::string Value = (*i).second;

		if (i!=Params.begin())
			EncodedStr += "&";

		EncodedStr += Name + "=" + URIEscape(Value);
	}
	return EncodedStr;
}

MusicBrainz5::CQuery::tQueryResult MusicBrainz5::CQuery::LastResult() const
{
	return m_d->m_LastResult;
}

int MusicBrainz5::CQuery::LastHTTPCode() const
{
	return m_d->m_LastHTTPCode;
}

std::string MusicBrainz5::CQuery::LastErrorMessage() const
{
	return m_d->m_LastErrorMessage;
}

std::string MusicBrainz5::CQuery::Version() const
{
	return PACKAGE "-v" VERSION;
}
