--[[
=head1 NAME

jive.net.Networking 

=head1 DESCRIPTION

This class implements methods for administering network interfaces.

Much of the methods in this class need to be done through Task. These methods are by convention prefixed with t_

=head1 SYNOPSIS

pull in networking object to manipulate wireless interface

	local networking = Networking(jnt, 'wireless')

=head1 FUNCTIONS

=cut
--]]

local assert, ipairs, pairs, pcall, tonumber, tostring = assert, ipairs, pairs, pcall, tonumber, tostring

local oo          = require("loop.simple")

local io          = require("io")
local os          = require("os")
local string      = require("string")
local table       = require("jive.utils.table")
local ltn12       = require("ltn12")

local debug       = require("jive.utils.debug")
local log         = require("jive.utils.log").logger("net.socket")

local Framework   = require("jive.ui.Framework")
local Socket      = require("jive.net.Socket")
local Task        = require("jive.ui.Task")
local wireless    = require("jiveWireless")


module("jive.net.Networking")
oo.class(_M, Socket)


local SSID_TIMEOUT = 20000

-- wpa scan results signal level -> quality
-- FIXME tune with production boards
local WIRELESS_LEVEL = {
	0,
	175,
	180,
	190,
}

-- iwpriv snr -> quality
-- FIXME tune with production boards
local WIRELESS_SNR = {
	0,
	10,
	18,
	25,
}

-- FIXME check this region mapping is correct for Marvell and Atheros
local REGION_CODE_MAPPING = {
	-- name, marvell code, atheros code
	[ "US" ] = { 0x10, 4  }, -- ch 1-11
	[ "CA" ] = { 0x20, 6  }, -- ch 1-11
	[ "EU" ] = { 0x30, 14 }, -- ch 1-13
	[ "FR" ] = { 0x30, 13 }, -- ch 1-13
	[ "CH" ] = { 0x30, 23 }, -- ch 1-13
	[ "TW" ] = { 0x30, 21 }, -- ch 1-13
	[ "AU" ] = { 0x10, 7  }, -- ch 1-11
	[ "JP" ] = { 0x41, 16 }, -- ch 1-13
}


-- global wireless network scan results
local _scanResults = {}

-- singleton wireless instance per interface
local _instance = {}

--[[

=head2 jive.net.Networking(jnt, ifType, name)

Constructs an object for administration of a network interface

=cut
--]]

function __init(self, jnt, ifType, name)

	local mappedInterface = whichInterface(ifType)

	if _instance[mappedInterface] then
		return _instance[mappedInterface]
	end

	local obj = oo.rawnew(self, Socket(jnt, name))

	obj.ifType        = ifType
	obj.interface     = mappedInterface
	obj.responseQueue = {}

	obj:open()

	_instance[mappedInterface] = obj
	return obj
end

--[[

=head2 jive.net.Networking:whichInterface(ifType)

Returns the mapped interface for a given type of networking

=cut
--]]
function whichInterface(self, ifType)

	local interface

	if ifType == 'wireless' then
		-- get available interface, likely from `wpa_cli status` or `wpa_cli interface`
		interface = ''
	else
		-- get available wired interface from ??
		interface = ''
	end
	return interface
end

--[[

=head2 jive.net.Networking:getRegionNames()

returns the available wireless region names

=cut
--]]

function getRegionNames(self)
	return pairs(REGION_CODE_MAPPING)
end

--[[

=head2 jive.net.Networking:getRegion()

returns the current region

=cut
--]]


function getRegion(self)
	-- check config file
	local fh = io.open("/etc/network/config")
	if fh then
		local file = fh:read("*a")
		fh:close()

		local region = string.match(file, "REGION=([^%s]+)")
		if  region then
			return region
		end
	end
	
	-- check marvell region
	local fh = assert(io.popen("/sbin/iwpriv " .. self.interface .. " getregioncode"))
	local line = fh:read("*a")
	fh:close()

	local code = tonumber(string.match(line, "getregioncode:(%d+)"))

	for name,mapping in pairs(REGION_CODE_MAPPING) do
		log:info("code=", code, " mapping[1]=", mapping[1])
		if mapping[1] == code then
			log:info("returning=", name)
			return name
		end
	end
	return nil
end

--[[

=head2 jive.net.Networking:setRegion(region)

sets the current region

=cut
--]]

function setRegion(self, region)
	local mapping = REGION_CODE_MAPPING[region]
	if not mapping then
		return
	end

	-- save config file
	local fh = assert(io.open("/etc/network/config", "w"))
	fh:write("REGION=" .. region .. "\n")
	fh:write("REGIONCODE=" .. mapping[1] .. "\n")
	fh:close()

	-- set new region
	local cmd = "/sbin/iwpriv " .. self.interface .. " setregioncode " .. mapping[1]
	log:info("setRegion: ", cmd)
	os.execute(cmd)
end


--[[

=head2 jive.net.Networking:getMarvellRegionCode()

returns the region code for Marvell on Jive

=cut
--]]

function getMarvellRegionCode(self)
	local mapping = REGION_CODE_MAPPING[region or self:getRegion()]
	return mapping and mapping[1] or 0
end

--[[

=head2 jive.net.Networking:getAtherosRegionCode()

returns the region code for Atheros on Squeezebox

=cut
--]]


function getAtherosRegionCode(self)
	local mapping = REGION_CODE_MAPPING[region or self:getRegion()]
	return mapping and mapping[2] or 0
end

--[[

=head2 jive.net.Networking:scan(callback)

start a wireless network scan in a new task

=cut
--]]

function scan(self, callback)
	Task("networkScan", self,
	     function()
		     t_scan(self, callback)
	     end):addTask()
end

--[[

=head2 jive.net.Networking:scanResults()

returns wireless scan results, or nil if a network scan has not been performed.

=cut
--]]

function scanResults(self)
	return _scanResults
end

--[[

=head2 jive.net.Networking:t_scan(callback)

network scanning. this can take a little time so we do this in 
the network thread so the ui is not bocked.

=cut
--]]


function t_scan(self, callback)
	assert(Task:running(), "Networking:scan must be called in a Task")

	local status, err = self:request("SCAN")
	if err then
		return
	end

	local status, err = self:request("STATUS")
	if err then
		return
	end

	local associated = string.match(status, "\nssid=([^\n]+)")

	local scanResults, err = self:request("SCAN_RESULTS")
	if err then
		return
	end

	_scanResults = _scanResults or {}

	local now = Framework:getTicks()

	for bssid, level, flags, ssid in string.gmatch(scanResults, "([%x:]+)\t%d+\t(%d+)\t(%S*)\t([^\n]+)\n") do

		local quality = 1
		level = tonumber(level)
		for i, l in ipairs(WIRELESS_LEVEL) do
			if level < l then
				break
			end
			quality = i
		end

		_scanResults[ssid] = {
			bssid = string.lower(bssid),
			flags = flags,
			level = level,
			quality = quality,
			associated = (ssid == associated),
			lastScan = now
		}
	end

	for ssid, entry in pairs(_scanResults) do
		if now - entry.lastScan > SSID_TIMEOUT then
			_scanResults[ssid] = nil
		end
	end

	-- Bug #5227 if we are associated use the same quality indicator
	-- as the icon bar
	if associated and _scanResults[associated] then
		_scanResults[associated].quality = self:getLinkQuality()
	end

	if callback then
		callback(_scanResults)
	end

	self.scanTask = nil
end

--[[

=head2 jive.net.Networking:t_wpaStatus()

parse and return wpa status

=cut
--]]

function t_wpaStatus(self)
	assert(Task:running(), "Networking:wpaStatus must be called in a Task")

	local statusStr = self:request("STATUS")

	local status = {}
	for k,v in string.gmatch(statusStr, "([^=]+)=([^\n]+)\n") do
		status[k] = v
	end

	return status
end

--[[

=head2 jive.net.Networking:t_addNetwork(ssid, option)

adds a network to the list of discovered networks

=cut
--]]


function t_addNetwork(self, ssid, option)
	assert(Task:running(), "Networking:addNetwork must be called in a Task")

	local request, response

	-- make sure this ssid is not in any configuration
	self:t_removeNetwork(ssid)

	log:info("Connect to ", ssid)
	local flags = (_scanResults[ssid] and _scanResults[ssid].flags) or ""

	-- Set to use dhcp by default
	self:_editNetworkInterfaces(ssid, "dhcp", "script /etc/network/udhcpc_action")

	response = self:request("ADD_NETWORK")
	local id = string.match(response, "%d+")
	assert(id, "wpa_cli failed: to add network")

	request = 'SET_NETWORK ' .. id .. ' ssid "' .. ssid .. '"'
	assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

	if string.find(flags, "IBSS") or option.ibss then
		request = 'SET_NETWORK ' .. id .. ' mode 1 '
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
	end

	if option.encryption == "wpa" then
		log:info("encryption WPA")

		request = 'SET_NETWORK ' .. id .. ' key_mgmt WPA-PSK'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

		request = 'SET_NETWORK ' .. id .. ' proto WPA'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

		-- Setting the PSK can timeout
		pcall(function()
			      request = 'SET_NETWORK ' .. id .. ' psk "' .. option.psk .. '"'
			      assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
		      end)
	elseif option.encryption == "wpa2" then
		log:info("encryption WPA2")

		request = 'SET_NETWORK ' .. id .. ' key_mgmt WPA-PSK'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

		request = 'SET_NETWORK ' .. id .. ' proto WPA2'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

		-- Setting the PSK can timeout
		pcall(function()
			      request = 'SET_NETWORK ' .. id .. ' psk "' .. option.psk .. '"'
			      assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
		      end)
	else
		request = 'SET_NETWORK ' .. id .. ' key_mgmt NONE'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
	end

	if option.encryption == "wep40" or option.encryption == "wep104" then
		log:info("encryption WEP")

		request = 'SET_NETWORK ' .. id .. ' wep_key0 ' .. option.key
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
	end

	-- If we have not scanned the ssid then enable scanning with ssid 
	-- specific probe requests. This allows us to find APs with hidden
	-- SSIDS
	if not _scanResults[ssid] then
		request = 'SET_NETWORK ' .. id .. ' scan_ssid 1'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
	end

	-- Disconnect from existing network
	self:t_disconnectNetwork()

	-- Use select network to disable all other networks
	request = 'SELECT_NETWORK ' .. id
	assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

	-- Allow association
	request = 'REASSOCIATE'
	assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

	-- Save config, it will be removed later if it fails
	request = 'SAVE_CONFIG'
	assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

	return id
end

--[[

=head2 jive.net.Networking:t_removeNetwork(ssid)

forgets a previously discovered network 

=cut
--]]

function t_removeNetwork(self, ssid)
	assert(Task:running(), "Networking:removeNetwork must be called in a Task")

	local networkResults = self:request("LIST_NETWORKS")

	local id = false
	for nid, nssid in string.gmatch(networkResults, "([%d]+)\t([^\t]*).-\n") do
		if nssid == ssid then
			id = nid
			break
		end
	end

	-- Remove ssid from wpa supplicant
	if id then
		local request = 'REMOVE_NETWORK ' .. id
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

		request = 'SAVE_CONFIG'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
	end

	-- Remove dhcp/static ip configuration for network
	self:_editNetworkInterfaces(ssid)
end

--[[

=head2 jive.net.Networking:t_disconnectNetwork()

brings down a network interface. If wireless, then force the wireless disconnect as well

=cut
--]]

function t_disconnectNetwork(self)

	assert(Task:running(), "Networking:disconnectNetwork must be called in a Task")

	-- Force disconnect from existing network
	if self.ifType == 'wireless' then
		local request = 'DISCONNECT'
		assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
	end

	-- Force the interface down
	local ifDown = "/sbin/ifdown -f " .. self.interface
	os.execute(ifDown)
end

--[[

=head2 jive.net.Networking:t_selectNetwork(ssid)

selects a network to connect to. wireless only

=cut
--]]


function t_selectNetwork(self, ssid)

	if self.ifType ~= 'wireless' then
		return
	end

	assert(Task:running(), "Networking:selectNetwork must be called in a Task")

	local networkResults = self:request("LIST_NETWORKS")
	log:info("list results ", networkResults)

	local id = false
	for nid, nssid in string.gmatch(networkResults, "([%d]+)\t([^\t]*).-\n") do
		log:info("id=", nid, " ssid=", nssid)
		if nssid == ssid then
			id = nid
			break
		end
	end

	-- Select network
	if not id then
		log:warn("can't find network ", ssid)
		return
	end

	-- Disconnect from existing network
	self:t_disconnectNetwork()

	local request = 'SELECT_NETWORK ' .. id
	assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

	-- Allow association
	request = 'REASSOCIATE'
	assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)

	-- Save configuration
	request = 'SAVE_CONFIG'
	assert(self:request(request) == "OK\n", "wpa_cli failed:" .. request)
end


--[[

=head2 jive.net.Networking:t_setStaticIp(ssid, ipAddress, ipSubnet, ipGateway, ipDNS)

apply IP address and associated configuration parameters to a network interface

=cut
--]]


function t_setStaticIP(self, ssid, ipAddress, ipSubnet, ipGateway, ipDNS)
	-- Reset the network
	local killCommand   = "kill -TERM `cat /var/run/udhcpc." .. self.interface .. "pid`"
	local configCommand = "/sbin/ifconfig " .. self.interface .. " 0.0.0.0"

	os.execute(killCommand)
	os.execute(configCommand)

	-- Set static ip configuration for network
	self:_editNetworkInterfaces(ssid, "static",
				    "address " .. ipAddress,
				    "netmask " .. ipSubnet,
				    "gateway " .. ipGateway,
				    "dns " .. ipDNS,
				    "up echo 'nameserver " .. ipDNS .. "' > /etc/resolv.conf"
			    )

	-- Bring up the network
	local ifUp = "/sbin/ifup " .. interface
	local status = os.execute(ifUp)
	log:info("ifup status=", status)
end


function _editNetworkInterfaces(self, ssid, method, ...)
	-- the interfaces file uses " \t" as word breaks so munge the ssid
	-- FIXME ssid's with \n are not supported
	assert(ssid, debug.traceback())
	ssid = string.gsub(ssid, "[ \t]", "_")
	log:info("munged ssid=", ssid)

	local fi = assert(io.open("/etc/network/interfaces", "r+"))
	local fo = assert(io.open("/etc/network/interfaces.tmp", "w"))

	local network = ""
	for line in fi:lines() do
		if string.match(line, "^mapping%s") or string.match(line, "^auto%s") then
			network = ""
		elseif string.match(line, "^iface%s") then
			network = string.match(line, "^iface%s([^%s]+)%s")
		end

		if network ~= ssid then
			fo:write(line .. "\n")
		end
	end

	if method then
		fo:write("iface " .. ssid .. " inet " .. method .. "\n")
		for _,v in ipairs{...} do
			fo:write("\t" .. v .. "\n")
		end
	end

	fi:close()
	fo:close()

	os.execute("/bin/mv /etc/network/interfaces.tmp /etc/network/interfaces")
end

--[[

=head2 jive.net.Networking:getLinkQuality()

returns "quality" of wireless interface
used for dividing SNR values into categorical levels of signal quality

	quality of 1 indicates SNR of 0
	quality of 2 indicates SNR <= 10
	quality of 3 indicates SNR <= 18
	quality of 4 indicates SNR <= 25

=cut
--]]

function getLinkQuality(self)

	local snr = self:getSNR()

	if snr == nil or snr == 0 then
		return nil
	end

	local quality = 1
	for i, l in ipairs(WIRELESS_SNR) do
		if snr <= l then
			break
		end
		quality = i
	end

	return quality
end

--[[

=head2 jive.net.Networking:getSNR()

returns signal to noise ratio of wireless link

=cut
--]]


function getSNR(self)
	local f = io.popen("/sbin/iwpriv " .. self.interface .. " getSNR 1")
	if f == nil then
		return 0
	end

	local t = f:read("*all")
	f:close()

	return tonumber(string.match(t, ":(%d+)"))
end

--[[

=head2 jive.net.Networking:getRSSI()

returns Received Signal Strength Indication (signal power) of a wireless interface
=cut
--]]

function getRSSI(self)
	local f = io.popen("/sbin/iwpriv " .. self.interface .. " getRSSI 1")
	if f == nil then
		return 0
	end

	local t = f:read("*all")
	f:close()

	return tonumber(string.match(t, ":(%-?%d+)"))
end

--[[

=head2 jive.net.Networking:getNF()

returns NF (?) of a wireless interface

=cut
--]]


function getNF(self)
	local f = io.popen("/sbin/iwpriv " .. self.interface .. " getNF 1")
	if f == nil then
		return 0
	end

	local t = f:read("*all")
	f:close()

	return tonumber(string.match(t, ":(%-?%d+)"))
end

--[[

=head2 jive.net.Networking:getTxBitRate()

returns bitrate of a wireless interface

=cut
--]]

function getTxBitRate(self)
	local f = io.popen("/sbin/iwconfig " .. self.interface)
	if f == nil then
		return "0"
	end

	local t = f:read("*all")
	f:close()

	return string.match(t, "Bit Rate:(%d+%s[^%s]+)")
end

--[[

=head2 jive.net.Networking:powerSave(enable)

sets wireless power save state

=cut
--]]


function powerSave(self, enable)
	if self._powerSaveState == enable then
		return
	end

	if not self.t_sock then
		return
	end

	self._powerSaveState = enable
	if enable then
		log:info("iwconfig power on")
		self.t_sock:setPower(true)
	else
		log:info("iwconfig power off")
		self.t_sock:setPower(false)
	end
end

--[[

=head2 jive.net.Networking:open()

opens a socket to pick up network events

=cut
--]]



function open(self)
	if self.t_sock then
		log:error("Socket already open")
		return
	end

	local err

	if self.ifType == 'wireless' then
		self.t_sock, err = wireless:open()
		if err then
			log:warn(err)
	
			self:close()
			return false
		end

		local source = function()
				       return self.t_sock:receive()
			       end
	
		local sink = function(chunk, err)
				     if chunk and string.sub(chunk, 1, 1) == "<" then
					     -- wpa event message
					     if self.eventSink  then
						     Task("wirelessEvent", nil,
							  function()
								  self.eventSink(chunk, err)
							  end):addTask()
					     end
				     else
					     -- request response
					     local task = table.remove(self.responseQueue, 1)
					     if task then
						     task:addTask(chunk, err)
					     end
				     end
			     end
	
		self:t_addRead(function()
				       -- XXXX handle timeout
				       local status, err = ltn12.pump.step(source, sink)
				       if err then
					       log:warn(err)
					       self:close()
				       end
			       end,
	       0) -- no timeout
	-- not wireless, do something else...
	else
		-- FIXME
	end

	return true
end

--[[

=head2 jive.net.Networking:close()

closes existing socket

=cut
--]]


function close(self)
	-- cancel queued requests
	for i, task in ipairs(self.responseQueue) do
		task:addTask(nil, "closed")
	end
	self.responseQueue = {}

	Socket.close(self)
end

--[[

=head2 jive.net.Networking:request()

pushes request to open socket

=cut
--]]


function request(self, ...)
	local task = Task:running()
	assert(task, "Networking:request must be called in a Task")

	log:info("REQUEST: ", ...)

	-- open the socket if it is closed
	if not self.t_sock and not self:open() then
		-- XXXX callers expect a string
		return "", "closed"
	end

	local status, err = self.t_sock:request(...)

	if err then
		log:warn(err)
		self:close()

		-- XXXX callers expect a string
		return "", err
	end

	-- yield task
	table.insert(self.responseQueue, task)
	local _, reply, err = Task:yield(false)

	if not reply or err then
		log:warn(err)
		self:close()

		-- XXXX callers expect a string
		return "", err
	end

	log:info("REPLY:", reply)
	return reply
end


function attach(self, sink)
	-- XXXX allow multiple sinks
	self.eventSink = sink
end


function detach(self)
	self.eventSink = nil
end


--[[

=head1 LICENSE

Copyright 2009 Logitech. All Rights Reserved.

This file is subject to the Logitech Public Source License Version 1.0. Please see the LICENCE file for details.

=cut
--]]