require "UtilFunc"

function WHOIMM(portParam,subsystemParam)

	local self = { port=portParam, savedLines={}, subsystemPrefix=subsystemParam, timerIndex=0 }

	local o =
	{
		-- Time to wait after adjusting the baud rate
		adjustBaudRateDelay = 100,

		-- This is an array of valid baud rates for the WHOI MicroModem
		allModemBaudRates = { 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400 },

		-- baud rate that the script wants to run at
		desiredBaudRate = 115200,

		-- maximum number of retries to use by default
		maxRetriesConfig = 2,

		-- my unit number
		myUnitNumber = 0,

		-- partner unit number
		partnerUnitNumber = 10,

		-- timeout (ms) to use for local commands
		shortTimeout = 1000
	}

	-- this is a coroutine class that listens for completed nmea sentences
	-- and sends them out as nmeaRx events
	local function nmeaReceiver(whoimmParam, evtqParam, publishFuncParam)

		local self = { evtq=evtqParam, outputMsgName="nmeaRx", publishFunc=publishFuncParam, rtn=nil, whoimm=whoimmParam }
		local o = { }

		local function eventHandler(evt)
			SMC:Log("debug", "nmeaReceiver:eventHandler got "..tostring(evt))
			coroutine.resume(self.rtn, evt)
		end

		local function localrun()
			SMC:Log("debug", "nmeaReceiver starting")
			local cbTable = { serialData=self.eventHandler }
			local cbidx = self.evtq.addDispatcher(cbTable)

			while true do
				local wmsg = self.whoimm.AwaitAnyMessage(nil)
				local evt = Event(self.outputMsgName, {msg=wmsg})
				self.publishFunc(evt)
			end
		end

		local function run()
			self.rtn = coroutine.create(function() localrun() end)
			coroutine.resume(self.rtn)
		end

		-----------------
		-- All Methods --
		-----------------

		self.eventHandler = eventHandler
		self.localrun = localrun
		self.run = run

		--------------------
		-- Public Methods --
		--------------------

		o.eventHandler = eventHandler
		o.run = run
		return o

	end

	local function mmMessage(msgModeParam, msgParam, fromParam, toParam, pktTypeParam, retriesParam, o)

		if (type(msgModeParam) ~= "string") or ((msgModeParam ~= "send") and (msgModeParam ~= "receive")) then
			error("mmMessage: mode parameter is not valid")
		elseif type(msgParam) ~= "string" then
			error("mmMessage: message parameter is not valid")
		elseif (type(pktTypeParam) ~= "number") or ((pktTypeParam ~= 0) and (pktTypeParam ~= 2) and (pktTypeParam ~= 3) and (pktTypeParam ~= 5)) then
			error("mmMessage: packet type is invalid or out of range")
		elseif (type(retriesParam) ~= "number") or (retriesParam < 0) then
			error("mmMessage: retries is invalid or out of range")
		end

		local self = {
			acks = { },
			frameInfo = { },
			from = fromParam,
			msg = msgParam,
			msgMode = msgModeParam,
			numPackets = 0,
			packetSize = 0,
			pieces = { },
			pktType = pktTypeParam,
			retries = retriesParam,
			sendDirection = (msgModeParam == "send"),
			to = toParam,
			tryNum = 1
		}

		o = o or { }

		self.frameSizes[0] = { maxFrames=1, maxFrameSize=32,  maxPacketSize=32,   bps=80   }
		--self.frameSizes[1] = { maxFrames=3, maxFrameSize=64,  maxPacketSize=192,  bps=250  }
		self.frameSizes[2] = { maxFrames=3, maxFrameSize=64,  maxPacketSize=192,  bps=500  }
		self.frameSizes[3] = { maxFrames=2, maxFrameSize=256, maxPacketSize=512,  bps=1200 }
		--self.frameSizes[4] = { maxFrames=2, maxFrameSize=256, maxPacketSize=512,  bps=1300 }
		self.frameSizes[5] = { maxFrames=8, maxFrameSize=256, maxPacketSize=2048, bps=5300 }
		--self.frameSizes[6] = { maxFrames=6, maxFrameSize=32,  maxPacketSize=192  }

		local t
		
		self.frameInfo = self.frameSizes[self.pktType]

		local function GetFrameCount()

			local frameSize = GetFrameSize()
			local result

			if (frameSize==0) or not msg then
				result=0
			else
				result=#msg/frameSize
			end

			return result

		end

		local function GetFrameData(frameIdx)

			local result = ""
			local fs = GetFrameSize()
			local fc = GetFrameCount()

			if msg and (frameIdx >= 1) and (frameidx <= fc) then
				local start = (frameIdx-1)*fs
				result = msg:sub(start,start+fs-1)
			end

			return result

		end

		local function GetFrameSize()

			local match = self.frameSizes[self.pktType]
			local result

			if match then result=match.maxFrameSize end
			if not result then result=0 end

			return result

		end

		local function GetTriesLeft()
			local result=0
			if (tryNum-1) >= retries then result = retries-tryNum+1 end
			return result
		end

		local function NextTry()
			self.retries = self.retries+1
			return self.GetTriesLeft() > 0
		end

	end

	-- gets the frame content from the string for the given frame number
	local function GetFrame(str,frameNum)

		local result
	
		if (frameNum >= 1) then
		
			local startIdx = frameNum*256
			local endIdx = startIdx+255
		
			if endIdx > str:len() then
				endIdx = str:len()
			end
		
			result = str:sub(startIdx,endIdx)
		
		end
	
		return result

	end

	-- Reconfigures the baud rate of port to match how the attached modem
	-- is communicating; returns true if successful or nil if not
	local function AdjustToCurrentBaudRate()

		local result

		SMC:Log("debug", "AdjustToCurrentBaudRate() starting")

		local v

		for _,v in ipairs(o.allModemBaudRates) do
			if TryCommsAt(v) then
				result = true
				break
			end
		end

		SMC:Log("debug", "AdjustToCurrentBaudRate(): "..tostring(not not result).." ("..tostring(v)..")")
		return result
	end

	-- Returns true if the baud rate configuration could be read over the port
	-- at the given port baud rate; false if failed
	function TryCommsAt(testBaudRate)

		SMC:Log("debug", "TryCommsAt("..tostring(testBaudRate)..")")

		self.port:SetBaudRate(testBaudRate)
		self.Wait(o.adjustBaudRateDelay)
		self.Transmit("\r\n")
		self.Wait(o.adjustBaudRateDelay)

		local response = self.SendModemCommand("CCCFQ,BR1", "CACFG", o.shortTimeout, 0)
		local result = not not response

		SMC:Log("debug", "TryCommsAt: "..tostring(result))
		return result

	end

	-- gets the count of frames available in the given string
	local function GetFrameCount(str)
		local result = math.ceil(str:len()/256)
		SMC:Log("debug", "GetFrameCount for "..tostring(str:len()).." bytes = "..tostring(result))
		return result
	end

	-- Converts the numeric baud rate pass in to the parameter
	-- to be used when setting the baud rate or nil if no match
	local function BaudRateToNum(baudRate)

		local idx, result

		for idx=1,#o.allModemBaudRates do
			if o.allModemBaudRates[idx] == baudRate then
				result = idx-1
				break
			end
		end

		return result

	end

	-- Computes the check value for a block (str) and returns it as a 2-digit ASCII
	-- string; trims off any leading $ or trailing *XX if found before computing the
	-- result
	local function ComputeCheckForBlock(str)

		local startIdx = 1
		local endIdx = str:len()

		if str:sub(startIdx,startIdx) == "$" then
			startIdx = 2
		end

		if str:find("[*][%x][%x]", endIdx-2) then
			endIdx = endIdx-3
		end

		local computed = 0
		local idx

		for idx = startIdx,endIdx do computed = SMC:BXor(computed,str:byte(idx)) end

		local result = string.format("%.2X", computed)
		return result

	end

	-- Returns the response parameter value or nil if failed
	local function GetConfig(configName)

		SMC:Log("debug", "GetConfig("..configName..")")
		local result
		local response = self.SendModemCommand("CCCFQ,"..tostring(configName), "CACFG", o.shortTimeout)

		if response and (#response == 3) then
			result = response[3]
		end

		SMC:Log("debug", "GetConfig: "..UtilFunc.ShowTable(result))
		return result
	end

	-- Returns non-nil if succesful setting the parameter and value or nil if failed
	local function SetConfig(configName, configValue)

		local response = self.SendModemCommand("CCCFG,"..tostring(configName)..","..tostring(configValue), "CACFG", o.shortTimeout)
		return response

	end

	-- Gets the text to send to the modem
	local function GetModemSendText(commandText)
		local result = "$"..commandText.."*"..self.ComputeCheckForBlock(commandText).."\r\n"
		return result
	end

	-- Sends the given command with the number of retries specified using
	-- the given timeout (ms); if no retries specified, uses the default value.
	-- Returns the parsed sentence received in response or nil if no
	-- response.
	local function SendModemCommand(commandText, expectedResponseTag, timeoutValue, numRetries)

		SMC:Log("debug", "SendModemCommand commandText="..commandText.." expectedResponseTag="..expectedResponseTag.." timeoutValue="..tostring(timeoutValue).." ms numRetries="..tostring(numRetries))

		if not numRetries then
			numRetries = o.maxRetriesConfig
		end

		local sendText = self.GetModemSendText(commandText)
		local response

		repeat
			self.Transmit(sendText)
			response = self.AwaitResponseMessage(expectedResponseTag, timeoutValue)
			if numRetries > 0 then numRetries = numRetries-1 end
		until (numRetries < 1) or response

		SMC:Log("debug", "SendModemCommand: "..UtilFunc.ShowTable(response))
		return response

	end

	-- Transmits the given data to the given port (supplies a comment in the
	-- log)
	local function Transmit(data)

		SMC:Log("debug", "Transmitting "..UtilFunc.TranslateString(data))
		self.port:Transmit(data)

	end

	-- Returns an iterator for string (str) that yields NMEA sentences matching the form
	-- $..text..*XX\r\n (optional \r)
	local function NMEALinesIter(str)

		local i = str:gmatch("[$].-[*][%x][%x]\r?\n")
		return i

	end

	-- Parses a data block into a list of parsed data block (returns nil if
	-- nothing was parsed)
	local function ParseIncomingData(data)

	SMC:Log("debug", "ParseIncomingData("..UtilFunc.TranslateString(data)..")")
		local result

		if data then

			local fullLine
			local numLines = 0

			for fullLine in self.NMEALinesIter(data) do

				numLines = numLines+1

				local line = fullLine:sub(1,fullLine:len()-2)
				local llen = line:len()
				local body = line:sub(2, llen-3)
				local checkData = line:sub(llen-1)
				local checkComputed = self.ComputeCheckForBlock(body)

				if checkComputed ~= checkData:upper() then
					SMC:Log("warning", "Sentence '"..line.."' computed check value "..checkComputed.." does not match "..checkData:upper())
				else
					local onetable = UtilFunc.SplitString(body..",", "(.-)[,]")

					onetable.raw = line
					SMC:Log("debug", "Valid sentence '"..line.."' with "..tostring(#onetable).." tokens")
					if result then table.insert(self.savedLines, onetable) else result = onetable end
				end

			end

			if numLines == 0 then
				SMC:Log("debug", "No NMEA lines found in data block '"..UtilFunc.TranslateString(data).."'")
			end
		
		end
	
	SMC:Log("debug", "ParseIncomingData: "..UtilFunc.ShowTable(result))
		return result
	
	end

	-- creates a new local timer
	local function NewLocalTimer(timeoutValue)

		local result
		local timerName

		if timeoutValue then

			timeoutValue = tonumber(timeoutValue)

			if timeoutValue >= 0 then
				timerName = self.subsystemPrefix.."-await#"..tostring(self.timerIndex)
				result = SMC:NewTimer(timerName, timeoutValue)
				self.timerIndex = self.timerIndex+1
			end
		end

		return result

	end

	-- waits for any message to be received and parses it, returns the
	-- result; the timeoutValue is in milliseconds; returns nil on 
	-- timeout
	local function AwaitAnyMessage(timeoutValue)

		local result

		SMC:Log("debug", "AwaitAnyMessage "..tostring(timeoutValue).." ms")

		if #self.savedLines ~= 0 then

			result = self.savedLines[1]
			table.remove(self.savedLines, 1)
			SMC:Log("debug", "AwaitAnyMessage: pulled '"..result.raw.."' with "..tostring(#result).." tokens out of the saved items queue")

		elseif not timeoutValue or (timeoutValue > 0) then

			local timeout = false
			local tmr = self.NewLocalTimer(timeoutValue)
			local data

			repeat
				local evt = coroutine.yield()

				if (evt.getEventName() == "serialData") and evt.serialData and (#evt.serialData ~= 0) then
					data = evt.serialData
				elseif timeoutValue and (evt.getEventName() == "timerExpired") and (evt.timerName == tmr:GetID()) then
					timeout = true
				end

			until data or timeout

			tmr = nil
			collectgarbage("collect")

			if data then
				result = self.ParseIncomingData(data)
			else
				SMC:Log("warning", "AwaitAnyMessage: timeout")
			end

		else
			SMC:Log("error", "ERROR: for AwaitAnyMessage("..tostring(timeoutValue).."), timeout parameter is invalid")
		end

		if result then
			SMC:Log("debug", "AwaitAnyMessage: "..UtilFunc.ShowTable(result))
		else
			SMC:Log("debug", "AwaitAnyMessage: "..tostring(result))
		end

		return result

	end

	-- Returns the next parsed message "nmeaRx", waits for up to the given
	-- number of milliseconds
	local function AwaitIncomingIPC(timeoutValue)

		SMC:Log("debug", "AwaitIncomingIPC "..tostring(timeoutValue).." ms")
		local result
		
		if not timeoutValue or (timeoutValue > 0) then

			local tmr = self.NewLocalTimer(timeoutValue)
			local timeout = false
			local data

			repeat
				local evt = coroutine.yield()
				
				if evt.getEventName() == "nmeaRx" then
					data = evt.msg
				elseif timeoutValue and (evt.getEventName() == "timerExpired") and (evt.timerName == tmr:GetID()) then
					timeout = true
				end
			until data or timeout

			tmr = nil
			collectgarbage("collect")

			if data then
				result = data
			else
				SMC:Log("warning", "AwaitIncomingIPC: timeout")
			end

		else
			SMC:Log("error", "ERROR: for AwaitIncomingIPC("..tostring(timeoutValue).."), timeout parameter is invalid")
		end
		
		return result

	end

	-- Awaits a response message with an NMEA sentce type (expectedResponseTag)
	-- for up to a maximum timeout (timeoutValue, milliseconds)
	local function AwaitResponseMessage(expectedResponseTag, timeoutValue, waitLocal)

		local wl
		if waitLocal then wl = "local" else wl = "ipc" end
		SMC:Log("debug", "AwaitResponseMessage '"..expectedResponseTag.."' "..tostring(timeoutValue).." ms "..wl)

		local startTime = SMC:SystemUptime()
		local elapsed = 0
		local remaining  = timeoutValue
		local result = nil

		SMC:Log("debug", "Starting wait for '"..expectedResponseTag.."' ("..tostring(timeoutValue).." ms)")

		repeat
	
			if waitLocal then
				result = self.AwaitAnyMessage(remaining)
			else
				result = self.AwaitIncomingIPC(remaining)
			end

			if result then

				if result[1] ~= expectedResponseTag then
					SMC:Log("debug", "Ignoring message "..result[1].." since "..expectedResponseTag.." was expected")
					result = nil
				else
					SMC:Log("debug", "Found expected response ("..expectedResponseTag.." with "..tostring(#result).." tokens)")
				end
			end

			elapsed = math.floor(1000*(SMC:SystemUptime()-startTime))
			if timeoutValue > 1 then remaining = timeoutValue-elapsed else remaining = 0 end

			SMC:Log("debug", "result="..tostring(result))
			SMC:Log("debug", "remaining="..tostring(remaining))

		until result or (remaining <= 0)

		SMC:Log("debug", "AwaitResponseMessage: "..tostring(not not result))
		return result

	end

	-- Delays for the given number of milliseconds
	local function Wait(timeoutValue)

		SMC:Log("debug", "Wait "..tostring(timeoutValue).." ms")

		if timeoutValue and (tonumber(timeoutValue) > 0) then

			local tmr = self.NewLocalTimer(tonumber(timeoutValue))
			local timeout = false

			repeat
				local evt = coroutine.yield()

				if timeoutValue and (evt.getEventName() == "timerExpired") and (evt.timerName == tmr:GetID()) then
					timeout = true
				else
					SMC:Log("warning", "Discarding message "..evt.getEventName().." in Wait()")
				end
			until data or timeout

			tmr = nil
			collectgarbage("collect")

		else
			SMC:Log("error", "ERROR: for Wait("..tostring(timeout).."), timeout parameter is invalid")
		end

		SMC:Log("debug", "Wait: complete")

	end

	-----------------
	-- All Methods --
	-----------------

	self.AdjustToCurrentBaudRate = AdjustToCurrentBaudRate
	self.AwaitAnyMessage = AwaitAnyMessage
	self.AwaitIncomingIPC = AwaitIncomingIPC
	self.AwaitResponseMessage = AwaitResponseMessage
	self.BaudRateToNum = BaudRateToNum
	self.ComputeCheckForBlock = ComputeCheckForBlock
	self.GetConfig = GetConfig
	self.GetFrame = GetFrame
	self.GetFrameCount = GetFrameCount
	self.NewLocalTimer = NewLocalTimer
	self.NMEALinesIter = NMEALinesIter
	self.NMEAReceiver = nmeaReceiver
	self.SetConfig = SetConfig
	self.GetModemSendText = GetModemSendText
	self.ParseIncomingData = ParseIncomingData
	self.SendModemCommand = SendModemCommand
	self.Transmit = Transmit
	self.TransmitModemSendText = TransmitModemSendText
	self.Wait = Wait

	--------------------
	-- Public Methods --
	--------------------

	o.AdjustToCurrentBaudRate = AdjustToCurrentBaudRate
	o.AwaitAnyMessage = AwaitAnyMessage
	o.AwaitIncomingIPC = AwaitIncomingIPC
	o.AwaitResponseMessage = AwaitResponseMessage
	o.BaudRateToNum = BaudRateToNum
	o.ComputeCheckForBlock = ComputeCheckForBlock
	o.GetConfig = GetConfig
	o.GetFrame = GetFrame
	o.GetFrameCount = GetFrameCount
	o.NMEALinesIter = NMEALinesIter
	o.NMEAReceiver = nmeaReceiver
	o.SetConfig = SetConfig
	o.GetModemSendText = GetModemSendText
	o.ParseIncomingData = ParseIncomingData
	o.SendModemCommand = SendModemCommand
	o.Transmit = Transmit
	o.TransmitModemSendText = TransmitModemSendText
	o.Wait = Wait
	return o

end

