-- Must be safe version of cjson-lib for error handling
local json = require 'cjson.safe'
local plpath = require 'pl.path'
local pldir = require 'pl.dir'
local plutils = require 'pl.utils'
local cqueues = require 'cqueues'
local posix = require 'posix'
local process = require 'cqp.process'
local RAME = require 'rame.rame'

-- wait time in secs to check bmd-streamer is still running
local WAIT_PROCESS = 1
local BMD_STREAMER = "/usr/bin/bmd-streamer"
local SCRIPT_PATH = "/var/run/ramerecorder-ffmpeg.sh"
local FLAG_FILE = "/var/run/ramerecorder"
local FIRMWARE_PATH = "/media/mmcblk0p1/bmd-streamer"
local SETTINGS_FILE = "recorder.json"

local recorder_fields = {
	avgVideoBitrate  = { typeof="string"  },
	maxVideoBitrate  = { typeof="string"  },
	h264Profile      = { typeof="string"  },
	h264Level        = { typeof="string"  },
	h264BFrames      = { typeof="string"  },
	h264Cabac        = { typeof="string"  },
	fpsDivider       = { typeof="string"  },
	audioBitrate     = { typeof="string"  },
	input            = { typeof="string"  },
	recorderEnabled  = { typeof="boolean" },
	streamingEnabled = { typeof="boolean" },
	streamMode       = { typeof="string"  },
}

-- Plugin Hooks
local Plugin = {}

Plugin.settings = nil
Plugin.streaming = false
Plugin.recording = false
Plugin.stopping = false

-- REST API: /recorder/
local RECORDER = { GET = {}, PUT = {} }

-- Generates ffmpeg script to be launched by bmd-stream
local function gen_script(cfg)
	local c = [[#!/bin/sh
# auto-generated by RamePlayer
exec /usr/bin/ffmpeg -fflags +genpts -i - ]]

	if cfg.recorderEnabled then
		-- might be required for mp4 (if we go back to it):
		-- c = c .. " \\\n\t-bsf:a aac_adtstoasc"
		-- probably required for flv (if we add support for it):
		-- c = c .. " \\\n\t-flvflags aac_seq_header_detect"
		c = c .. " \\\n\t-codec:a copy -codec:v copy"
		c = c .. " \\\n\t-f mpegts"
		c = c .. " \\\n\t\"" .. cfg.recordingPath .. "\""
	end

	if cfg.streamingEnabled then
		if cfg.streamMode == "1" or cfg.streamMode == "extServer" then
			local server = "rtmp://localhost/rame/program"
			if cfg.streamMode == "extServer" then
				server = cfg.streamServer
			end
			c = c .. " \\\n\t-bsf:a aac_adtstoasc"
			c = c .. " \\\n\t-f flv -codec:a copy -codec:v copy"
			c = c .. " \\\n\t" .. server
		elseif cfg.streamMode == "2" then
			c = c .. " \\\n\t-bsf:a aac_adtstoasc"
			local bvrate = 1700
			if cfg.avgVideoBitrate ~= "" then
				bvrate = cfg.avgVideoBitrate / 2
			end
--			c = c .. " \\\n\t-f flv -codec:v copy -strict experimental -codec:a aac -b:a 256k -af \"pan=1c|c0=c0\" rtmp://localhost/rame/audio1"
--			c = c .. " \\\n\t-f flv -codec:v copy -strict experimental -codec:a aac -b:a 256k -af \"pan=1c|c0=c1\" rtmp://localhost/rame/audio2"
			c = c .. " \\\n\t-f flv -codec:v copy -b:v "..bvrate.."k -ar 44100 -codec:a aac -b:a 128k -af \"pan=1c|c0=c0\" rtmp://localhost/rame/audio1"
			c = c .. " \\\n\t-f flv -codec:v copy -b:v "..bvrate.."k -ar 44100 -codec:a aac -b:a 128k -af \"pan=1c|c0=c1\" rtmp://localhost/rame/audio2"
		elseif cfg.streamMode == "custom" then
			c = c .. " \\\n\t" .. cfg.customParams
		end
	end
	return c .. "\n"
end

local function write_script(data)
	plutils.writefile(SCRIPT_PATH, data)
	posix.chmod(SCRIPT_PATH, "rwxrwxrwx")
end


local function stop_process()
	Plugin.stopping = true
	RAME.log.debug('stop streaming/recording')
	if Plugin.process then
		Plugin.process:kill(15)
	end
	Plugin.process = nil
end

-- Starts bmd-streamer
local function start_process(cfg)
	Plugin.stopping = false
	RAME.recorder.running(true)
	if cfg.recorderEnabled then
		Plugin.recording = true
		RAME.recorder.recording(true)
	end
	if cfg.streamingEnabled then
		Plugin.streaming = true
		RAME.recorder.streaming(true)
	end

	write_script(gen_script(cfg))

	cqueues.running():wrap(function()

		local function run()
			local cmd = {
				BMD_STREAMER,
				"-S", cfg.input,
				-- "--syslog", -- we want stderr instead
				"--firmware-dir", FIRMWARE_PATH,
				"--exec", SCRIPT_PATH,
			}

			if cfg.recorderEnabled then
				table.insert(cmd, "-z")
				if tonumber(cfg.avgVideoBitrate) > 3000 then
					table.insert(cmd, "8192")
				else
					table.insert(cmd, "4096")
				end
			end

			if cfg.avgVideoBitrate ~= "" then
				table.insert(cmd, "--video-kbps")
				table.insert(cmd, "" .. cfg.avgVideoBitrate)
			end

			if cfg.maxVideoBitrate ~= "" then
				table.insert(cmd, "--video-max-kbps")
				table.insert(cmd, "" .. cfg.maxVideoBitrate)
			end

			if cfg.audioBitrate ~= "" then
				RAME.log.debug(cfg.audioBitrate)
				table.insert(cmd, "--audio-kbps")
				table.insert(cmd, "" .. cfg.audioBitrate)
			end

			if cfg.h264Profile then
				table.insert(cmd, "--h264-profile")
				table.insert(cmd, cfg.h264Profile)
			end

			if cfg.h264Level ~= "" then
				table.insert(cmd, "--h264-level")
				table.insert(cmd, "" .. cfg.h264Level)
			end

			if cfg.h264BFrames == "enabled" then
				table.insert(cmd, "--h264-bframes")
			elseif cfg.h264BFrames == "disabled" then
				table.insert(cmd, "--h264-no-bframes")
			end

			if cfg.h264Cabac == "enabled" then
				table.insert(cmd, "--h264-cabac")
			elseif cfg.h264Cabac == "disabled" then
				table.insert(cmd, "--h264-no-cabac")
			end

			if cfg.fpsDivider ~= nil then
				table.insert(cmd, "--fps-divider")
				table.insert(cmd, "" .. cfg.fpsDivider)
			end

			RAME.log.info("start streaming/recording")
			RAME.log.debug('cmd:', table.unpack(cmd))
			--Plugin.process = process.spawn(table.unpack(cmd))

			RAME.recorder.last_statusline = nil
			RAME.recorder.last_warning = nil
			RAME.recorder.last_error = nil

			local out, process = process.popen_err(table.unpack(cmd))
			if out ~= nil and process ~= nil then
				Plugin.process = process
				local start_time = cqueues.monotime()
				local read_buf = ""
				local finished = false
				local encoder_started = false
				local scheduled_stop = 0
				repeat
					-- split buffered read of stderr data to lines
					local line_sep = '[\r\n]'  -- accept both \r,\n for endline
					local line = nil
					local stop_reading = false
					local data, errmsg, errnum
					repeat
						data, errmsg, errnum = out:read(30)
						if data == nil and errnum == posix.EAGAIN then
							cqueues.poll(out)
						elseif data == nil or #data == 0 then
							finished = true
							stop_reading = true
							line = read_buf -- last line w/o endline
						else
							local nlpos = data:find(line_sep)
							if nlpos == nil then
								-- add to buffer, keep reading more
								read_buf = read_buf .. data
							else
								-- line = prev.buffer and current data up to newline
								line = read_buf .. data:sub(1, nlpos - 1)
								-- rest of current data is buffered for next line
								read_buf = data:sub(nlpos + 1)
								stop_reading = true
							end
						end
					until stop_reading

					-- process a single line
					--print("# "..line)  -- for debug

					local rec_time = cqueues.monotime() - start_time

					-- don't send initial lines as status
					if rec_time > 5 or line:find("frame=") then
						RAME.recorder.last_statusline = line
					end

					if rec_time > 15 then
						if line:find("error writing MPEG TS: Resource temporarily unavailable") then
							-- "write errors" may happen at start, but should not come later
							RAME.recorder.last_warning = "Write error (USB drive may be too slow)"
							RAME.log.warn(RAME.recorder.last_warning)
						elseif (line:find("Error parsing") or line:find("Error applying")) then
							RAME.recorder.last_warning = "Data error (USB drive may be too slow)"
							RAME.log.warn(RAME.recorder.last_warning)
						elseif line:find("AAC bitstream not in ADTS format") then
							RAME.recorder.last_warning = "Audio error (USB drive may be too slow)"
							RAME.log.warn(RAME.recorder.last_warning)
						end
					end

					if not encoder_started then
						if line:find("ffmpeg") or line:find("frame=") or line:find("configuring and starting encoder") then
							encoder_started = true
							RAME.log.info("Encoder started")
						elseif rec_time > 8 then
							RAME.recorder.last_error = "Encoder doesn't seem to start"
							RAME.log.warn(RAME.recorder.last_error)
						end
					end

					if line:find("No space left on device") then
						RAME.recorder.last_warning = "Out of disk space!"
						RAME.log.warn("Out of disk space while recording")
						finished = true
					end

					if not Plugin.stopping and (line:find("error writing MPEG TS: Broken pipe") or line:find("stopping encoder")) then
						RAME.recorder.last_error = "Encoder stopped unexpectedly"
						RAME.log.warn(RAME.recorder.last_error)
						finished = true
					end

				until finished
				out:close()

				---- wait until process is terminated
				--local status = Plugin.process:wait()
				--RAME.log.info(('bmd-streamer terminated: %d'):format(status))
				RAME.log.info("Recording/streaming ended")
			else
				RAME.log.error("Can't start bmd-streamer process")
			end

			stop_process()

			RAME.recorder.running(false)
			RAME.recorder.recording(false)
			RAME.recorder.streaming(false)
			Plugin.recording = false
			Plugin.streaming = false

			-- clear only after a while
			-- otherwise the UI will miss the last notifications
			-- (todo: should refine this to be more robust)
			cqueues.poll(5.0)
			RAME.recorder.last_error = nil
			RAME.recorder.last_warning = nil
			Plugin.stopping = false
		end

		if cfg.recorderEnabled then
			-- mountpoint needs to remounted rw before recording
			local mountpoint = RAME.get_mountpoint(cfg.recordingPath)
			-- make sure mountpoint is remounted ro after stopping recording
			RAME.remounter:wrap(mountpoint, run)
		else
			run()  -- just streaming
		end
	end)

	-- wait to see if running flag has changed
	cqueues.poll(WAIT_PROCESS)
	return RAME.recorder.running()
end

local function check_firmware_binaries()
	if not plpath.isdir(FIRMWARE_PATH) or
	  #pldir.getfiles(FIRMWARE_PATH, "*.bin") == 0 then
		local errmsg = "Recorder firmware binaries are missing"
		RAME.recorder.last_error = errmsg
		RAME.log.error(errmsg)
	end
end

-- udev script calls GET /recorder/enable when device is plugged in
function RECORDER.GET.enable()
	RAME.recorder.enabled(true)
	RAME.log.info('Recorder device connected')
	check_firmware_binaries()
	return 200, {}
end

-- udev script calls GET /recorder/disable when device is removed
function RECORDER.GET.disable()
	RAME.recorder.enabled(false)
	RAME.log.info('Recorder device disconnected')
	return 200, {}
end

function RECORDER.GET.config()
	return 200, Plugin.settings or {}
end

function RECORDER.PUT.start(ctx, reply)
	local cfg = ctx.args

	-- validate request
	err, msg = RAME.check_fields(cfg, recorder_fields)
	if err then return err, msg end

	if Plugin.streaming or Plugin.recording then
		return 503, { error = "bmd-streamer already running" }
	end

	-- validate recording path
	if cfg.recorderEnabled then
		local exists = cfg.recordingPath == "" or plpath.exists(cfg.recordingPath)
		if exists then
			return 500, { error="Recording file already exists"}
		end
	end

	if start_process(cfg) then
		Plugin.settings = cfg
		if not RAME.write_settings_file(SETTINGS_FILE, json.encode(cfg)) then
			RAME.log.error("File write error: "..SETTINGS_FILE)
			return 500, { error="File write error: "..SETTINGS_FILE }
		end
		return 200, {}
	else
		return 500, { error = "bmd-streamer not started" }
	end
end

function RECORDER.GET.stop(ctx, reply)
	if Plugin.process then
		stop_process()
		return 200, {}
	end
	return 500, { error = "bmd-streamer was not running" }
end

function Plugin.init()
	-- check if flag file exists, created by udev script
	RAME.recorder.enabled(plpath.exists(FLAG_FILE))

	if RAME.recorder.enabled() then
		RAME.log.info('Recorder device detected')
		check_firmware_binaries()
	else
		RAME.log.info('Recorder device not detected')
	end

	local cfg = json.decode(RAME.read_settings_file(SETTINGS_FILE) or "")
	if cfg == nil then
		-- default values
		cfg = {}
		cfg.streamingEnabled = false
		cfg.recorderEnabled = false
		cfg.input = "sdi"
		cfg.avgVideoBitrate = "3000"
		cfg.maxVideoBitrate = "3100"
		cfg.h264Profile = "main"
		cfg.h264Level = "32"
		cfg.h264BFrames = "disabled"
		cfg.h264Cabac = "default"
		cfg.fpsDivider = ""
		cfg.audioBitrate = ""
		cfg.streamMode = "1"
		cfg.recordingPath = "/media/sda1/recording.ts"
		cfg.streamServer = ""
		cfg.customParams = ""
	end
	if RAME.check_fields(cfg, recorder_fields) == nil then
		Plugin.settings = cfg
	end

	RAME.rest.recorder = function(ctx, reply) return ctx:route(reply, RECORDER) end
end

return Plugin
