module(...,package.seeall)

require "json";
function process(self)
	self = self or {}
	if not self.flowDefinition then
		error("flowDefinition argument is required",2);
	end
	if not self.instanceID then
		error("instanceID argument is required",2);
	end
	if not self.flowName then
		error("flowName argument is required",2);
	end

	self.data = self.processData or {}
	self.data.__pl__ = self.data.__pl__ or 1  -- Program line
	self.data.__al__ = self.data.__al__ or 1  -- Activity line
	local runenv = {}
	local built_ins = {
		print=print,
		loadstring=loadstring,
		pcall=pcall,
		pairs=pairs,ipairs=ipairs,
		_G=self.data,
		setfenv=setfenv,
		tostring = tostring,
		tonumber = tonumber,
		type = type,
		json = json,
		table = table,
		string = string,
		require = require
	}

	setmetatable(self.data,{
		__index=function(self,idx)
			return built_ins[idx] or runenv[idx]
		end,
	});
	local activityOutput
	function self:getData()
		return self.data;
	end
	function self:getInstanceID()
		return self.instanceID
	end
	function self:getFlowName()
		return self.flowName
	end
	function self:continue(params,environment)
		local loadstring = loadstring;
		local pcall = pcall;
		local setfenv = setfenv;

		if type(environment) == "table" then
			runenv=environment
		else
			runenv = {}
		end
		if not self.flowDefinition then
			error("Can't continue a unknown flowDefinition",2)
		end

		params = params or {}
		for k,v in pairs(params) do
			self.data[k]=v
		end
		LogDebug("flowrun:interaction=%s continue arguments=%s",self.data.interactionId or "",json.encode(params))
		
		local pl
		repeat
			pl = self.data.__pl__
			local cl = self.flowDefinition.run[pl];
			if cl then
				if cl.transition then
					local idx = 0;
					repeat
						if activityOutput == cl.result then
							self.data.__pl__ = cl.goto
							self.data.__al__ = 1
							break;
						end
						idx=idx+1
						cl = self.flowDefinition.run[pl+idx]
					until not cl or not cl.transition or pl ~= self.data.__pl__
				else
					LogDebug("flowrun:interaction=%s activity=%s  actline=%d",self.data.interactionId or "",cl.exec,self.data.__al__);
					local act = self.flowDefinition.activities[cl.exec]
					local f,err = loadstring(act.lines[self.data.__al__].text or "")
					built_ins.self = {
						config=cl
					};
					if f then
						setfenv(f,self.data);
						local r,result = pcall(f)

						if not r then
							error("flowrun:interaction=" .. (self.data.interactionId or "") .. " activity=" .. cl.exec .. " line=" .. self.data.__al__ .. " error=" .. result,2)
						end
						if type (result)=="string" then
							LogDebug("flowrun:interaction=%s activity=%s exitcode=%s",self.data.interactionId or "",cl.exec,result);
							self.data.__pl__=self.data.__pl__+1;
							self.data.__al__ = 1
							activityOutput=result;
						elseif type(result) == "number" then
							LogDebug("flowrun:interaction=%s activity=%s suspended=%d",self.data.interactionId or "",cl.exec,result);
							self.data.__al__ = result;
						end
					else
						error(err)
					end
				end
			end
		until pl == self.data.__pl__
	end

	return self
end

function flowSource(self)
	self = self or {}
	function self:load(flowName)
	end
	function self:store(flowName,flowDefinition)
	end
	return self
end

function processPersistence(self)
	self = self or {}
	if not self.vccName then
		error("vccName argument is required",2)
	end
	function self:store(process)
	end
	function self:load(instanceID)
	end

	return self
end

function filesystemFlowSource(self)
	self = self or {}
	if not self.vccName then
		error("vccName argument is required",2);
	end
	self.basePath = self.basePath or "./"

	self = flowSource(self)

	function self:load(flowName)
		local fh = io.open(self.basePath .. self.vccName .. "." .. flowName .. ".json","rb")
		if not fh then
			return
		end
		local flowDefinition
		pcall(
			function()
				flowDefinition= json.decode(fh:read("*a"))
			end
		)
		fh:close();
		return flowDefinition;

	end
	function self:store(flowName,flowDefinition)
		local fh = io.open(self.basePath .. self.vccName .. "." .. flowName .. ".json","wb")
		if not fh then
			return
		end
		fh:write(json.encode(flowDefinition,true))
		fh:close();
		return true

	end

	return self
end

function filesystemProcessPersistence(self)
	self = self or {}
	if not self.vccName then
		error("vccName argument is required",2);
	end
	self.basePath = self.basePath or "./"
	self = processPersistence(self)

	function self:store(process)
		local fh = io.open(self.basePath .. self.vccName .. "." .. process:getInstanceID() .. ".json","wb")
		if not fh then
			return false
		end
		local pdata = {
			flowName = process:getFlowName(),
			data=process:getData()
		}
		fh:write(json.encode(pdata,true))
		fh:close()
		return true
	end
	function self:load(instanceID)
		local fh = io.open(self.basePath .. self.vccName .. "." .. instanceID .. ".json","rb")
		if not fh then
			return false
		end
		local s = fh:read("*a")
		fh:close()
		return json.decode(s);
	end

	return self
end
function redisProcessPersistence(self)
	self = self or {}
	if not self.vccName then
		error("vccName argument is required",2);
	end
	if not self.redisClient then
		error("redisClient argument is required",2);
	end

	self = processPersistence(self)
	function self:store(process)
		local pdata = {
			flowName = process:getFlowName(),
			data=process:getData()
		}
		self.redisClient:set("flowprocessdata:" .. self.vccName .. ":" .. process:getInstanceID(), json.encode(pdata))
		return true;
	end
	function self:load(instanceID)
		local s = self.redisClient:get("flowprocessdata:" .. self.vccName .. ":" .. instanceID)
		if s then
			return json.decode(s);
		else
			return false
		end
	end

	return self
end
function redisFlowSource(self)
	self = self or {}
	if not self.vccName then
		error("vccName argument is required",2);
	end
	if not self.redisClient then
		error("redisClient argument is required",2);
	end

	self = flowSource(self)

	function self:load(flowName)
		local flowDefinition= self.redisClient:get("flowdefinition:" .. self.vccName .. ":" .. flowName);
		if flowDefinition then
			pcall(
				function()
					flowDefinition= json.decode(flowDefinition)
				end
			)
		end
		return flowDefinition;

	end
	function self:store(flowName,flowDefinition)
		self.redisClient:set("flowdefinition:" .. self.vccName .. ":" .. flowName,json.encode(flowDefinition))
		return true
	end

	return self
end
function IRProcessPersistence(self)
	self = self or {}
	if not self.vccName then
		error("vccName argument is required",2);
	end
	if not self.UpdateInteractionData then
		error("UpdateInteractionData function argument is required",2)
	end
	if not self.GetInteraction then
		error("GetInteraction function argument is required",2)
	end

	if not self.interactionId then
		error("interactionId argument is required",2)
	end


	self = processPersistence(self)
	function self:store(process)
		local pdata = {
			flowName = process:getFlowName(),
			data=process:getData()
		}
		self.UpdateInteractionData(self.interactionId,function(data)
			data.processInstanceData = pdata;
			return data;
		end)
		return true;
	end
	function self:load(instanceID)
		local interaction = self.GetInteraction(self.interactionId)

		if interaction and interaction.interactionData.processInstanceData then
			return interaction.interactionData.processInstanceData;
		else
			return false
		end
	end

	return self
end


function processMngr(self)
	self=self or {}
	local pmngr = self;
	if not self.flowSource then
		error("flowSource argument is required",2)
	end

	if not self.processPersistence then
		error("processPersistence argument is required",2)
	end

	local function pinstance(process)
		local self = {}
		function self:continue(args,environment)
			local persist = function()
				local data = process:getData();
				if not pmngr.processPersistence:store(process) then
					error("Can't persist process information ",3)
				end
			end
			local env = environment or pmngr.environment
			env._process = {
				synccall=function(self,activityLine,fn,...)
					if type(activityLine) == "number" then
						process:getData().__al__ = activityLine
						local status,err = pcall(
							function()
								persist();
							end
						)
						if status then
							status,err = pcall(fn,...)
						end
						return status,err
					else
						return false, "Invalid line number argument en synccall"
					end
				end
			};
			local res = process:continue(args,environment or pmngr.environment)
			persist();
			return res;
		end
		return self
	end

	function self:new(args)
		args=args or {}
		if not args.flowName then
			error("flowName argument is required",2)
		end
		local flowDefinition=self.flowSource:load(args.flowName)
		if not flowDefinition then
			error("Can't load flow definition " .. args.flowName,2)
		end
		return pinstance(process({instanceID=args.instanceID or os.time(),flowDefinition=flowDefinition,flowName=args.flowName}))
	end


	function self:load(instanceID)
		if not instanceID then
			error("instanceID argument is required",2)
		end

		local instanceData=self.processPersistence:load(instanceID)
		if not instanceData then
			error("Can't load process instance data",2)
		end
		local flowDefinition=self.flowSource:load(instanceData.flowName)
		if not flowDefinition then
			error("Can't load flow definition " .. instanceData.flowName,2)
		end
		return pinstance(process({instanceID=instanceID,flowDefinition=flowDefinition,flowName=instanceData.flowName,processData=instanceData.data}))


	end
	return self
end

function IRProcessMngr(self)
	self = self or {}
	local m_UpdateInteractionData
	if not self.UpdateInteractionData then -- or not GetInteraction or not scriptData or not sidereal then
		error("UpdateInteractionData argument is required")
	end
	if not self.GetInteraction then -- or not GetInteraction or not scriptData or not sidereal then
		error("GetInteraction argument is required")
	end
	if not self.scriptData then -- or not GetInteraction or not scriptData or not sidereal then
		error("scriptData argument is required")
	end

	if not self.vccName then
		error("vccName argument is required")
	end
	if not self.interactionId then
		error("interactionId argument is required",2)
	end

	local function send_auth(client_redis, pwd)
		if not pwd or type(pwd) ~= 'string' or pwd == "" then return true end
		local authresult = client_redis:auth(pwd)
		return authresult == "OK"
	end	
	local redisClient = sidereal.connect(self.scriptData.redisIP,self.scriptData.redisPort)
	redisClient = send_auth(redisClient, os.getenv("REDIS_PWD")) and redisClient or nil

	self.flowSource = redisFlowSource{
		vccName=self.vccName,
		redisClient = redisClient
	}
	self.processPersistence=IRProcessPersistence{
		vccName=self.vccName,
		UpdateInteractionData=self.UpdateInteractionData,
		GetInteraction=self.GetInteraction,
		interactionId = self.interactionId
	}
	self = processMngr(self);
	local load = self.load
	function self:load()
		local status,res = pcall(
			function()
				return load(self,"")
			end
		)
		if not status then
			LogDebug("flowrun: description=%s",res)
			return {
				['continue']=function()
					return false
				end
			}
		else
			return {
				['continue']=function(_,args)
					local status,result = pcall(function()
						return res:continue(args)
					end)
					if not status then
						LogDebug("flowrun: description=%s",result)
					end
					
					return status
				end
			}
		end
	
	end
	local new = self.new
	function self:new(args)
		local status,res = pcall(
			function()
				return new(self,args)
			end
		)
		if not status then
			LogDebug("flowrun: flowName=%s description=%s",args.flowName,res)
			return {
				['continue']=function()
					return false
				end
			}
		else
			return {
				['continue']=function(_,args)
					local status,result = pcall(function()
						return res:continue(args)
					end)
					if not status then
						LogDebug("flowrun: description=%s",result)
					end
					return status
				end
			}
		end
	end


	return self
end


return {
	processMngr=processMngr,
	filesystemFlowSource=filesystemFlowSource,
	redisFlowSource=redisFlowSource,
	filesystemProcessPersistence=filesystemProcessPersistence,
	redisProcessPersistence=redisProcessPersistence,

	IRProcessMngr=IRProcessMngr,
	IRProcessPersistence=IRProcessPersistence
}



--[=[
i=os.time()

local  p = processMngr({
	flowSource=filesystemFlowSource{
		vccName="designer",
		basePath=[[C:\Users\Fabian\Documents\shared\flows\]]

	},
	processPersistence=filesystemProcessPersistence{
		vccName="designer",
		basePath=[[C:\Users\Fabian\Documents\shared\flows\]]
	}
})

--p:new{flowName="flow1",instanceID="test1"}
--:continue{};

for i=1,10 do
	p:load("test1"):continue({i=i})
end


print(os.time()-i)

--]=]
