local oo = require "loop.base"
local table = require "table"
local strlen = string.len
local pairs = pairs
local strsub = string.sub
local type = type
local tostring = tostring
local getmetatable = getmetatable
local ipairs = ipairs
local strlower = string.lower

module("LuaOrm.Serializers.Base", oo.class)

------------------------------------------------------------
--Remove node from visited in ONE serializacion procedure
------------------------------------------------------------
local function RemoveVisitedNode(visitedNodes, object)
	if (visitedNodes[object.m_model.name] == nil) then 
		return
	end
	local list = visitedNodes[object.m_model.name]
	for index, entity in ipairs(list) do
		if (entity:IsEqual(object) == true) then
				list[index] = nil
			return true
		end
	end
end

------------------------------------------------------------
--Sets node as visited in ONE serializacion procedure
------------------------------------------------------------
local function AddVisitedNode(visitedNodes, object)
	if (visitedNodes[object.m_model.name] == nil) then 
		visitedNodes[object.m_model.name] = {} 
	end
	local list = visitedNodes[object.m_model.name]
	list[#list + 1] = object
end

------------------------------------------------------------
--Already beign in this node?
------------------------------------------------------------
local function IsAlreadyVisited(visitedNodes, object)
	if (object == nil or object.m_model == nil) then
		return true
	end
	if (visitedNodes == nil or visitedNodes[object.m_model.name] == nil) then 
		return false
	end
	local list = visitedNodes[object.m_model.name]
	for index, entity in ipairs(list) do
		if (entity:IsEqual(object) == true) then
			return true
		end
	end
	return false
end

function SerializeMultipleRelationship(self, object, relationship, all, depth, visitedNodes, serializationPaths, includeAttributes)
	local serializedRows = self:newArray()
	local relationName = relationship:GetRelatedName()
	local relationshipNamePlural = relationName.."s"
	local relatedEntities = object[relationshipNamePlural]
	if (relatedEntities and relatedEntities:Count() > 0) then
		for index = 1,  relatedEntities:Count() do
			local v = relatedEntities:GetItem(index)
			if (v) then 
				local serializedRow = self:Serialize(v, all, depth, visitedNodes, serializationPaths, includeAttributes)
				if (serializedRow) then
					serializedRows = self:AppendNode(serializedRows, serializedRow)
				end
			end
		end
	end
	return serializedRows, relationshipNamePlural
end

function SerializeSingleRelationship(self, object, relationship, all, depth, visitedNodes, serializationPaths, includeAttributes)
	local relationshipName = relationship:GetRelatedName()
	return self:Serialize(object[relationshipName], all, depth, visitedNodes, serializationPaths, includeAttributes), relationshipName
end

function BelongsToPath(relationship, paths)
	if (paths == nil or paths == "") then
		return false
	end
	if (type(paths) == "string") then
		paths = {paths}
	end
	local relationName = relationship:GetRelatedName()
	if (relationship.isMultiple) then
		relationName = relationName .. "s"
	end
	for _, path in ipairs(paths) do
		local matched = path:match("^" .. relationName .. ".") or path:match("^" .. relationName .. "$")
		if (matched) then
		    local nextRelation = strsub(path, strlen(relationName) + 2)
			return true, {nextRelation}
		end
	end
	return false
end

------------------------------------------------------------
--Serializes the LuarOrm.Entity object
--object = entity to serialize, must be of LUAORM.ENTITY package
--all = boolean, should all the relationship hierarchy be serialized? (if not only belonging relationships apply)
--depth = maximum relationship depth to serialize
--visitedNodes = nodes already serialized (or alternatively nodes to be left out)
--serializationPaths = array of relation1.relation2.relation3 -> which non belonging relationships you want to be serialized anyway
------------------------------------------------------------
function Serialize(self, object, all, depth, visitedNodes, serializationPaths, includeAttributes)
	local visitedNodes = visitedNodes or {}
	if (IsAlreadyVisited(visitedNodes, object) == true) then
		return nil
	end
	local serialized = self:newHash()
	if (depth) then depth = depth - 1 end
	--serialize the data fields
	for i, m in ipairs(object.m_model.columnInfo) do
		local value = object[m.name]
		if (m.type == "datetime") then
			--esto esta aca porque el ORM me retorna la fecha actual cuando un campo date tiene valor NULL.
			if (value and value._time) then
				value = tostring(value)
			else
				value = nil
			end
		end
		serialized = self:AppendNode(serialized, self:Encode(value), m.name)
	end
	--serialize dynamic attributes
	if (object.m_model.Attributes) then
		for name, attribute in pairs(object.m_model.Attributes) do
			--[[
				Si la variable 'includeAttributes' viene con los siguientes valores encodea o no los atributos calculados.
					-si viene con el valor en false, no serializa ningun atributo calculado.
					-si viene en nil (por defecto), serializa todos los atributos calculados.
					-si la tabla a serializar no se encuentra en la lista de tablas a incluir, serializa el campo.
					-si el nombre de la tabla se encuentra y tambien el nombre del campo, el mismo es serializado, por ende
					 si el nombre de la tabla se encuentra pero el nombre del campo no se encuentra en la tabla 'includeAttributes'
					 el atributo calculado no es serializado.
			--]]
			local tableName = strlower(object.m_model.tableName)
			local attributeName = strlower(name)
			if includeAttributes ~= false then
				if not includeAttributes or not includeAttributes[tableName] or 
							(includeAttributes[tableName] and includeAttributes[tableName][attributeName]) then
					local value = nil
					if (type(attribute) == "function") then
						value = attribute(object) 
					else
						value = attribute
					end
					serialized = self:AppendNode(serialized, self:Encode(value), name)
				end
			end
		end
	end
	if (depth == nil or depth > 0) then
		AddVisitedNode(visitedNodes, object)
		--serialize the associations 
		for asocName, asocObject in ipairs(object.m_model.Associations) do
			local belongsToPath, newPath = BelongsToPath(asocObject, serializationPaths)
			if (asocObject.m_belongsToEntity or all or belongsToPath) then
				if (asocObject.isMultiple) then
					serialized = self:AppendNode(serialized, self:SerializeMultipleRelationship(object, asocObject, all, depth, visitedNodes, newPath, includeAttributes))
				else
					local serializedRow, rowName = self:SerializeSingleRelationship(object, asocObject, all, depth, visitedNodes, newPath, includeAttributes)
					if (serializedRow ~= nil) then
						serialized = self:AppendNode(serialized, serializedRow, rowName)
					end
				end
			end
		end
	   RemoveVisitedNode(visitedNodes, object)
	end
	return serialized
end

------------------------------------------------------------
--Decodes the object, class must be of the LUAORM.MODEL package
------------------------------------------------------------
function Deserialize(self, object, class, attributes)
    local decodedTable = self:Decode(object)
	if (type(attributes) == "table") then
		for k, v in pairs(attributes) do
			decodedTable[k] = v
		end
	end
	--siempre trato las desserializaciones como objetos nuevos
	return class:New(decodedTable, true)
end

------------------------------------------------------------
--Iteratively serializes a table of LUAORM.ENTITY objects
--same parameters as Serialize apply
------------------------------------------------------------
function SerializeList(self, objectList, all, depth, serializationPaths, includeAttributes)
	local serialized = self:newArray()
	local serializedEntity
	local pathList = serializationPaths
	if (type(objectList) == "table") then
		for k, entity in pairs(objectList) do
			serializationPaths = pathList
			serializedEntity = self:Serialize(entity, all, depth, nil, serializationPaths, includeAttributes)
			if (serializedEntity) then
				serialized = self:AppendNode(serialized, serializedEntity)
			end
		end
	end
	return serialized
end
