--[[

	Tube Library
	============

	Copyright (C) 2017-2020 Joachim Stolberg

	AGPL v3
	See LICENSE.txt for more information

	node_states.lua:

	A state model/class for tubelib nodes.

]]--


--[[ 

Node states:

        +-----------------------------------+    +------------+
        |                                   |    |            |                                      
        |                                   V    V            |                                      
        |                                +---------+          |                                      
        |                                |         |          |                                      
        |                      +---------| STOPPED |          |                                      
        |                      |         |         |          |                                      
        |               button |         +---------+          |                                      
        |                      |              ^               |                                      
 repair |                      V              | button        |                                      
        |                 +---------+         |               | button                               
        |                 |         |---------+               |                                      
        |                 | RUNNING |                         |                                      
        |        +--------|         |---------+               |                                      
        |        |        +---------+         |               |                                      
        |        |           ^    |           |               |                                      
        |        |           |    |           |               |                                      
        |        V           |    V           V               |                                      
        |   +---------+   +----------+   +---------+          |                                      
        |   |         |   |          |   |         |          |                                      
        +---| DEFECT  |   | STANDBY/ |   |  FAULT  |----------+                                      
            |         |   | BLOCKED  |   |         |                                                 
            +---------+   +----------+   +---------+                                                 

Node metadata:
	"tubelib_number"     - string with tubelib number, like "0123"
	"tubelib_state"      - node state, like "RUNNING"
	"tubelib_item_meter" - node item/runtime counter
	"tubelib_countdown"  - countdown to stadby mode
	"tubelib_aging"      - aging counter
]]--

-- for lazy programmers
local S = function(pos) if pos then return minetest.pos_to_string(pos) end end
local P = minetest.string_to_pos
local M = minetest.get_meta


local AGING_FACTOR = 4  -- defect random factor

--
-- Local States
--
local STOPPED = tubelib.STOPPED
local RUNNING = tubelib.RUNNING
local STANDBY = tubelib.STANDBY
local FAULT   = tubelib.FAULT
local BLOCKED = tubelib.BLOCKED
local DEFECT  = tubelib.DEFECT


--
-- NodeStates Class Functions
--
tubelib.NodeStates = {}
local NodeStates = tubelib.NodeStates

local function start_condition_fullfilled(pos, meta)
	return true
end

function NodeStates:new(attr)
	local o = {
		-- mandatory
		cycle_time = attr.cycle_time, -- for running state
		standby_ticks = attr.standby_ticks, -- for standby state
		has_item_meter = attr.has_item_meter, -- true/false
		-- optional
		node_name_passive = attr.node_name_passive,
		node_name_active = attr.node_name_active, 
		node_name_defect = attr.node_name_defect,
		infotext_name = attr.infotext_name,
		start_condition_fullfilled = attr.start_condition_fullfilled or start_condition_fullfilled,
		on_start = attr.on_start,
		on_stop = attr.on_stop,
		formspec_func = attr.formspec_func,
	}
	if attr.aging_factor then
		o.aging_level1 = attr.aging_factor * tubelib.machine_aging_value
		o.aging_level2 = attr.aging_factor * tubelib.machine_aging_value * AGING_FACTOR
	end
	setmetatable(o, self)
	self.__index = self
	return o
end

function NodeStates:node_init(pos, number)
	local meta = M(pos)
	meta:set_int("tubelib_state", STOPPED)
	meta:set_string("tubelib_number", number)
	if self.infotext_name then
		meta:set_string("infotext", self.infotext_name.." "..number..": stopped")
	end
	if self.has_item_meter then
		meta:set_int("tubelib_item_meter", 0)
	end
	if self.aging_level1 then
		meta:set_int("tubelib_aging", 0)
	end
	if self.formspec_func then
		meta:set_string("formspec", self.formspec_func(self, pos, meta))
	end
end

function NodeStates:stop(pos, meta)
	local state = meta:get_int("tubelib_state")
	if state ~= DEFECT then
		if self.on_stop then
			self.on_stop(pos, meta, state)
		end
		meta:set_int("tubelib_state", STOPPED)
		if self.node_name_passive then
			local node = minetest.get_node(pos)
			node.name = self.node_name_passive
			minetest.swap_node(pos, node)
		end
		if self.infotext_name then
			local number = meta:get_string("tubelib_number")
			meta:set_string("infotext", self.infotext_name.." "..number..": stopped")
		end
		if self.formspec_func then
			meta:set_string("formspec", self.formspec_func(self, pos, meta))
		end
		minetest.get_node_timer(pos):stop()
		return true
	end
	return false
end

function NodeStates:start(pos, meta, called_from_on_timer)
	local state = meta:get_int("tubelib_state")
	if state == STOPPED or state == STANDBY or state == BLOCKED then
		if not self.start_condition_fullfilled(pos, meta) then
			return false
		end
		if self.on_start then
			self.on_start(pos, meta, state)
		end
		meta:set_int("tubelib_state", RUNNING)
		meta:set_int("tubelib_countdown", 4)
		if called_from_on_timer then
			-- timer has to be stopped once to be able to be restarted
			self.stop_timer = true
		end
		if self.node_name_active then
			local node = minetest.get_node(pos)
			node.name = self.node_name_active
			minetest.swap_node(pos, node)
		end
		if self.infotext_name then
			local number = meta:get_string("tubelib_number")
			meta:set_string("infotext", self.infotext_name.." "..number..": running")
		end
		if self.formspec_func then
			meta:set_string("formspec", self.formspec_func(self, pos, meta))
		end
		minetest.get_node_timer(pos):start(self.cycle_time)
		return true
	end
	return false
end

function NodeStates:standby(pos, meta)
	if meta:get_int("tubelib_state") == RUNNING then
		meta:set_int("tubelib_state", STANDBY)
		-- timer has to be stopped once to be able to be restarted
		self.stop_timer = true
		if self.node_name_passive then
			local node = minetest.get_node(pos)
			node.name = self.node_name_passive
			minetest.swap_node(pos, node)
		end
		if self.infotext_name then
			local number = meta:get_string("tubelib_number")
			meta:set_string("infotext", self.infotext_name.." "..number..": standby")
		end
		if self.formspec_func then
			meta:set_string("formspec", self.formspec_func(self, pos, meta))
		end
		minetest.get_node_timer(pos):start(self.cycle_time * self.standby_ticks)
		return true
	end
	return false
end	

-- special case of standby for pushing nodes
function NodeStates:blocked(pos, meta)
	if meta:get_int("tubelib_state") == RUNNING then
		meta:set_int("tubelib_state", BLOCKED)
		-- timer has to be stopped once to be able to be restarted
		self.stop_timer = true
		if self.node_name_passive then
			local node = minetest.get_node(pos)
			node.name = self.node_name_passive
			minetest.swap_node(pos, node)
		end
		if self.infotext_name then
			local number = meta:get_string("tubelib_number")
			meta:set_string("infotext", self.infotext_name.." "..number..": blocked")
		end
		if self.formspec_func then
			meta:set_string("formspec", self.formspec_func(self, pos, meta))
		end
		minetest.get_node_timer(pos):start(self.cycle_time * self.standby_ticks)
		return true
	end
	return false
end	

function NodeStates:fault(pos, meta)
	if meta:get_int("tubelib_state") == RUNNING then
		meta:set_int("tubelib_state", FAULT)
		if self.node_name_passive then
			local node = minetest.get_node(pos)
			node.name = self.node_name_passive
			minetest.swap_node(pos, node)
		end
		if self.infotext_name then
			local number = meta:get_string("tubelib_number")
			meta:set_string("infotext", self.infotext_name.." "..number..": fault")
		end
		if self.formspec_func then
			meta:set_string("formspec", self.formspec_func(self, pos, meta))
		end
		minetest.get_node_timer(pos):stop()
		return true
	end
	return false
end	

function NodeStates:defect(pos, meta)
	meta:set_int("tubelib_state", DEFECT)
	if self.node_name_defect then
		local node = minetest.get_node(pos)
		node.name = self.node_name_defect
		minetest.swap_node(pos, node)
	end
	if self.infotext_name then
		local number = meta:get_string("tubelib_number")
		meta:set_string("infotext", self.infotext_name.." "..number..": defect")
	end
	if self.formspec_func then
		meta:set_string("formspec", self.formspec_func(self, pos, meta))
	end
	minetest.get_node_timer(pos):stop()
	return true
end	

function NodeStates:get_state(meta)
	return meta:get_int("tubelib_state")
end

function NodeStates:get_state_string(meta)
	return tubelib.StateStrings[meta:get_int("tubelib_state")]
end

function NodeStates:is_active(meta)
	local state = meta:get_int("tubelib_state")
	if self.stop_timer == true then
		self.stop_timer = false
		return false
	end
	return state == RUNNING or state == STANDBY or state == BLOCKED
end

-- To be called if node is idle.
-- If countdown reaches zero, the node is set to STANDBY.
function NodeStates:idle(pos, meta)
	local countdown = meta:get_int("tubelib_countdown") - 1
	meta:set_int("tubelib_countdown", countdown)
	if countdown < 0 then
		self:standby(pos, meta)
	end
end

-- To be called after successful node action to raise the timer
-- and keep the node in state RUNNING
function NodeStates:keep_running(pos, meta, val, num_items)
	if not num_items or num_items < 1 then num_items = 1 end
	-- set to RUNNING if not already done
	self:start(pos, meta, true)
	meta:set_int("tubelib_countdown", val)
	meta:set_int("tubelib_item_meter", meta:get_int("tubelib_item_meter") + (num_items or 1))
		
	if self.aging_level1 then
		local cnt = meta:get_int("tubelib_aging") + num_items
		meta:set_int("tubelib_aging", cnt)
		if (cnt > (self.aging_level1) and math.random(math.max(1, math.floor(self.aging_level2/num_items))) == 1)
		or cnt >= 999999 then
			self:defect(pos, meta)
		end
	end
end

-- Start/stop node based on button events.
-- if function returns false, no button was pressed
function NodeStates:state_button_event(pos, fields)
	if fields.state_button ~= nil then
		local state = self:get_state(M(pos))
		if state == STOPPED or state == STANDBY or state == BLOCKED then
			self:start(pos, M(pos))
		elseif state == RUNNING or state == FAULT then
			self:stop(pos, M(pos))
		end
		return true
	end
	return false
end

function NodeStates:get_state_button_image(meta)
	local state = meta:get_int("tubelib_state")
	return tubelib.state_button(state)
end

-- command interface
function NodeStates:on_receive_message(pos, topic, payload)
	if topic == "on" then
		self:start(pos, M(pos))
		return true
	elseif topic == "off" then
		self:stop(pos, M(pos))
		return true
	elseif topic == "state" then
		local node = minetest.get_node(pos)
		if node.name == "ignore" then  -- unloaded node?
			return "blocked"
		end
		return self:get_state_string(M(pos))
	elseif self.has_item_meter and topic == "counter" then
		return M(pos):get_int("tubelib_item_meter")
	elseif self.has_item_meter and topic == "clear_counter" then
		M(pos):set_int("tubelib_item_meter", 0)
		return true
	elseif self.aging_level1 and topic == "aging" then
		return M(pos):get_int("tubelib_aging")
	end
end
	
-- repair corrupt node data and/or migrate node to state2
function NodeStates:on_node_load(pos, not_start_timer)
	local meta = minetest.get_meta(pos)
	
	-- legacy node number/state/counter?
	local number = meta:get_string("number")
	if number ~= "" and number ~= nil then
		meta:set_string("tubelib_number", number)
		meta:set_int("tubelib_state", tubelib.state(meta:get_int("running")))
		if self.has_item_meter then
			meta:set_int("tubelib_item_meter", meta:get_int("counter"))
		end
		if self.aging_level1 then
			meta:set_int("tubelib_aging", 0)
		end
		meta:set_string("number", nil)
		meta:set_int("running", 0)
		meta:set_int("counter", 0)
	end

	-- node corrupt?
	if not tubelib.data_not_corrupted(pos) then
		return
	end
	
	-- state corrupt?
	local state = meta:get_int("tubelib_state")
	if state == 0 then
		if minetest.get_node_timer(pos):is_started() then
			meta:set_int("tubelib_state", RUNNING)
		else
			meta:set_int("tubelib_state", STOPPED)
		end
	elseif state == RUNNING and not not_start_timer then
		minetest.get_node_timer(pos):start(self.cycle_time)
	elseif state == STANDBY then
		minetest.get_node_timer(pos):start(self.cycle_time * self.standby_ticks)
	elseif state == BLOCKED then
		minetest.get_node_timer(pos):start(self.cycle_time * self.standby_ticks)
	end
	
	if self.formspec_func then
		meta:set_string("formspec", self.formspec_func(self, pos, meta))
	end
end

-- Repair of defect (feature!) nodes
function NodeStates:on_node_repair(pos)
	local meta = M(pos)
	if meta:get_int("tubelib_state") == DEFECT then
		meta:set_int("tubelib_state", STOPPED)
		if self.node_name_passive then
			local node = minetest.get_node(pos)
			node.name = self.node_name_passive
			minetest.swap_node(pos, node)
		end
		if self.aging_level1 then
			meta:set_int("tubelib_aging", 0)
		end
		if self.infotext_name then
			local number = meta:get_string("tubelib_number")
			meta:set_string("infotext", self.infotext_name.." "..number..": stopped")
		end
		if self.formspec_func then
			meta:set_string("formspec", self.formspec_func(self, pos, meta))
		end
		return true
	end
	return false
end	


--[[
Callback after digging a node but before removing the node.

The tubelib node becomes defect after digging it:
	- always if the aging counter "tubelib_aging" is greater than self.aging_level2
	- with a certain probability if the aging counter "tubelib_aging" is greater than self.aging_level1
	but smaller than self.aging_level2
	
Info: If a tubelib machine has been running quite some time but is dropped as a non-defect machine and then placed back again, the
tubelib machine will be reset to new (digging will reset the aging counter). So this code tries to prevent this exploit

]]--
function NodeStates:on_dig_node(pos, node, player)
	local meta = M(pos)
	local cnt = tonumber(meta:get_string("tubelib_aging"))
	if (not cnt or cnt < 1) then
		cnt = 1
	end
	
	local is_defect = (cnt > self.aging_level1) and ( math.random(math.max(1, math.floor(self.aging_level2 / cnt))) == 1 )
	
	if is_defect then
			self:defect(pos, meta) -- replace node with defect one 
		node = minetest.get_node(pos) 
	end
	
	
	minetest.node_dig(pos, node, player) -- default behaviour (this function is called automatically if on_dig() callback isn't set)

end