-------------------------------------------------------------------------------
-- Copyright (c) 2015 Sierra Wireless. All rights reserved.
--
-- Contributors:
--     Randall Pfeifer - initial implementation
--
--	MQTT Protocol handler
--	Creates an object that starts a series of tasks to establish and maintain
--	a connection with the MQTT broker, recieve and queue data to be sent,
--	retain unsent data in a locally persistent data store, and send the data
--	to the broker when connection is available.
-------------------------------------------------------------------------------

-- ----------------------------------------------------------------------------
-- REQUIRES
-- ----------------------------------------------------------------------------

-- Add current directory to cpath so we can link in our binary libs
package.cpath = package.cpath .. ";./?.so"

local mqtt_lua = require 'mqtt_lua'
local loader = require 'loader'
local systime = require "sched.timer.core"
local string = require 'string'

loader.addpath "AVTA"

-- ----------------------------------------------------------------------------
-- CONSTANTS
-- ----------------------------------------------------------------------------

-- Parameters for saving data queue to file system. Attempt to
-- balance data integrity with write frequency (minimise flash wear) 
local CONNECT_POLL_RATE = 0.2	-- Rate for connect poll
local TX_POLL_RATE = 0.2		-- Rate for Tx poll

local MAX_SEND_WAIT = 10		-- If unable to clear a message after this long, reset connection
local MAX_CONNECT_WAIT = 10		-- Max time to wait for connection establishment

-- Parameters for signalling within the mqtt module.
local MQTT_EMITTER = "AVTA_MQTT"
local SIG_STOP = "STOP"

------------------------------------------------------------------------------------------
-- Format a table of data for sending. Converts non-numeric data types to strings		--
-- (lists become comma-separated values)
------------------------------------------------------------------------------------------
local function format_data (assetname, data)
	local val, value
	
	local timestamp = data.timestamp
	if not timestamp then
		-- No valid timestamp - create one (999 last 3 digits indicates this happened)
		timestamp = (os.time() * 1000) - 1
	end
	
	local payload = ""

	local asset = ""
	if assetname ~= "" then
		asset = assetname.."."
	end

	for key, val in pairs(data) do
		if key ~= "timestamp" then	-- Don't include timestamp in data (and don't change the data)
			-- Format the data point into value, according to it's type
			if type(val) == "number" then
				-- number - just convert to string
				value = tostring(val)
			elseif type(val) == "table" then
				-- a list of values - convert to comma separated string, is quotes
				value = '"'
				for i,v in ipairs(val) do
					if i == 1 then
						value = string.format("%s%s", value, v)
					else
						value = string.format("%s,%s", value, v)
					end
				end
				value = string.format('%s"', value)
			else
				-- anything else - convert to string in quotes
				value = string.format('"%s"', tostring(val))
			end

			if payload == "" then
				payload = string.format('"%s":{"%s%s":%s',timestamp, asset, key, value)
			else
				payload = string.format('%s,"%s%s":%s', payload, asset, key, value)
			end
		end
	end
	payload = payload.."}"

	return payload
end

------------------------------------------------------------------
-- m_send - method to send data via mqtt
-- 
-- Inputs:
--	topic			- mqtt topic to send data to
--
-- Returns:
-- 	true		- data was queued to send
-- 	false		- queue is full
--
-- Notes
-- 	formats the data into JSON format appropriate for AVMS
-- 	(including time stamp).
-- 	
-- 	Inserts message to indicate queue is full before it stops accepting
-- 	data. Next data poing after this message is next data sent. 
------------------------------------------------------------------
local function m_send(self, data)
	if type(data) ~= "table" then
		-- Empty data set - do nothing
		return nil, "Invalid data"
	elseif not self.mqtt_lib then
		return nil, "Not active"
	elseif self.mqtt_lib:pending() ~= 0 then
		return nil, "Message pending"
	else
		if #data < 1 then
			log(self.app_name, "DETAIL", "MQTT empty data")
		elseif self:connect() then
			local payload = "{}"
			local msg = {}
			for n, content in ipairs(data) do
				table.insert(msg, format_data(self.assetname, content))
			end
			payload = "{" .. table.concat(msg, ",") .. "}"

			log(self.app_name, "DETAIL", "MQTT Send: %s - %s", self.topic, payload)
			local send_time = systime.time()

			local rc = self.mqtt_lib:send(self.topic, payload)
			if rc ~= 0 then
				return nil, string.format("mqtt_lib:send error %d", rc)
			end

			repeat
				if systime.time() > send_time + self.send_wait then
					-- AVMS has nasty habit of not acknowledging messages it
					-- doesn't like. MQTT stack tries to send forever. If waiting
					-- too long, pop the offending message and dump the connection
					-- (it will re-connect) to clear the log jam.
					log(self.app_name, "WARNING", "MQTT IS STUCK - restarting connection")
					self.mqtt_lib:disconnect()
					return nil, "Timeout"
				end

				-- ToDo: use a signal from mqtt lib instead of polling for send
				if sched.wait(MQTT_EMITTER, {SIG_STOP, TX_POLL_RATE}) == SIG_STOP then
					return nil, "terminating"
				end
			until self.mqtt_lib:pending() == 0
		else
			return nil, "Not connected"
		end
	end
	return "ok"
end
	
------------------------------------------------------------------
-- m_run - method to start mqtt handler
-- 
-- Inputs:
--
-- Returns:
-- 	true		- mqtt started
-- 	false		- mqtt not started or already running
--
-- Notes
--	Starts the MQTT handler system. Takes care of establishing and
--	maintaining the connection, sending data, and ensuring that queued
--	data is saved to disk so that data is not lost on reboot. Starts
--	background tasks to handle these.
------------------------------------------------------------------
local function m_run(self)
	if self.mqtt_lib ~= nil then
		return false
	end
	
	log (self.app_name,"WARNING", "Instantiating MQTT with connection to " .. self.address )
	self.mqtt_lib = mqtt_lua.create(self.address, self.cid)
	self:connect()

	return true
end

------------------------------------------------------------------
-- m_stop - method to stop MQTT handler
-- 
-- Inputs:
--
-- Returns:
-- 	true		- mqtt stopped
-- 	false		- mqtt not stopped (not running)
--
-- Notes
--	Stops the MQTT handler system.
--	Disconnect MQTT and Stop the MQTT processes (send, connect, save).
--	The queue should be saved on stop, but data may continue
--	to accumulate
------------------------------------------------------------------
local function m_stop (self)
	if not self.mqtt_lib then
		return false
	end

	if self:isconnected() then
		self.mqtt_lib:disconnect()
		while self:isconnected() do
			sched.wait(CONNECT_POLL_RATE)
		end
	end

	-- Delete mqtt_lib instance - gc should trigger destroy
	-- and nil mqtt_lib will terminate associated tasks 
	self.mqtt_lib = nil
	sched.signal(MQTT_EMITTER, SIG_STOP)
	return true
end

------------------------------------------------------------------
-- m_configure - method to configure MQTT handler
-- 
-- Inputs:
--	config		- table of parameter name/value pairs
--
-- Returns:
--
-- Notes
--	Use to configure MQTT parameters before starting MQTT manager.
--	Do not set config parameters while running. Stop mqtt,
--	set config, then re-start 
------------------------------------------------------------------
local function m_configure(self, config)
	self.address = config.address or self.address
	self.cid = config.cid or self.cid
	self.userid = config.userid or self.userid
	self.password =  config.password or self.password
	self.topic =  config.topic or self.topic
	self.assetname =  config.assetname or self.assetname
	self.connect_wait = config.connect_wait or self.connect_wait
	self.send_wait = config.send_wait or self.send_wait
	self.cert_pack = config.certpack or self.cert_pack
	self.truststore = config.truststore or self.truststore
	self.keystore = config.keystore or self.keystore
	self.privatekey = config.privatekey or self.privatekey
end

------------------------------------------------------------------
-- m_isconnected - method to get connection state
-- 
-- Inputs:
--	config		- table of parameter name/value pairs
--
-- Returns:
--	(1)
--	true	- if connected
--	false	- if not connected
--
--	(2)
--	connection progress (if running)
--	nothing if not running
--
-- Notes
------------------------------------------------------------------
local function m_isconnected(self)
	local names = {"idle", "connecting", "connected", "failed", "lost"}
	if not self.mqtt_lib then
		return false
	end
	local connected, status = self.mqtt_lib:isconnected()
	return connected, names[status]
end

------------------------------------------------------------------
-- m_connect - method to attempt to establish connection
-- 
-- Inputs:
--
-- Returns:
--	(1)
--	true	- if connected successfully
--	false	- if connection failed
--
--	(2)
--	connection state reason if failed
--
-- Notes
------------------------------------------------------------------
local function m_connect(self)
	local connect_time = systime.time()
	local connected, reason = self:isconnected()
	if connected then
		log(self.app_name, "DETAIL", "MQTT Already connected")
		return "ok"
	end

	self.connect_counter = self.connect_counter + 1
	if self.connect_counter % 120 == 0 then
		log(self.app_name,"INFO","Connects %d", self.connect_counter )	
	end

	-- If the url includes ssl:, and we have a client cert, connect with certificates
	-- otherwise, use user/password
	-- Bit ugly to use "clean=true" here, because that results in a re-connect on practically
	-- every send, but if we use "clean=false" then AVTA gets stuck and never re-connects
	-- if the WAN link suddenly switches (eg cell link fails and device switches to Wifi fallback)
	if self.keystore ~= nil and string.find(self.address, "ssl:") then
		log(self.app_name, "DETAIL", "MQTT connect params: ../" .. self.cert_pack .. "/cert/" .. self.truststore .. ", ../" .. self.cert_pack .. "/cert/" .. self.keystore .. ", ../" .. self.cert_pack .. "/cert/" .. self.privatekey )
		pcall(self.mqtt_lib.connect, self.mqtt_lib, {
											truststore = "../" .. self.cert_pack .. "/cert/" .. self.truststore,
											keystore = "../" .. self.cert_pack .. "/cert/" .. self.keystore,
											privatekey = "../" .. self.cert_pack .. "/cert/" .. self.privatekey,
											clean=true,
											enableservercertauth=0
		})
	else
		log(self.app_name, "DETAIL", "MQTT connect params: " .. self.userid .. ", pwd not shown" )
		pcall(self.mqtt_lib.connect, self.mqtt_lib, {
											userid=self.userid,
											password=self.password,
											clean=true,
											enableservercertauth=0
		})
	end
	
	repeat
		-- ToDo: use a signal from mqtt lib instead of polling for send
		if sched.wait(MQTT_EMITTER, SIG_STOP, CONNECT_POLL_RATE) == SIG_STOP then
			return nil, "Terminated"
		end
		connected, reason = self:isconnected()
	until connected or systime.time() > connect_time + self.connect_wait

	if connected then
		log(self.app_name, "DETAIL", "MQTT Sucessfully connected (" .. tostring(systime.time() - connect_time) .. "S)")
		return "ok"
	else
		if reason then
		    log(self.app_name, "WARNING", "MQTT Connect failure: %s", reason)
		else
			log(self.app_name, "WARNING", "MQTT Connect failure")
		end
		return nil, reason
	end
end

-- Function to create the MQTT instance. Returned by require
------------------------------------------------------------------
-- m_create - creator for the MQTT instance
-- 
-- Inputs:
--
-- Returns:
--	MQTT instance object
--
-- Notes
------------------------------------------------------------------
local function m_create(app_name)
	local m = {}
	m.app_name = app_name
	m.configure = m_configure
	m.run = m_run
	m.stop = m_stop
	m.isconnected = m_isconnected
	m.connect = m_connect
	m.send = m_send
	
	m.connect_wait = MAX_CONNECT_WAIT
	m.send_wait = MAX_SEND_WAIT
	
	-- useful for debugging
	m.format_data = format_data
	
	m.connect_counter = 0

	return m
end

return m_create