----------------------------------------------------------------------
--																	--
--	This is a direct port of WAN events generation code from AMM	--
--	module MSCIModel.java (ported to Lua, of course). So, the		--
--	events generated *should* be identical for same conditions.		--
--																	--
----------------------------------------------------------------------

local session = {}	-- holds variables

local EventModel = {
	DELS_WANSTATUS = 262,		-- p0=0|1, p1=0
	DELS_VPNSTATUS = 263,		-- p0=0|1, p1=0
    DELS_WANUP = 512,			-- p0=DPI, p1=0
    DELS_WANDOWN = 513,			-- p0=DPI, p1=0
    DELS_WANACTIVE = 514,		-- p0=DPI, p1=0
    DELS_WANNOTACTIVE = 515,	-- p0=DPI, p1=0
    DELS_WANADDRESS = 516,		-- p0=DPI, p1=addr
    DELS_WANTXBYTES = 520,		-- p0=DPI, p1=bytes
    DELS_WANRXBYTES = 521		-- p0=DPI, p1=bytes
}

-- Need to suppress sending redundant DELS events, but some
-- are complementary events, complicating this. This maps
-- those events to same filter slots, so those events can be
-- compared to each other. Where applicable, the filter number
-- is added to the DPI to create separate slots for each interface
local efids = {
	[EventModel.DELS_WANSTATUS] = 1,
	[EventModel.DELS_VPNSTATUS] = 2,
    [EventModel.DELS_WANUP] = 3,
    [EventModel.DELS_WANDOWN] = 3,
    [EventModel.DELS_WANACTIVE] = 4,
    [EventModel.DELS_WANNOTACTIVE] = 4,
    [EventModel.DELS_WANADDRESS] = 5,
    [EventModel.DELS_WANTXBYTES] = 6,
    [EventModel.DELS_WANRXBYTES] = 7
}

local MSCI_ID_NETWORK_STATE = 'aleos.259'
local MSCI_ID_IPADDRESS = 'aleos.301'

-- related to WAN state
local MSCI_ID_PRIMARY_WAN_IFACE = 'aleos.10438'		-- 1=cell, 2=Wi-Fi, 3=Ethernet
local MSCI_ID_WIFI_MODE = 'aleos.10451'				-- 0=disable, 1=LAN, 2=WAN, 3=both
local MSCI_ID_ETH_PORT4_MODE = 'aleos.5762'			-- 0=invalid, 1=LAN, 2=WAN, 3=mirror
local MSCI_ID_ETH_PORT1_MODE = 'aleos.2900'			-- 0=invalid, 1=LAN, 2=WAN, 3=mirror
local MSCI_ID_CELL_WAN_STATE = 'aleos.10415'		-- 0=not connected, 1=connected, 2=no service
local MSCI_ID_ETH_WAN_STATE  = 'aleos.10416'		-- 0=not connected, 1=connected, 2=no service
local MSCI_ID_WIFI_WAN_STATE = 'aleos.10417'		-- 0=not connected, 1=connected, 2=no service

local MSCI_ID_CELL_TX_BYTES = 'aleos.283'
local MSCI_ID_CELL_RX_BYTES = 'aleos.284'

local MSCI_ID_CELL_IP = 'aleos.303'
local MSCI_ID_WIFI_IP = 'aleos.304'
local MSCI_ID_ETH_IP = 'aleos.310'

local MSCI_ID_VPN1_STATE = 'aleos.3176'
local MSCI_ID_VPN2_STATE = 'aleos.3205'
local MSCI_ID_VPN3_STATE = 'aleos.3231'
local MSCI_ID_VPN4_STATE = 'aleos.3257'
local MSCI_ID_VPN5_STATE = 'aleos.3283'

-- platform specific MSCIIDs, may not present in other platforms/earlier versions
local MSCI_ID_LX60_ETH2_MODE = 'aleos.2850'

local ALEOS_WAN_CELL_LINK = 1
local ALEOS_WAN_WIFI_LINK = 2
local ALEOS_WAN_ETH_LINK  = 3
local ALEOS_WAN_DEFAULT_LINK = ALEOS_WAN_CELL_LINK

local NOT_CONNECTED = 0
local CONNECTED = 1
-- public static final int  NO_SERVICE = 2; /* this state is combined with 0 */

-- WiFi link modes
local LINK_MODE_DIS = 0
local LINK_MODE_AP = 1
local LINK_MODE_WAN = 2
local LINK_MODE_MIRROR = 3	-- WiFi
local LINK_MODE_BOTH = 3	-- eth

local ALEOS_CELLLINK_DPI = 0x30200000 -- 807403520
local ALEOS_WIFILINK_DPI = 0x30100000 -- 806354944
local ALEOS_ETHPORT4_DPI = 0x09030000 -- 151191552
local ALEOS_ETHPORT2_DPI = 0x09010000 -- 151060480
local ALEOS_ETHPORT1_DPI = 0x09000000 -- 150994944

local PLATFORM_AIRLINK_MP_70 = "MP70"
local PLATFORM_AIRLINK_LX_60 = "LX60"

-- Utility functions --
-----------------------

local function getPlatformType()
	return session.devicetree.get("system.hw_info.product_str")
end

-- Convert IP address as dotted quad string to integer
local function ipToInt (addr)
	local num = 0
	if type(addr) == 'string' then
		addr:gsub("%d+", function(s) num = num * 256 + tonumber(s) end)
	end
	return num
end

------------------------------------------------------------------
--	Functions Ported from AMM MSCIModel
------------------------------------------------------------------

-- convert linkType to DPI value
------------------------------------------------------------------
local function getDpiFromLinkType(linkType)
	if linkType == ALEOS_WAN_CELL_LINK then
		return ALEOS_CELLLINK_DPI
	elseif linkType == ALEOS_WAN_WIFI_LINK then
		return ALEOS_WIFILINK_DPI;
	elseif linkType == ALEOS_WAN_ETH_LINK then
		if getPlatformType() == PLATFORM_AIRLINK_MP_70 then
			return ALEOS_ETHPORT4_DPI
		elseif getPlatformType() == PLATFORM_AIRLINK_LX_60 then
			return ALEOS_ETHPORT2_DPI
		else
			return ALEOS_ETHPORT1_DPI
		end
	end

	return 0
end

-- Return the MSCI ID needed to get the IP address, given the dpi
------------------------------------------------------------------
local function dpiToIpAddrMsciId(dpi)
	if dpi == ALEOS_CELLLINK_DPI then
		return MSCI_ID_CELL_IP
	end
	if dpi == ALEOS_WIFILINK_DPI then
		return MSCI_ID_WIFI_IP
	end
	if	dpi == ALEOS_ETHPORT4_DPI or
		dpi == ALEOS_ETHPORT1_DPI or
		dpi == ALEOS_ETHPORT2_DPI then
		return MSCI_ID_ETH_IP
	end

	session.error("dpiToIpAddrMsciId got bad argument " .. dpi)
	return 0
end

-- Return the link state for specified link
------------------------------------------------------------------
local function getWanLinkState(linkType)
	local msciId
	if linkType == ALEOS_WAN_CELL_LINK then
		msciId = MSCI_ID_CELL_WAN_STATE
	elseif linkType == ALEOS_WAN_WIFI_LINK then
		msciId = MSCI_ID_WIFI_WAN_STATE
	elseif linkType ~= ALEOS_WAN_ETH_LINK then
		session.error("unexpected argument in getWanLinkState " .. tostring(linkType));
		return NOT_CONNECTED
	else
		msciId = MSCI_ID_ETH_WAN_STATE
	end

	local val = session.devicetree.get(msciId)
	local retval = tonumber(val)
	if retval == nil then
		session.error(String.format("Response to MSCI %s: %s",
			msciId, val) );
		return NOT_CONNECTED
	end

	-- 0 = not connected, 2 = no service
	if retval == CONNECTED then
		return retval
	else
		return NOT_CONNECTED
	end
end

-- Return true if linkType is WAN
------------------------------------------------------------------
local function isLinkWanMode(linkType)
	if linkType == ALEOS_WAN_CELL_LINK then
		return true
	end

	local val
	local msciId
	if linkType == ALEOS_WAN_WIFI_LINK then
		msciId = MSCI_ID_WIFI_MODE
	elseif linkType ~= ALEOS_WAN_ETH_LINK then
		session.error("unexpected argument in isLinkWanMode " .. tostring(linkType));
		return false
	else
		-- Can query eth WAN mode on MP70 and LX60 only.
		val = getPlatformType()
		if val == PLATFORM_AIRLINK_MP_70 then
			msciId = MSCI_ID_ETH_PORT4_MODE
		elseif val == PLATFORM_AIRLINK_LX_60 then
			msciId = MSCI_ID_LX60_ETH2_MODE
		else
			return false
		end
	end

	val = session.devicetree.get(msciId)
	local mode = tonumber(val)
	if mode == nil then
		session.error(string.format("Response to MSCI %s: %s",
			msciId, tostring(val)));
		return false
	end
	-- 0 = invalid, 1 = LAN, 3 = mirror (for WiFi 3 = both)
	if linkType == ALEOS_WAN_WIFI_LINK then
		return (mode == LINK_MODE_WAN or mode == LINK_MODE_BOTH)
	else
		return mode == LINK_MODE_WAN
	end
end

-- Return 1 if any VPN is connected, else 0
------------------------------------------------------------------
local function atLeastOneVpnIsUp()
	local vpnStateIds = {MSCI_ID_VPN1_STATE, MSCI_ID_VPN2_STATE,
		MSCI_ID_VPN3_STATE, MSCI_ID_VPN4_STATE, MSCI_ID_VPN5_STATE}
	local vpnUpState = "connected"

	local val
	for k, msciId in ipairs(vpnStateIds) do
		val = session.devicetree.get(msciId)
		if vpnUpState == string.lower(val) then
			return 1
		end
	end
	return 0
end

-- Return list of WAN links (dpi, state. type is for debugging)
-- First link in list is primary WAN link.
------------------------------------------------------------------
local function extractWanLinks()
	local retval = {}; -- list of WanState
	local val = session.devicetree.get(MSCI_ID_PRIMARY_WAN_IFACE)
	local waninfo
	local state

	-- firmware version before 4.8.0 does not know MSCI 10438
	if val == nil then
		if string.find(tostring(session.devicetree.get(MSCI_ID_NETWORK_STATE)), "Network Ready") then
			state = CONNECTED
		else
			state = NOT_CONNECTED
		end
		wanInfo = {	dpi = getDpiFromLinkType(ALEOS_WAN_CELL_LINK),
					state = state,
					type = ALEOS_WAN_CELL_LINK}

		table.insert(retval, wanInfo)
		return retval;
	end

	-- From here on the gw should be running 4.8.0 or higher.

	local primaryLink = tonumber(val)
	if primaryLink < 0 or primaryLink > ALEOS_WAN_ETH_LINK then
		session.error(string.format(
			"Response to MSCI %s: %s",
			MSCI_ID_PRIMARY_WAN_IFACE, tostring(val)));
		primaryLink = ALEOS_WAN_CELL_LINK;
	end

	if primaryLink == 0 then
		-- 0 means no primary link
		wanInfo = {	dpi = 0,
					state = NOT_CONNECTED,
					type = 0}
	else
		wanInfo = {	dpi = getDpiFromLinkType(primaryLink),
					state = CONNECTED,
					type = primaryLink}
	end
	
	table.insert(retval, wanInfo) 

	if primaryLink ~= ALEOS_WAN_CELL_LINK then
		-- Add cell link in retval because cell is always WAN.
		state = getWanLinkState(ALEOS_WAN_CELL_LINK)
		wanInfo = {	dpi = getDpiFromLinkType(ALEOS_WAN_CELL_LINK),
					state = state,
					type = ALEOS_WAN_CELL_LINK}
		table.insert(retval, wanInfo)
	end

	if primaryLink ~= ALEOS_WAN_WIFI_LINK and
		isLinkWanMode(ALEOS_WAN_WIFI_LINK) then
		state = getWanLinkState(ALEOS_WAN_WIFI_LINK)
		wanInfo = {	dpi = getDpiFromLinkType(ALEOS_WAN_WIFI_LINK),
					state = state,
					type = ALEOS_WAN_WIFI_LINK}
		table.insert(retval, wanInfo)
		end

	if primaryLink ~= ALEOS_WAN_ETH_LINK and
		isLinkWanMode(ALEOS_WAN_ETH_LINK) then
		state = getWanLinkState(ALEOS_WAN_ETH_LINK)
		wanInfo = {	dpi = getDpiFromLinkType(ALEOS_WAN_ETH_LINK),
					state = state,
					type = ALEOS_WAN_ETH_LINK}
		table.insert(retval, wanInfo)
	end

	return retval;
end

-- Create and return list of WAN DELS events for list of WAN links
-- En extra parameter (efid for event filter id) is added to each
-- to make it easier to filter complementary events against each
-- other, which will be removed prior to sending. DPI makes it unique
-- across interfaces.
------------------------------------------------------------------
local function createWanLinkEvents(links)
	local eventsToAdd = {}

	local linkUpEvents = {EventModel.DELS_WANUP, EventModel.DELS_WANADDRESS}
	local linkDownEvents = {EventModel.DELS_WANDOWN}
	local linkNotPrimaryEvents = {EventModel.DELS_WANNOTACTIVE}
	local primaryLinkUpEvents = {EventModel.DELS_WANACTIVE, EventModel.DELS_WANSTATUS}
	local primaryLinkDownEvents = {EventModel.DELS_WANNOTACTIVE, EventModel.DELS_WANSTATUS}
	-- NB: MSCI will never see the primary link down. AMMER can.

	local isPrimary = true;

	for l, link in ipairs(links) do
		if link.dpi == 0 then
			-- There is no primary link
			isPrimary = false

			local messageid = EventModel.DELS_WANSTATUS
			table.insert(eventsToAdd, {efid=efids[messageid], messageid=messageid, param0=0, param1=0, extra=""})
		else
			if link.state == CONNECTED then
				for k, messageid in ipairs(linkUpEvents) do
					local p1 = 0
					if messageid == EventModel.DELS_WANADDRESS then
						local msciid
						
						if isPrimary then
							msciId = MSCI_ID_IPADDRESS
						else
							msciId = dpiToIpAddrMsciId(link.dpi)
						end

						local ipAddr = session.devicetree.get(msciId)
						p1 = ipToInt(ipAddr)
						if p1 > 0 then
							-- Don't report IP address = 0.0.0.0
							table.insert(eventsToAdd, {efid=efids[messageid] + link.dpi, messageid=messageid, param0=link.dpi, param1=p1, extra=""})
						end
					else
						table.insert(eventsToAdd, {efid=efids[messageid] + link.dpi, messageid=messageid, param0=link.dpi, param1=p1, extra=""})
					end
				end
			else
				-- secondary WAN links can be in down state
				for k, messageid in ipairs(linkDownEvents) do
					table.insert(eventsToAdd, {efid=efids[messageid] + link.dpi, messageid=messageid, param0=link.dpi, param1=0, extra=""})
				end
			end

			-- First item in the list is the primary.
			if isPrimary then
				if link.state == CONNECTED then
					local p0
					local p1
					-- primary WAN link is up
					for k, messageid in ipairs(primaryLinkUpEvents) do
						if messageid == EventModel.DELS_WANACTIVE then
							p0 = link.dpi
							p1 = 0
							table.insert(eventsToAdd, {efid=efids[messageid] + link.dpi, messageid=messageid, param0=p0, param1=p1, extra=""})
						elseif messageid == EventModel.DELS_WANSTATUS then
							p0 = 1
							p1 = 0
							table.insert(eventsToAdd, {efid=efids[messageid], messageid=messageid, param0=p0, param1=p1, extra=""})
						else
						-- whoever added an event in primaryLinkUpEvents[] adjust here.
						end
					end -- for each primary WAN link up event
				else
					-- primary WAN link is down
					for k, messageid in ipairs(primaryLinkDownEvents) do
						if messageid == EventModel.DELS_WANNOTACTIVE then
							p0 = link.dpi
							p1 = 0
							table.insert(eventsToAdd, {efid=efids[messageid] + link.dpi, messageid=messageid, param0=p0, param1=p1, extra=""})
						elseif messageid == EventModel.DELS_WANSTATUS then
							p0 = 0
							p1 = 0
							table.insert(eventsToAdd, {efid=efids[messageid], messageid=messageid, param0=p0, param1=p1, extra=""})
						else
						-- whoever added an event in primaryLinkDownEvents[] adjust here.
						end
					end -- for each primary WAN down event
				end

				-- When finish processing the primary link, clear the flag
				isPrimary = false
			else
				-- Not a primary link - can't be active
				local p0
				local p1
				for k, messageid in ipairs(linkNotPrimaryEvents) do
					if messageid == EventModel.DELS_WANNOTACTIVE then
						p0 = link.dpi
						p1 = 0
						table.insert(eventsToAdd, {efid=efids[messageid] + link.dpi, messageid=messageid, param0=p0, param1=p1, extra=""})
					else
					-- whoever added an event in primaryLinkDownEvents[] adjust here.
					end
				end
			end -- if isPrimary
		end
	end -- for each WAN link

	-- misc link events (Rx/Tx byte counts moved to funcs below)
	local p0 = atLeastOneVpnIsUp()
	local messageid=EventModel.DELS_VPNSTATUS
	table.insert(eventsToAdd, {efid=efids[messageid], messageid=messageid, param0=p0, param1=0, extra=""})
	
	return eventsToAdd
end

-- These are called periodically from main, so that Tx & Rx
-- byte counts can be collected passed thru filter

-- Return event for cellular Tx byte count
local function getCellTxEvent()
	local val = session.devicetree.get(MSCI_ID_CELL_TX_BYTES)
	local numBytes = tonumber(val)
	if numBytes and numBytes > 0 then
		return {messageid=EventModel.DELS_WANTXBYTES, param0=ALEOS_CELLLINK_DPI, param1=numBytes, extra=""}
	else
		return nil
	end
end

-- Return event for cellular Rx byte count
local function getCellRxEvent()
	val = session.devicetree.get(MSCI_ID_CELL_RX_BYTES)
	numBytes = tonumber(val)
	if numBytes and numBytes > 0 then
		return {messageid=EventModel.DELS_WANRXBYTES, param0=ALEOS_CELLLINK_DPI, param1=numBytes, extra=""}
	else
		return nil
	end
end

-- Initialize wanstate module. devicetree must be specified.
-- showlog is optional, and specifies handler for log messages
local function init(devicetree, showlog)
	session.devicetree = devicetree
	if showlog then
		session.error = showlog
	end
end

-- These are the methods that will most likely be used externally
session.init = init
session.extractWanLinks = extractWanLinks
session.createWanLinkEvents = createWanLinkEvents
session.getCellTxEvent = getCellTxEvent
session.getCellRxEvent = getCellRxEvent

-- These are made available for unit testing
session.getPlatformType = getPlatformType
session.ipToInt = ipToInt
session.getDpiFromLinkType = getDpiFromLinkType
session.dpiToIpAddrMsciId = dpiToIpAddrMsciId
session.getWanLinkState = getWanLinkState
session.isLinkWanMode = isLinkWanMode
session.atLeastOneVpnIsUp = atLeastOneVpnIsUp
session.EventModel = EventModel
session.efids = efids

-- Default error just displays message
session.error = function(message)
	print(message)
end

-- The list of msciids to be watched for change state
-- to trigger generation of WAN stat events
session.msciids = {
	MSCI_ID_NETWORK_STATE,
	MSCI_ID_PRIMARY_WAN_IFACE,
	MSCI_ID_WIFI_MODE,
	MSCI_ID_ETH_PORT4_MODE,
	MSCI_ID_ETH_PORT1_MODE,
	MSCI_ID_CELL_WAN_STATE,
	MSCI_ID_ETH_WAN_STATE,
	MSCI_ID_WIFI_WAN_STATE,
	MSCI_ID_CELL_IP,
	MSCI_ID_WIFI_IP,
	MSCI_ID_ETH_IP,
	MSCI_ID_VPN1_STATE,
	MSCI_ID_VPN2_STATE,
	MSCI_ID_VPN3_STATE,
	MSCI_ID_VPN4_STATE,
	MSCI_ID_VPN5_STATE
}

-- Not actually used, but handy for debugging
session.EventModel = EventModel

return session

