local table = require "table"
local oo = require "loop.simple"
local string = require "string"
local jsonEncoder = require "LuaOrm.Serializers.Json"
local Utils = require "LuaOrm.Utils"
require "LuaOrm.Associations.AssociationArray"

local type = type
local ipairs = ipairs
local pairs = pairs
local tostring = tostring
local rawget = rawget
local tostring = tostring
local print = print
local getmetatable = getmetatable
local setmetatable = setmetatable
local next = next

local rpairs = LuaOrm.Associations.AssociationArray.Iterator

module("LuaOrm.Entity") 

--
-- Members of this class are:
-- m_model: an instance of LuaOrm.Model
-- m_newRecord: flag that indicates if this entity is present on the database or not


------------------------------------------------------------
-- Searches for a defined dynamic attribute and return its value
------------------------------------------------------------
local function FindCalculatedAttribute(self, attributeName)
	if (not self.m_model.Attributes) then return end
	local attribute = self.m_model.Attributes[attributeName]
	if type(attribute) == "function" then
		return attribute(self)
	end
	return attribute
end


------------------------------------------------------------
-- Helper function to search for a related model
------------------------------------------------------------
local function helper_FindRelatedModel(self, name, relatedName, asocObject)
	if not relatedName then
		return nil
	end
	local match = string.match(name, "^" .. relatedName .. "(.*)")
	if match == "s" then
		--asked for a plural form, has to be a many relationship => return an array..
		if asocObject.isMultiple then
			return asocObject:LoadAll(self)
		end
	else
		if match == "" then
			--must be a one relationship
			if not asocObject.isMultiple then
				return asocObject:Load(self)
			end
		end
	end
	return nil
end

------------------------------------------------------------------------
-- Searches for a defined relationship by name (looks up its alias too)
------------------------------------------------------------------------
local function FindRelatedModel(self, name)
	for index, asocObject in ipairs(self.m_model.Associations) do
		if (asocObject.m_belongsToEntity and rawget(self, "m_lazy")) or (not asocObject.m_belongsToEntity) then 
			local model = helper_FindRelatedModel(self, name, asocObject:GetRelatedName(), asocObject)
			if not model then
				model = helper_FindRelatedModel(self, name, asocObject:GetRelationAliasedName(), asocObject)
			end
			if model then
				return model
			end
		end
	end
	return nil
end

------------------------------------------------------------
-- Validates whether the requests is for an associated object
-- in the form of entity.relatedEntity or entity.relatedEntitys
------------------------------------------------------------
local function entityIndexOperator(self, name)
	local meta = getmetatable(self)
	if (meta.__classIndex) then
		local relatedModel = FindRelatedModel(self, name)
		if (relatedModel) then
			--no quiero que vuelva mas a cargar de la base
			self[name] = relatedModel
			return relatedModel
		end
		local attribute = FindCalculatedAttribute(self, name) 
		if (attribute) then return attribute end
	end
	if (type(meta.__classIndex) == "table") then
		return meta.__classIndex[name]
	else
		if (type(meta.__classIndex) == "function") then
			return meta.__classIndex(self, name)
		else
			return nil
		end
	end
end

------------------------------------------------------------
--encodes the current entity to json, by default it encodes all the relatioships all the way down
------------------------------------------------------------
local function encode2Json(self)
	local encoder = oo.new(jsonEncoder)
	return encoder:Serialize(self)  
end

local ClassMetaTable = {
	__classIndex = _M,
	__index = entityIndexOperator,
	__toJson = encode2Json
}
function __init(class, object)
	local object = object or {}
	return setmetatable(object or {}, ClassMetaTable)
end

oo.class(_M)



function YieldMultipleRelationship(self,  operation, relationship)
	local relatedEntities = self[relationship:GetRelatedName().."s"]
	if (relatedEntities and relatedEntities:Count() > 0) then
		for index = 1,  relatedEntities:Count() do
			local v = relatedEntities:GetItem(index)
			if (v) then 
				if (v.m_deleted == true) then
					local res, err = v:Delete()
					if not res then return false, err end
				else
					local res, err = v[operation](v)
					if not res then return false, err end
				end
			end
		end
	end
	return true
end

------------------------------------------------------------
-- Save the entity with it belonging relationships (as defined in the model)
-- everthing is done inside a transaction 
------------------------------------------------------------
function Save(self)
	local failed = false
	--begintrans
	local res, err = self.m_model:Save(self)
	if res then
		--save associations belonging to entity
		for asocName, asocObject in ipairs(self.m_model.Associations) do
			if (asocObject.m_belongsToEntity) then
				if (asocObject.isMultiple) then
					res, err = self:YieldMultipleRelationship("Save", asocObject)
					if not res then
						failed = true
						break
					end
				else
					if (self[asocObject:GetRelatedName()]) then
						local delete = self[asocObject:GetRelatedName()].m_deleted
						if (delete) then
							res, err = self[asocObject:GetRelatedName()]:Delete()
							if not res then
								failed = true
								break
							end
						else
							res, err = self[asocObject:GetRelatedName()]:Save()
							if not res then
								failed = true
								break
							end
						end
					end
				end
			end
		end
	else
		failed = true
	end
	if (not failed) then
		--commit trans
		if (self.m_newRecord) then
			self.m_newRecord = nil
		end
		return true
	end
	--rollback trans
	return false, err
end

--[=[----------------------------------------------------------
Delete the entity with it belonging relationships (as defined in the model)
cascade backward delete. Everthing is done inside a transaction.
Propagates the error code in case it's needed.
------------------------------------------------------------]=]
function Delete(self)
	local failed = false
	local res, err = nil, nil
	--tengo que hacer un delete en cascada, asociaciones y despues yo mismo...
	--begin trans
	--delete associations belonging to entity
	for asocName, asocObject in ipairs(self.m_model.Associations) do
		if (asocObject.m_belongsToEntity) then
			if (asocObject.isMultiple) then
				res, err = self:YieldMultipleRelationship("Delete", asocObject)
				if not res then
					failed = true
					break
				end
			else
				if (self[asocObject:GetRelatedName()]) then
					res, err = self[asocObject:GetRelatedName()]:Delete()
					if not res then
						failed = true
						break
					end
				end
			end
		end
	end
	if (not failed) then
		res, err = self.m_model:Delete(self)
		failed = not res
	end
	if (not failed) then
		--commit trans
		return true
	end
	--rollback trans
	return false, err
end

function IsEqual(self, object)
	for k, v in pairs(self.m_model.primaryKey) do
		if (self[v] ~= object[v]) then
			return false
		end
	end
	return true
end

------------------------------------------------------------
-- Get entity id according to model defined primary key
------------------------------------------------------------
function GetId(self)
	local pk = {}
	for k, v in pairs(self.m_model.primaryKey) do
		pk[v] = self[v]
	end
	if (next(pk) == nil) then pk = nil end
	return pk
end

--
-- Appends an update operation for a given entity
-- @param updateTable the table where we're collecting the operations
-- @param entity the entity we're updating
-- @param operation ("C", "D")
-- @return nothing
function AppendUpdateOperation(updateTable, entity, operation)
	if (type(updateTable) ~= "table") then
		return
	end
	
	local entry = {
		Entity = entity.m_model.name,
		Id = entity:GetId(),
		Fields = {}
	}
	local rowEscaped = Utils.EscapeValues(entity.m_model, entity)
	for k, v in ipairs(entity.m_model.columnInfo) do
		local field = {
			Name = v.name,
			ActualValue = rowEscaped[v.name],
			PreviousValue = rowEscaped[v.name]
		}
		table.insert(entry.Fields, field)
	end
	entry.Operation = operation
	table.insert(updateTable, entry)
end


local function UpdateMultipleRelationship(old, new, relationshipName, updateTable)
	local oldArray = old[relationshipName]
	local newArray = new[relationshipName]
	for k, v in rpairs(oldArray) do
		v.m_deleted = true
	end
	for k, v in rpairs(newArray) do
		local id = v:GetId()
		local oldObject = oldArray[id]
		if (oldObject) then
			oldObject.m_deleted = false
			oldObject:Update(v, updateTable)
		else
			v.m_newRecord = true
			oldArray:Add(v)
			AppendUpdateOperation(updateTable, v, "C")
		end
	end
	for k, v in rpairs(oldArray) do
		if (v.m_deleted) then
			AppendUpdateOperation(updateTable, v, "D")
		end
	end
end


local function UpdateSingleRelationship(old, new, relationshipName, updateTable)
	if (new[relationshipName] and old[relationshipName]) then
		old[relationshipName]:Update(new[relationshipName], updateTable)
	else
		if (new[relationshipName]) then
			old[relationshipName] = new[relationshipName]
			old[relationshipName].m_newRecord = true
			AppendUpdateOperation(updateTable, new[relationshipName], "C")
		else
			if (old[relationshipName]) then
				old[relationshipName].m_deleted = true
				AppendUpdateOperation(updateTable, old[relationshipName], "D")
			end
		end
	end
end

------------------------------------------------------------
-- Updates the content of an entity using the values and relationships of a new one
-- returns a modification or delta table
------------------------------------------------------------
function Update(self, newEntity, updateTable)
	local modification = {}
	if (updateTable) then
		modification.Entity = self.m_model.name
		modification.Id = self:GetId()
		modification.Fields = {}
		modification.Operation = "U"
	end
	--Internal object data
	for k, v in ipairs(self.m_model.columnInfo) do
		--no actualizo los campos clave
		if (self:GetId()[v.name] == nil) then
			if (updateTable and tostring(self[v.name]) ~= tostring(newEntity[v.name])) then
				local field = {}
				field.Name = v.name
				if (newEntity[v.name]) then
					field.ActualValue = Utils.EscapeValue(self.m_model.dataSource, v, newEntity[v.name]) 
				end
				if (self[v.name]) then
					field.PreviousValue = Utils.EscapeValue(self.m_model.dataSource, v, self[v.name]) 
				end
				table.insert(modification.Fields, field)
			end
			self[v.name] = newEntity[v.name]
		end
	end
	if (updateTable) then table.insert(updateTable, modification) end
	--Relationships
	for asocName, asocObject in ipairs(self.m_model.Associations) do
		if (asocObject.m_belongsToEntity) then
			local relationshipName = asocObject:GetRelatedName()
			if (asocObject.isMultiple) then
				UpdateMultipleRelationship(self, newEntity, relationshipName.."s", updateTable) 
			else
				UpdateSingleRelationship(self, newEntity, relationshipName, updateTable) 
				if (type(asocObject.SetRelationshipFields) == "function" and self[relationshipName]) then
					asocObject:SetRelationshipFields(self, self[relationshipName])
				end
			end
		end
	end
end

------------------------------------------------------------
--Redefine the indexof operator in order to dinamically load
--related class models
------------------------------------------------------------
function HasAssociations(self)
	--associations belonging to entity are loaded at construction time
	if (self.m_model.Associations) then
		for asocName, asocObject in ipairs(self.m_model.Associations) do
			if (asocObject.m_belongsToEntity) then
				if (asocObject.isMultiple) then
					self[asocObject:GetRelatedName().."s"] = asocObject:LoadAll(self)
				else
					self[asocObject:GetRelatedName()] = asocObject:Load(self)
				end
			end
		end
	end
end


--[=[----------------------------------------------------------
Delete the entity inside a transaction 
(should only be called for top level entities on a transaction)
------------------------------------------------------------]=]
function TransactionalDelete(self)
	self.m_model.dataSource:BeginTransaction()
	local succeeded, err = self:Delete()
	if succeeded then
		self.m_model.dataSource:CommitTransaction()
	else
		self.m_model.dataSource:RollbackTransaction()
	end
	return succeeded, err
end


------------------------------------------------------------
-- Save the entity inside a transaction
-- (should only be called for top level entities on a transaction)
------------------------------------------------------------
function TransactionalSave(self)
	self.m_model.dataSource:BeginTransaction()
	local succeeded, err = self:Save()
	if succeeded then
		self.m_model.dataSource:CommitTransaction()
	else
		self.m_model.dataSource:RollbackTransaction()
	end
	return succeeded, err
end


-----------------------------------------------------------
--Save and delete a list of entities atomically 
-----------------------------------------------------------
function TransactionalModifyEntityList(saveEntityList, deleteEntityList)
	local dataSources = {}
	for entity in pairs(saveEntityList) do
		if not dataSources[entity.m_model.dataSource] then
			dataSources[entity.m_model.dataSource] = entity.m_model.dataSource
			entity.m_model.dataSource:BeginTransaction()
		end
		
		local succeeded, err = entity:Save()
		if not succeeded then
			for dataSource in pairs(dataSources) do
				dataSource:RollbackTransaction()
			end
			return succeeded, err
		end       
	end
	
	for entity in pairs(deleteEntityList) do
		if not dataSources[entity.m_model.dataSource] then
			dataSources[entity.m_model.dataSource] = entity.m_model.dataSource
			entity.m_model.dataSource:BeginTransaction()
		end
		
		local succeeded, err = entity:Delete()
		if not succeeded then
			for dataSource in pairs(dataSources) do
				dataSource:RollbackTransaction()
			end
			return succeeded, err
		end        
	end
	
	for dataSource in pairs(dataSources) do
		dataSource:CommitTransaction()
	end
	return true
end

