#include "EABaseTypesHTTP/proxyhttpplugin.h"

#include "UTFInternet/HTTPServer.h"

#include "UTFXml/XmlWriter.h"
#include "EAJson/JsonWriter.h"

#include "EABaseTypesMessage/messageproxy.h"
#include "EABaseTypesMessage/deletable.h"

#include "EABaseTypesPlus/vrefcountedica.h"

#include "EABaseTypes/functioncallwrappers.h"

#include "EAMessage/EAMessage.h"

#include "eastdc/eastopwatch.h"

#include "EABaseTypesJson/basetypejsonparser.h"
#include "EABaseTypesJson/rawstringwritestream.h"
#include "EABaseTypesJson/jsonencoder.h"

#include "EABaseTypesXml/basetypexmlparser.h"
#include "EABaseTypesXml/xmlencoder.h"

namespace EA
{
	namespace Types
	{
		//lint -esym(1925, EA::Types::RawStringStream::mString)
		class RawStringStream : public EA::IO::IStream
		{
		public:
		
			//lint -efunc(1401, EA::Types::RawStringStream::RawStringWriteStream)
			RawStringStream(EA::RawString* str):mString(str) {}
			virtual ~RawStringStream() {}

			virtual int                 AddRef() { return 0; }
			virtual int                 Release() { return 0; }
			virtual uint32_t            GetType() const { return 0; }
			virtual int                 GetAccessFlags() const { return 0; }
			virtual int                 GetState() const { return 0; }
			virtual bool                Close() { return false; }
			virtual EA::IO::size_type   GetSize() const { return 0; }
			virtual bool                SetSize(EA::IO::size_type) { return false; }
			virtual EA::IO::off_type    GetPosition(EA::IO::PositionType) const { return 0; }
			virtual bool                SetPosition(EA::IO::off_type, EA::IO::PositionType) { return false; }
			virtual EA::IO::size_type   GetAvailable() const { return 0; }
			virtual EA::IO::size_type   Read(void*, EA::IO::size_type) { return 0; }
			virtual bool                Flush() { return false; }

			virtual bool Write(const void* pData, EA::IO::size_type nSize)
			{
				mString->append(static_cast<const char8_t*>(pData), static_cast<eastl_size_t>(nSize));
				return true;
			}

		private:

			EA::RawString*	mString;
		};

		EABASETYPES_FORWARD_DECLARATION(class, ProxyHTTPCallback)

		class ProxyHTTPCallback : public EA::Types::FunctorRaw
		{
		public:

			ProxyHTTPCallback(EA::Types::Factory* factory, bool* done, IEncoder* encoder):
				EA::Types::FunctorRaw(factory, static_cast<ProxyHTTPCallback*>(NULL), &ProxyHTTPCallback::Do),
				mDone(done),
				mEncoder(encoder)
			{
				SetObject(this);
			}
			
			virtual ~ProxyHTTPCallback() {}

			void Clear()
			{
				mDone = NULL;
			}

		private:

			ProxyHTTPCallback(const ProxyHTTPCallback& other);
			const ProxyHTTPCallback& operator=(const ProxyHTTPCallback& rhs);

			EA::Types::BaseTypeReturn Do(EA::Types::ArrayIn parameters)
			{
				if(mDone != NULL)
				{
					EA::Types::BaseTypeDecoder<IEncoder>::Decode(mEncoder, parameters.GetUnsafePointer());

					*mDone = true;
				}

				return NULL;
			}

			bool*						mDone;
			IEncoder*					mEncoder;
		};

		class ProxyHTTPMethod : public EA::Internet::IHTTPServerDataSource
		{
		public:

			ProxyHTTPMethod(EA::Types::Factory* factory,
							const char* name,
							EA::Types::MessageProxyIn remoteProxy,
							const char* urlPath):
				mFactory(factory),
				mRefCount(0), // due to weird refcounting in UTFInternet >_<
				mRemoteProxy(remoteProxy),
				mURLPath(factory, urlPath),
				mLocalProxy(NULL),
				mWriteString(factory->GetAllocator()),
				mReadPosition(0)
			{

				mLocalProxy = 
					CORE_NEW(mFactory->GetAllocator(), "ProxyHTTPMethod::mLocalProxy", EA::Allocator::MEM_TEMP) 
						EA::Types::MessageProxy(mFactory, name);

				mLocalProxy->AddRemoteProxy(mRemoteProxy);

				mRemoteProxy->AddRemoteProxy(mLocalProxy);
			}

			//lint -efunc(423, EA::Types::ProxyHTTPMethod::~ProxyHTTPMethod)
			virtual ~ProxyHTTPMethod()
			{
				mRemoteProxy->RemoveRemoteProxy(mLocalProxy);

				mLocalProxy->RemoveRemoteProxy(mRemoteProxy);

				mLocalProxy->Shutdown();
			}

            virtual int AddRef()
			{
				return ++mRefCount;
			}

            virtual int Release()
			{
				if(--mRefCount <= 0)
				{
					CORE_DELETE(mFactory->GetAllocator(), this);
					return 0;
				}
				else
				{
					return mRefCount;
				}
			}

			virtual int CanServeURL(EA::Internet::HTTPServer* pHTTPServer,
									EA::Internet::HTTPServerJob* pHTTPJob,
									const EA::Internet::URL& url, 
									EA::Internet::HTTPMethod httpMethod)
			{
				return EA::Internet::kHTTPStatusCodeOK;
			}

			virtual uint32_t GetCustomHeaderLines(EA::Internet::HTTPServer* pHTTPServer, 
												  EA::Internet::HTTPServerJob* pHTTPJob, 
												  const EA::Internet::URL& url, 
												  EA::Internet::StringArray& headerLineArray)
			{
				headerLineArray.push_back();
				headerLineArray[0] = "Content-type: text/json; charset=utf-8\r\n";

				headerLineArray.push_back();
				headerLineArray[1] = "Cache-Control: no-cache\r\n";

				headerLineArray.push_back(); // Some browsers recognize Pragma but not Cache-Control.
				headerLineArray[2] = "Pragma: no-cache\r\n";

				return 3; // is this the number of lines?
			}

			//lint -efunc(747, EA::Types::ProxyHTTPMethod::ReadBegin)
			//lint -efunc(904, EA::Types::ProxyHTTPMethod::ReadBegin) 
			virtual uint32_t ReadBegin(EA::Internet::HTTPServer* pHTTPServer, 
									   EA::Internet::HTTPServerJob* pHTTPJob, 
									   const EA::Internet::URL& url, 
									   EA::Internet::HTTPMethod httpMethod)
			{
				EA::Internet::String8 path;

				ParsePath(path, url);

				EA::Types::ObjectRef query = ParseQuery(mFactory, url);

				EA::Types::Object::const_iterator q = query->find("callback");
				bool callback = q == query->end() || (*q).second->AsEAString() != "false";

				q = query->find("xml");
				bool xml = q != query->end() && (*q).second->AsEAString() == "true";

				EA::Allocator::ICoreAllocator* allocator = mFactory->GetAllocator();

				EA::RawString requestBody(allocator);
				RawStringStream requestBodyStream(&requestBody);

				pHTTPJob->ReadBody(&requestBodyStream);

				EA::Types::ArrayRef requestParameters = NULL;

				if(!requestBody.empty())
				{
					EA::RawString error(allocator);

					EA::Types::BaseTypeRef data;

					if(xml)
					{
						data = EA::Types::BaseTypeXmlParser::Parse(mFactory, const_cast<char *>(requestBody.c_str()), requestBody.size(), error);
					}
					else
					{
						data = EA::Types::BaseTypeJsonParser::Parse(mFactory, const_cast<char *>(requestBody.c_str()), requestBody.size(), error);
					}

					if(data != NULL)
					{
						if(data->GetType() == EA::Types::ARRAY_TYPE)
						{
							requestParameters = data->AsArray();
						}
					}

					if(requestParameters == NULL)
					{
						// do something with the error?
						return EA::Internet::kHTTPStatusCodeBadRequest;
					}
				}
				else
				{
					requestParameters = mFactory->Array();
				}

				EA::RawString proxyPath(allocator, mRemoteProxy->GetName().c_str());
				proxyPath.append("/");
				proxyPath.append(path.c_str());
				EA::Types::FunctionRef function = mLocalProxy->GetFunction(proxyPath.c_str());

				bool done = false;

				EA::Types::ArrayRef parameters = mFactory->Array();

				ProxyHTTPCallbackRef httpCallback = NULL;

				IEncoder* encoder = NULL;

				// make both encoding pipelines, even though we'll only use one of them
				// this is so they're on the stack for easy access...
				RawStringStream xmlWriteStream(&mWriteString);
				EA::XML::XmlWriter xmlWriter;
				xmlWriter.SetOutputStream(&xmlWriteStream);
				XmlEncoder xmlEncoder(mFactory->GetAllocator(), &xmlWriter);
				IEncoderImpl<XmlEncoder> xmlIEncoder(&xmlEncoder);

				EA::RawStringWriteStream jsonWriteStream(&mWriteString);
				EA::Json::JsonWriter jsonWriter;
				jsonWriter.SetStream(&jsonWriteStream);
				JsonEncoder jsonEncoder(&jsonWriter);
				IEncoderImpl<JsonEncoder> jsonIEncoder(&jsonEncoder);

				if(xml)
				{
					xmlWriter.WriteXmlHeader();
					encoder = &xmlIEncoder;
				}
				else
				{
					encoder = &jsonIEncoder;
				}

				if(callback)
				{
					httpCallback = new (mFactory) ProxyHTTPCallback(mFactory, &done, encoder);
					parameters->push_back(httpCallback);
				}

				EA::Types::Array::const_iterator it = requestParameters->begin();
				EA::Types::Array::const_iterator end = requestParameters->end();
				for(; it != end; it++)
				{
					parameters->push_back((*it));
				}

				function->Call(parameters);

				if(callback)
				{
					EA::StdC::Stopwatch timer(EA::StdC::Stopwatch::kUnitsMilliseconds);
					timer.Start();

					while(!done && timer.GetElapsedTime() < 10000)
					{
						mLocalProxy->GetLocalServer()->ProcessQueue();
						EA::Thread::ThreadSleep(10);
					}
						
					EA_ASSERT(httpCallback != NULL);

					// make sure we don't stomp the stack if the callback gets called later...
					httpCallback->Clear();

					// Since we are a proxy, we'll call this a gateway timeout...
					if(!done)
					{
						return EA::Internet::kHTTPStatusCodeGatewayTimeOut;
					}
				}

				return EA::Internet::kHTTPStatusCodeOK;
			}

			virtual EA::IO::size_type GetReadSize(EA::Internet::HTTPServer* pHTTPServer, 
												  EA::Internet::HTTPServerJob* pHTTPJob, 
												  const EA::Internet::URL& url)
			{
				return mWriteString.size();
			}

			virtual EA::IO::size_type Read(EA::Internet::HTTPServer* pHTTPServer, 
										   EA::Internet::HTTPServerJob* pHTTPJob, 
										   const EA::Internet::URL& url, 
										   void* pData, 
										   EA::IO::size_type nSize)
			{
				EA::IO::size_type size = mWriteString.size();
				EA::IO::size_type amount = 0;
				if(size != mReadPosition)
				{
					unsigned int remaining = (unsigned int)(size - mReadPosition);
					amount = remaining < nSize ? remaining : nSize;

					memcpy(pData, mWriteString.c_str() + mReadPosition, amount);

					mReadPosition += amount;
				}
				return amount;
			}

			virtual void ReadEnd(EA::Internet::HTTPServer* pHTTPServer, 
								 EA::Internet::HTTPServerJob* pHTTPJob, 
								 const EA::Internet::URL& url)
			{
			}

		protected:

			//lint -efunc(530, EA::Types::ProxyHTTPMethod::ParsePath)
			//lint -efunc(732, EA::Types::ProxyHTTPMethod::ParsePath)
			//lint -efunc(1013, EA::Types::ProxyHTTPMethod::ParsePath)
			//lint -efunc(1015, EA::Types::ProxyHTTPMethod::ParsePath)
			//lint -efunc(904, EA::Types::ProxyHTTPMethod::ParsePath)
			static void ParsePath(EA::Internet::String8& dst, const EA::Internet::URL& url)
			{
				const EA::Internet::String8& path = url.GetComponent(EA::Internet::URL::kComponentAbsolutePath);

				EA::Internet::String8::size_type end = EA::Internet::String8::npos;

				EA::Internet::String8::size_type current = path.find_first_of('/', 1);

				dst = path.substr(current, end).c_str();
			}

			//lint -efunc(732, EA::Types::ProxyHTTPMethod::ParseQuery)
			//lint -efunc(904, EA::Types::ProxyHTTPMethod::ParseQuery)
			static EA::Types::ObjectReturn ParseQuery(EA::Types::Factory* factory, const EA::Internet::URL& url)
			{
				const EA::Internet::String8& query = url.GetComponent(EA::Internet::URL::kComponentQuery);

				EA::Internet::String8::size_type end = EA::Internet::String8::npos;

				EA::Internet::String8::size_type current = query.find_first_not_of('?');

				EA::Types::ObjectRef dst = factory->Object();

				if(current == end) return dst;

				do
				{
					EA::Internet::String8::size_type next = query.find_first_of("=", current);

					if(next == end)
					{
						break;
					}

					EA::Internet::String8 key = query.substr(current, next - current);

					current = next + 1;

					next = query.find_first_of("&", current);

					EA::Internet::String8 value = query.substr(current, next != end ? next - current : end);

					dst->insert(key.c_str(), value.c_str());

					current = next != end ? next + 1 : next;
				}
				while(current != end);

				return dst;
			}
			
			EA::Types::Factory*							mFactory;
			int											mRefCount;

			EA::Types::MessageProxyRef					mRemoteProxy;
			EA::String									mURLPath;

			EA::Types::MessageProxyRef					mLocalProxy;

			EA::RawString								mWriteString;
			EA::IO::size_type							mReadPosition;
			
		private:
		
			ProxyHTTPMethod(const ProxyHTTPMethod& other);
			const ProxyHTTPMethod& operator=(const ProxyHTTPMethod& rhs);
		};

		class ProxyHTTPPluginImpl : public EA::Types::VRefCountedICA<ProxyHTTPPlugin>
		{
		public:

			ProxyHTTPPluginImpl(EA::Types::Factory* factory,
								EA::Internet::HTTPServer* server,
								EA::Types::MessageProxyIn remoteProxy,
								const char* urlBranch):
				EA::Types::VRefCountedICA<ProxyHTTPPlugin>(factory->GetAllocator()),
				mFactory(factory),
				mServer(server),
				mRemoteProxy(remoteProxy),
				mURLBranch(factory, urlBranch)
			{
				mServer->RegisterDataSource(&ProxyHTTPPluginImpl::GetFunction, this, "ProxyHTTPPlugin_GET_POST", urlBranch, EA::Internet::kHTTPMethodFlagGet | EA::Internet::kHTTPMethodFlagPost, 0xFFFFFFFF);
			}

			virtual ~ProxyHTTPPluginImpl()
			{
				mServer->UnregisterDataSource(&ProxyHTTPPluginImpl::GetFunction, this, mURLBranch.c_str());
			}

		private:

			static EA::Internet::IHTTPServerDataSource* GetFunction(const char8_t* pURLPath, void* pContext)
			{
				static unsigned int id = 0;
				id++;

				ProxyHTTPPluginImpl* plugin = static_cast<ProxyHTTPPluginImpl*>(pContext);

				EA::RawString name(plugin->mAllocator);
				name.sprintf("ProxyHTTP_%x", id);

				return CORE_NEW(plugin->mAllocator, "ProxyHTTPMethod", EA::Allocator::MEM_TEMP) ProxyHTTPMethod(plugin->mFactory, name.c_str(), plugin->mRemoteProxy, pURLPath);
			}

			EA::Types::Factory*			mFactory;
			EA::Internet::HTTPServer*	mServer;
			EA::Types::MessageProxyRef	mRemoteProxy;
			EA::String					mURLBranch;
		};

		/* static */ ProxyHTTPPluginReturn ProxyHTTPPlugin::Create(EA::Types::Factory* factory, 
																   EA::Internet::HTTPServer* server,
																   EA::Types::MessageProxyIn remoteProxy,
																   const char* urlBranch)
		{
			return CORE_NEW(factory->GetAllocator(), "ProxyHTTPPlugin", EA::Allocator::MEM_PERM) 
				ProxyHTTPPluginImpl(factory,
									server,
									remoteProxy,
									urlBranch);
		}
	}
}
