/** @file typechecking.cpp
 *  Interface of the type checking system for generic EA types
 *	@headerfile typechecking.h "EABaseTypes/typechecking.h"
 *	@author Chris Burns (cburns@ea.com)
 *
 *	EABaseTypes
 *	(c) 2011 Electronic Arts Inc. 
 */

#include "EABaseTypes/typechecking.h"

#if !defined(EA_FINAL)

#include "EABaseTypes/datatypes.h"
#include "EABaseTypes/metatypes.h"
#include "EABaseTypes/factory.h"

#include "eaassert/eaassert.h"

#include "coreallocator/icoreallocatormacros.h"
#include "eastdc/eastring.h"

namespace EA
{
	namespace Types
	{
		static bool IsType(MetaTypes* metaTypes, const char* name, const EA::RawString& context, EA::RawString& message)
		{
			bool result = false;
			// could we use some kind of hash instead?
			// this is EXPENSIVE
			if(
				EA::StdC::Strcmp(name, "any") == 0 ||
				EA::StdC::Strcmp(name, "bool") == 0 ||
				EA::StdC::Strcmp(name, "int") == 0 ||
				EA::StdC::Strcmp(name, "float") == 0 ||
				EA::StdC::Strcmp(name, "number") == 0 ||
 				EA::StdC::Strcmp(name, "string") == 0 ||
				EA::StdC::Strcmp(name, "array") == 0 ||
				EA::StdC::Strcmp(name, "object") == 0 ||
				EA::StdC::Strcmp(name, "function") == 0 ||
				EA::StdC::Strcmp(name, "bool_array") == 0 ||
				EA::StdC::Strcmp(name, "int_array") == 0 ||
				EA::StdC::Strcmp(name, "float_array") == 0 ||
				EA::StdC::Strcmp(name, "number_array") == 0 ||
				EA::StdC::Strcmp(name, "string_array") == 0 ||
				EA::StdC::Strcmp(name, "array_array") == 0 ||
				EA::StdC::Strcmp(name, "object_array") == 0
				)
			{
				result = true;
			}
			else if(metaTypes != NULL && metaTypes->IsMetaType(name))
			{
				result = true;
			}
			
			message.append_sprintf("%s is invalid type name %s", context.c_str(), name);
			
			return result;
		}

		static bool IsType(MetaTypes* metaTypes, ConstStringIn data, const EA::RawString& context, EA::RawString& message)
		{
			return IsType(metaTypes, *data, context, message);
		}

		static bool IsType(MetaTypes* metaTypes, ConstBaseTypeIn data, const EA::RawString& context, EA::RawString& message);

		static bool IsArrayType(MetaTypes* metaTypes, ConstObjectIn data, const EA::RawString& context, EA::RawString& message)
		{
			bool result = false;
			if(data->contains("elementType"))
			{
				EA::RawString newContext(context);

				newContext.append(".elementType");

				result = IsType(metaTypes, ConstBaseTypeIn((*data)["elementType"]), newContext, message);
			}

			return result;
		}

		static bool IsObjectType(MetaTypes* metaTypes, ConstObjectIn data, const EA::RawString& context, EA::RawString& message)
		{
			bool result = false;
			if(data->contains("members"))
			{
				ConstBaseTypeRef members = (*data)["members"];

				if(members->GetType() == OBJECT_TYPE)
				{
					result = true;
					ConstObjectRef object = members->AsObject();

					Object::const_iterator it = object->begin();
					Object::const_iterator end = object->end();

					for(; it != end; ++it)
					{
						EA::RawString newContext(context);

						newContext.append(".members.");
						newContext.append((*it).first.c_str());

						if(!IsType(metaTypes, ConstBaseTypeIn((*it).second), newContext, message))
						{
							result = false;
							break;
						}
					}
				}
			}
			else
			{
				message.append_sprintf("%s should have member \"members\"", context.c_str());
			}

			return result;
		}

		static bool IsFunctionType(MetaTypes* metaTypes, ConstObjectIn data, const EA::RawString& context, EA::RawString& message)
		{
			bool result = false;
			if(data->contains("data"))
			{
				ConstBaseTypeRef fnData = (*data)["data"];
				if(fnData->GetType() == ARRAY_TYPE)
				{
					result = true;
					ConstArrayRef params = fnData->AsArray();
					Array::const_iterator it = params->begin();
					Array::const_iterator end = params->end();

					for(; it != end; ++it)
					{
						EA::RawString newContext(context);
						newContext.append(".functiondata.");
						if(!IsType(metaTypes, ConstBaseTypeIn((*(*it)->AsObject())["type"]), newContext, message))
						{
							result = false;
							break;
						}
					}
				}
				else
				{
					message.append_sprintf("%s should have data array", context.c_str());
				}
			}
			else
			{
				message.append_sprintf("%s should have data \"members\"", context.c_str());
			}
			return result;
		}

		static bool IsType(MetaTypes* metaTypes, ConstObjectIn data, const EA::RawString& context, EA::RawString& message)
		{
			bool result = false;
			
			if(data->contains("metaType") != data->contains("type"))
			{	
				if(data->contains("metaType"))
				{
					ConstBaseTypeRef metaType = (*data)["metaType"];

					if(metaType->GetType() == STRING_TYPE)
					{
						const EA::String& metaTypeStr = *(metaType->AsString());

						if(metaTypeStr == "array")
						{
							result = IsArrayType(metaTypes, data, context, message);
						}
						else if(metaTypeStr == "object")
						{
							result = IsObjectType(metaTypes, data, context, message);
						}
						else if(metaTypeStr == "function")
						{
							result = IsFunctionType(metaTypes, data, context, message);
						}
						else
						{
							message.append_sprintf("%s has invalid metaType %s", context.c_str(), metaTypeStr.c_str());
						}
					}
				}
				else if(data->contains("type"))
				{
					ConstBaseTypeRef type = (*data)["type"];
					result = IsType(metaTypes, ConstBaseTypeIn(type), context, message);
				}
			}
			else
			{
				if(data->contains("metaType") && data->contains("type"))
				{
					message.append_sprintf("%s cannot contain \"metaType\" and \"type\"", context.c_str());
				}
				else
				{
					message.append_sprintf("%s should have member \"metaType\" or \"type\"", context.c_str());
				}
				
			}
			
			return result;
		}

		static bool IsType(MetaTypes* metaTypes, ConstBaseTypeIn data, const EA::RawString& context, EA::RawString& message)
		{
			bool result = false;
			if(data != NULL)
			{
				switch(data->GetType())
				{
					case STRING_TYPE:
						result = IsType(metaTypes, ConstStringIn(data->AsString()), context, message);
						break;
					case OBJECT_TYPE:
						result = IsType(metaTypes, ConstObjectIn(data->AsObject()), context, message);
						break;
					default:
						break;
				}
			}

			return result;
		}

		bool IsType(MetaTypes* metaTypes, ConstBaseTypeIn data)
		{
			EA::Allocator::ICoreAllocator* allocator = data->GetFactory()->GetAllocator();
			EA::RawString message(allocator);

			return IsType(metaTypes, data, EA::RawString(allocator, "type"), message);
		}	

		static bool CheckTypeComplex(MetaTypes* metaTypes, BaseTypeIn data, ObjectIn type, const EA::RawString& context, EA::RawString& message);

		static bool CheckTypePrimitiveArray(MetaTypes* metaTypes, BaseTypeIn data, const char* primitive, const EA::RawString& context, EA::RawString& message)
		{
			//EABASETYPES_LOGD("EABaseTypeTypeChecking", ("CheckTypePrimitiveArray(%s)", context.c_str()));

			EA_ASSERT_FORMATTED(IsType(metaTypes, primitive, EA::RawString(data->GetFactory()->GetAllocator(), "type"), message), ("%s is not a valid type", primitive));

			Factory* factory = data->GetFactory();
			Object arrayType(factory);

			arrayType.insert("metaType").first->second = factory->String("array");
			arrayType.insert("elementType").first->second = factory->String(primitive);

			return CheckTypeComplex(metaTypes, data, ObjectIn(&arrayType), context, message);
		}

		//lint -efunc(904, EA::Types::CheckTypePrimitive)
		static bool CheckTypePrimitive(MetaTypes* metaTypes, BaseTypeIn data, const EA::String& type, const EA::RawString& context, EA::RawString& message)
		{
			//EABASETYPES_LOGD("EABaseTypeTypeChecking", ("CheckTypePrimitive(%s)", context.c_str()));
			
			if(type == "any")
			{
				return true;
			}
			else if(type == "bool")
			{
				if(data != NULL && data->GetType() == BOOLEAN_TYPE)
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be a bool", context.c_str());
				}
			}
			else if(type == "int")
			{
				if(data != NULL && (data->GetType() == NUMBER_TYPE)  && (data->AsNumber()->IsInt() == true) )
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be an int", context.c_str());
				}
			}
			else if(type == "float")
			{
				if(data != NULL && data->GetType() == NUMBER_TYPE)
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be a float", context.c_str());
				}
			}
			else if(type == "number")
			{
				if(data != NULL && data->GetType() == NUMBER_TYPE)
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be a number", context.c_str());
				}
			}
			else if(type == "string")
			{
				if(data != NULL && data->GetType() == STRING_TYPE)
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be a string", context.c_str());
				}
			}
			else if(type == "array")
			{
				if(data == NULL || data->GetType() == ARRAY_TYPE)
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be an array", context.c_str());
				}
			}
			else if(type == "object")
			{
				if(data == NULL || data->GetType() == OBJECT_TYPE)
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be an object", context.c_str());
				}
			}
			else if(type == "function")
			{
				if(data != NULL && data->GetType() == FUNCTION_TYPE)
				{
					return true;
				}
				else
				{
					message.append_sprintf("%s should be an function", context.c_str());
				}
			}
			else if(type == "any_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "any", context, message);
			}
			else if(type == "bool_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "bool", context, message);
			}
			else if(type == "int_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "int", context, message);
			}
			else if(type == "float_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "float", context, message);
			}
			else if(type == "number_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "number", context, message);
			}
			else if(type == "string_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "string", context, message);
			}
			else if(type == "array_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "array", context, message);
			}
			else if(type == "object_array")
			{
				return CheckTypePrimitiveArray(metaTypes, data, "object", context, message);
			}
			else if(metaTypes != NULL && metaTypes->IsMetaType(type.c_str()))
			{
				return metaTypes->CheckMetaType(data, type.c_str(), message);
			}
			else
			{
				EA_FAIL_FORMATTED(("Unrecognized type %s", type.c_str()));
			}

			return false;
		}

		static bool CheckType(MetaTypes* metaTypes, BaseTypeIn data, BaseTypeIn type, const EA::RawString& context, EA::RawString& message);

		static bool CheckTypeArray(MetaTypes* metaTypes, BaseTypeIn data, ObjectIn type, const EA::RawString& context, EA::RawString& message)
		{
			//EABASETYPES_LOGD("EABaseTypeTypeChecking", ("CheckTypeArray(%s)", context.c_str()));

			bool result = false;
			EA_ASSERT(type->contains("elementType"));
			EA_ASSERT(IsType(metaTypes, (*type)["elementType"]));

			if(data != NULL && data->GetType() == ARRAY_TYPE)
			{
				result = true;
				BaseTypeRef elementType = (*type)["elementType"];

				ArrayRef elements = data->AsArray();

				Array::iterator it = elements->begin();
				Array::iterator end = elements->end();
				
				for(int i = 0; it != end; ++it, ++i)
				{
					EA::RawString newContext(context);
					newContext.append("[");
					newContext.append_sprintf("%d", i);
					newContext.append("]");

					if(!CheckType(metaTypes, *it, elementType, newContext, message))
					{
						result = false;
						break;
					}
				}
			}
			
			return result;
		}

		static bool CheckTypeObject(MetaTypes* metaTypes, BaseTypeIn data, ObjectIn type, const EA::RawString& context, EA::RawString& message)
		{
			//EABASETYPES_LOGD("EABaseTypeTypeChecking", ("CheckTypeObject(%s)", context.c_str()));

			bool result = false;
			EA_ASSERT(type->contains("members"));
			EA_ASSERT((*type)["members"]->GetType() == OBJECT_TYPE);
			
			if(data != NULL && data->GetType() == OBJECT_TYPE)
			{
				ObjectRef memberTypes = (*type)["members"]->AsObject();
				ObjectRef members = data->AsObject();
				result = true;

				Object::iterator it = memberTypes->begin();
				Object::iterator end = memberTypes->end();

				Object::iterator mEnd = members->end();
				Object::iterator mIt;
				for(; it != end; ++it)
				{
					BaseTypeRef typeDef = (*it).second;
					bool bOptional = false;
					mIt = members->find((*it).first);
					BaseTypeRef memberData = NULL;
					if(mIt != mEnd)
					{
						memberData = (*mIt).second;
					}
					
					if(typeDef->GetType() == OBJECT_TYPE)
					{
						bOptional = typeDef->AsObject()->contains("optional") ? (*(typeDef->AsObject()))["optional"]->AsBool() : false;
					}

					EA::RawString newContext(context);
					newContext.append(".");
					newContext.append((*it).first.c_str());

					if((!bOptional && memberData == NULL) || 
						!CheckType(metaTypes, memberData, typeDef, newContext, message))
					{
						result = false;
						break;
					}
					
				}

				//now check for keys that exist in the data but are not found in the type definition
				if(result)
				{
					mIt = members->begin();
					mEnd = members->end();
					end = memberTypes->end();
					for(; mIt != mEnd; ++mIt)
					{
						it = memberTypes->find((*mIt).first);
						if(it == end)
						{
							message.append_sprintf("%s contains property %s that is not in the type definition", context.c_str(), (*mIt).first.c_str());
							result = false;
							break;
						}
					}
				}
			}

			return result;
		}

		static bool CheckTypeFunction(MetaTypes* metaTypes, BaseTypeIn data, ObjectIn type, const EA::RawString& context/*, EA::RawString& message*/)
		{
			//EABASETYPES_LOGD("EABaseTypeTypeChecking", ("CheckTypeFunction(%s)", context.c_str()));
			bool result = false;
			EA_ASSERT(type->contains("data"));
			EA_ASSERT((*type)["data"]->GetType() == ARRAY_TYPE);
			if(data != NULL && data->GetType() == FUNCTION_TYPE)
			{
				ArrayRef typeArr = (*type)["data"]->AsArray();
				Array::iterator typeIt = typeArr->begin();
				Array::iterator typeEnd = typeArr->end();
				Factory* factory = data->GetFactory();
				Types::ArrayRef pDataDef = factory->Array();
				for(; typeIt != typeEnd; ++typeIt)
				{
					EA_ASSERT((*typeIt)->GetType() == Types::OBJECT_TYPE);
					ObjectRef pParamType = (*typeIt)->AsObject();
					EA_ASSERT(pParamType->contains("type"));
					switch((*pParamType)["type"]->GetType())
					{
						case Types::STRING_TYPE:
						case Types::OBJECT_TYPE:
							pDataDef->push_back((*pParamType)["type"]);
						break;
						default: 
							EA_FAIL_FORMATTED(("Unexpected Type %d", (*pParamType)["type"]->GetType())); 
						break;
					}
				}
				
				data->AsFunction()->SetDataDef(pDataDef);
				result = true;
			}
			return result;
		}

		static bool CheckTypeComplex(MetaTypes* metaTypes, BaseTypeIn data, ObjectIn type, const EA::RawString& context, EA::RawString& message)
		{
			//EABASETYPES_LOGD("EABaseTypeTypeChecking", ("CheckTypeComplex(%s)", context.c_str()));
			bool result = false;
			EA_ASSERT_MESSAGE((type->contains("metaType") != type->contains("type")), ("Type cannot contain metaType and type"));
			bool bOptional = type->contains("optional") ? (*type)["optional"]->AsBool() : false;
			if(!bOptional || data != NULL)
			{
				if(type->contains("metaType"))
				{
					EA_ASSERT((*type)["metaType"]->GetType() == STRING_TYPE);

					const EA::String& metaTypeStr = *((*type)["metaType"]->AsString());
					if(metaTypeStr == "array")
					{
						result = CheckTypeArray(metaTypes, data, type, context, message);
					}
					else if(metaTypeStr == "object")
					{
						result = CheckTypeObject(metaTypes, data, type, context, message);
					}
					else if(metaTypeStr == "function")
					{
						result = CheckTypeFunction(metaTypes, data, type, context/*, message*/);
					}
					else
					{
						EA_FAIL_FORMATTED(("Invalid metaType %s", metaTypeStr.c_str()));
					}

				}
				else if(type->contains("type"))
				{
					BaseTypeRef typeDef = (*type)["type"];
					result = CheckType(metaTypes, data, typeDef, context, message);
				}
			}
			else 
			{
				result = true;
			}

			return result;
		}

		static bool CheckType(MetaTypes* metaTypes, BaseTypeIn data, BaseTypeIn type, const EA::RawString& context, EA::RawString& message)
		{
			//EABASETYPES_LOGD("EABaseTypeTypeChecking", ("CheckType(%s)", context.c_str()));
			bool result = false;
			switch(type->GetType())
			{
				case STRING_TYPE:
					result = CheckTypePrimitive(metaTypes, data, *(type->AsString()), context, message);
					break;
				case OBJECT_TYPE:
					result = CheckTypeComplex(metaTypes, data, type->AsObject(), context, message);
					break;
				default:
					EA_FAIL_MESSAGE("type doesn't appear to be valid - we should have asserted much earlier");
			}

			return result;
		}

		bool CheckType(MetaTypes* metaTypes, BaseTypeIn data, BaseTypeIn type, EA::RawString& message)
		{
			bool result = true;
			if(type != NULL)
			{
				EA_ASSERT_MESSAGE(IsType(metaTypes, type), "type must be a proper type description");
				result = CheckType(metaTypes, data, type, EA::RawString(data->GetFactory()->GetAllocator(), "value"), message);
			}
			// no type description so just assume this is valid
			return result;
		}
	}
}

#else // !defined(EA_FINAL)

namespace EA
{
	namespace Types
	{
		bool IsType(MetaTypes* metaTypes, const BaseTypeIn data) { return true; }

		bool CheckType(MetaTypes* metaTypes, BaseTypeIn data, BaseTypeIn type, const EA::RawString& message) { return true; }
	}
}

#endif
