local table = require "table"
local oo    = require "loop.base"
local Entity = require "LuaOrm.Entity"
local Utils = require "LuaOrm.Utils"
local PagedRequest = require "LuaOrm.PagedRequest"
local Date = require"LuaOrm.Date"
local os = require "os"

local type = type
local ipairs = ipairs
local pairs = pairs
local print = print
local rawset = rawset
local getmetatable, setmetatable = getmetatable, setmetatable
local now = os.time
local error = error
local tostring = tostring

module("LuaOrm.Model", oo.class)

--
-- Members of this class are:
-- name: the name of the model
-- primaryKey: the primary key of the model
-- columnInfo: the column info (metadata obtained from the RDBMS)
-- dataSource: an instance of LuaOrm.DBMS.DataSource which this model is bound to
-- updatedField (no se usa, pero si se quiere cambiar el nombre del campo de la tabla de "updated_at" a otra cosa, setearlo en algún lado)
-- Associations: a table with associated Models

function __init(class, newEntity)
	local self = newEntity or {}
	if (newEntity.dataSource == nil) then
		error("Cannot initialize without datasource")
		return nil
	end
	if (type(newEntity["tableName"]) ~= "string") then
		--we cannot initialize without a table name
		error("Cannot initialize without table name")
		return nil
	else
		if (type(newEntity["name"]) ~= "string") then
			self.name = newEntity.tableName
		end
		--initialize table meta data...
		if (type(newEntity["primaryKey"]) ~= "table") then
			self.primaryKey = newEntity.dataSource:GetTablePrimaryKey(newEntity.tableName)
			if (self.primaryKey == nil) then self.primaryKey = { [0] = "id" } end
		end
		if (type(newEntity["columnInfo"]) ~= "table") then
			self.columnInfo = newEntity.dataSource:GetTableMetadata(newEntity.tableName)
			if (newEntity.updatedField == nil and self.columnInfo["updated_at"]) then
				newEntity.updatedField = "updated_at"
			end
			if (newEntity.createdField == nil and self.columnInfo["created_at"]) then
				newEntity.createdField = "created_at"
			end
		end
	end
	local newmodel = oo.rawnew(class, self)
	if (newmodel) then
		newmodel.dataSource:AddModel(newmodel)
	end
	return newmodel
end

--
--
function GetRelationship(self, name)
	return self.Associations[name]
end

------------------------------------------------------------
--Validates that the defined associations are properly related
------------------------------------------------------------
function ValidateAssociations(self, modelList)
	if (type(self.Associations) == "table") then
		for k, v in ipairs(self.Associations) do
			local relatedModel = modelList[v.m_associatedModelName]
			if not relatedModel then
				error("Relationship defined with unexistent model '" .. v.m_associatedModelName .."'")
			end
			if (v:Validate(self, relatedModel)) then
				self.Associations[v.m_associatedModelName] = v
			end
		end
	end
end

--[=[
Returns a table with this model's constraints for a given property.
i.e:
local constraints = models.User:Constraints("length")
constraints.Name => 50 (equivale a models.User.columnInfo.Name.length)
]=]
function Constraints(self, constraint_type)
	local constraints = {}
	setmetatable(constraints, { __index = function(tab, key)
							local constraint = self.columnInfo[key][constraint_type]
							rawset(tab, key, constraint)
							return constraint
						end })
	return constraints
end

------------------------------------------------------------
--Creates a new entity for this model
------------------------------------------------------------
function CreateEntity(self, baseObject)
	baseObject.m_model = self
	return Entity(baseObject)
end

--
-- Creates a new entity for this model based on a row obtained from a DataSource
-- @param row the row returned by a datasource
-- @param lazy flag that indicates if the entity's relationships will be loaded upfront
-- @return a new Entity instance
function CreateEntityFromRow(self, row, lazy)
	local newObject = self:CreateEntity(row)
	if not lazy then
		newObject:HasAssociations()
	end
	newObject.m_lazy = not not lazy -- gives true or false, even when lazy is nil
	
	--Deprecated: all NULL values MUST be nil attributes on lua ORM entity:
	--fill those projected NULL fields that didn't get into the table object
	--Utils.FillValues(newObject, self.columnInfo, self.dataSource.m_driver)
	return newObject
end

------------------------------------------------------------
--In case the model has a single-field primary key creates
--a table with id supporting various operations using non-table format
------------------------------------------------------------
local function GetPrimaryKeyTable(self, id)
	local objectId = id
	if type(id) ~= "table" then
		if (#self.primaryKey > 1) then
			error("find error: id must be a table")
		end
		objectId = { [self.primaryKey[0]] = objectId}
	end
	return objectId
end

------------------------------------------------------------
--Creates a query based on the primary key of the model
------------------------------------------------------------
local function BuildPrimaryKeyCondition(keys, values)
	local condition = ""
	local args = {}
	for index, v in pairs(keys) do 
		if #condition > 0 then
			condition = condition .. " and "
		end
		condition = condition .. keys[index] .. " = ?" 
		args[#args + 1] = tostring(values[keys[index]])
	end
	return condition, args
end

------------------------------------------------------------
--Search and retrieve an entity from this model using its primary key
--in case the primary key is single-field a non-table parameter is supported
-- model:Find("userid")
-- model:Find({userid = "john", country = "ukraine"})
------------------------------------------------------------
function Find(self, id, lazy)
	local tableid = GetPrimaryKeyTable(self, id)
	local condition, args = BuildPrimaryKeyCondition(self.primaryKey, tableid)
	local query = self.dataSource:BuildQuery(self, condition, args)
	local row = self.dataSource:FetchRow(self, query)
	if (row) then
		return self:CreateEntityFromRow(row, lazy)
	else
		return nil
	end
end

-----------------------------------------------------------------------------------
--Initializes internal relationship structures from table objects in case available
-----------------------------------------------------------------------------------
local function initRelationshipsFromRawTable(entity, table)
	for asocName, asocObject in ipairs(entity.m_model.Associations) do
		local relatedName = asocObject:GetRelatedName()
		if (asocObject.isMultiple) then
			relatedName = relatedName.."s"
		end
		local relatedData = table[relatedName]
		if (relatedData) then 
			if (asocObject.m_belongsToEntity) then
				if (asocObject.isMultiple) then
					for k, v in pairs(relatedData) do
						--detail lines are always new cause it always gets deleted before insertion
						if (type(asocObject.SetRelationshipFields) == "function") then
							asocObject:SetRelationshipFields(entity, v)
						end
						local newObj = asocObject.m_associatedModel:New(v, true)
						entity[relatedName]:Add(newObj)
					end
				else
					--le seteo los valores de FK a la tabla raw para que en el new del objeto se propaguen las foreign keys
					if (type(asocObject.SetRelationshipFields) == "function") then
						asocObject:SetRelationshipFields(entity, relatedData)
					end
					entity[relatedName] = asocObject.m_associatedModel:New(relatedData, entity.m_newRecord)
				end
			end
		end
	end
end

------------------------------------------------------------
--Creates a new entity for this model (the start point for creating new "rows")
------------------------------------------------------------
function New(self, rawTable, newRecord)
	local newObject = {}
	local rawdata = rawTable or {}
	if (newRecord ~= nil) then
		newObject.m_newRecord = (newRecord == true)
	else
		newObject.m_newRecord = true
	end
	newObject = self:CreateEntityFromRow(newObject, false)
	--initialize column data 
	Utils.InitValues(newObject, self.columnInfo, rawdata)
	--initialize relationship data in case available
	if (rawTable) then
		initRelationshipsFromRawTable(newObject, rawdata)
	end
	return newObject
end

------------------------------------------------------------
--Finds the first item matching with the condition provided (see FindAll for condition sintax)
------------------------------------------------------------
function FindFirst(self, condition, args, lazy)
	local query = self.dataSource:BuildQuery(self, condition, args)
	local row = self.dataSource:FetchRow(self, query)
	if not row then
		return nil
	end
	return self:CreateEntityFromRow(row, lazy)
end

------------------------------------------------------------
--
------------------------------------------------------------

--theoretically we should receive in args the number of records per page
--and the number of intended pages per view (in order to cache rpp * ppv)
function FindAllPaged(self, condition, args, rpp)
	local pagedData = PagedRequest()
	local order = nil
	if args and args.order then order = args.order end
	local condition, tables, fields, hasRelationship = self.dataSource:BuildCondition(self, condition, args)
	pagedData:Initialize(self, condition, tables, fields, order, rpp, hasRelationship)
	return pagedData
end

------------------------------------------------------------
--Finds all the items matching the condition. In case the model has relationships defined they may be used
--in the condition by its name. ej:
--model:FindAll("Country = ? and Production.Corn > ?",{"Spain", 12355})
------------------------------------------------------------
function FindAll(self, condition, args, lazy)
	local query = self.dataSource:BuildQuery(self, condition, args)
	local rows = self.dataSource:FetchRows(self, query, (args and args.count) or (condition and condition.count))
	if not rows then
		return {}
	end
	for k, row in pairs(rows) do
		rows[k] = self:CreateEntityFromRow(row, lazy) 
	end
	collectgarbage()
	return rows
end

------------------------------------------------------------
-- Updates the current entity/model (changing timestamp in case defined)
------------------------------------------------------------
local function Update(self, entity)
	local rowEscaped = Utils.EscapeValues(self, entity)
	local updates = {}
	if self.updatedField and self.columnInfo[self.updatedField] then
		local currentTime = os.date("!%Y-%m-%d %H:%M:%S")	-- hora actual en UTC
		entity[self.updatedField] = currentTime
		rowEscaped[self.updatedField] = Utils.EscapeValue(self.dataSource, self.columnInfo[self.updatedField], currentTime)
	end
	local tableid = GetPrimaryKeyTable(self, entity:GetId())
	for fieldName, fieldValue in pairs(rowEscaped) do
		-- if not the primary key, updates the field
		if not tableid[fieldName] then
			table.insert(updates, self.dataSource.Format("%Q = %s", fieldName, fieldValue))
		end
	end
	if #updates == 0 then
		return true
	end
	local condition, args = BuildPrimaryKeyCondition(self.primaryKey, tableid)
	local wherecond , tables, fields = self.dataSource:BuildCondition(self, condition, args)
	return self.dataSource:Update(self.tableName, updates, wherecond)
end

------------------------------------------------------------
-- Delete the entity
------------------------------------------------------------
function Delete(self, entity)
	if entity:GetId() then
		local condition, args = BuildPrimaryKeyCondition(self.primaryKey, GetPrimaryKeyTable(self, entity:GetId()))
		local wherecond , tables, fields = self.dataSource:BuildCondition(self, condition, args)
		return self.dataSource:Delete(self.tableName, wherecond)
	else
		return false
	end
end

------------------------------------------------------------
-- Inserts current entity/model (changing timestamp for creation and modification in case defined)
------------------------------------------------------------
local function Insert(self, entity)
	local rowEscaped = Utils.EscapeValues(self, entity)
	local currentTime = os.date("!%Y-%m-%d %H:%M:%S")	-- hora actual en UTC
	if self.createdField and self.columnInfo[self.createdField] then
		self[self.createdField] = currentTime
		rowEscaped[self.createdField] = Utils.EscapeValue(self.dataSource, self.columnInfo[self.createdField], currentTime)
	end
	if self.updatedField and self.columnInfo[self.updatedField] then
		entity[self.updatedField] = currentTime
		rowEscaped[self.updatedField] = Utils.EscapeValue(self.dataSource, self.columnInfo[self.updatedField], currentTime)
	end
	local columns, values = {}, {}
	for k, v in pairs(rowEscaped) do
		--print("Field: "..k.." - Type: "..self.columnInfo[k].type.. " - AutoNumber: ", self.columnInfo[k].isAutoNumber)
		if (not self.columnInfo[k].isAutoNumber) then
			table.insert(columns, self.dataSource.Format("%Q", k))
			table.insert(values, v)
		end
	end
	return self.dataSource:Insert(self.tableName, columns, values)
end



------------------------------------------------------------
-- Save the entity
------------------------------------------------------------
function Save(self, entity)
	if entity.m_newRecord then
		return Insert(self, entity)
	else
		return Update(self, entity)
	end
end
