-- license:BSD-3-Clause
-- copyright-holders:Vas Crabb
local exports = {
	name = 'timecode',
	version = '0.0.1',
	description = 'Timecode recorder plugin',
	license = 'BSD-3-Clause',
	author = { name = 'Vas Crabb' } }


local timecode = exports

local frame_subscription, stop_subscription

function timecode.startplugin()
	local file                  -- the timecode log file
	local write                 -- whether to record a timecode on the next emulated frame
	local text                  -- name of current part
	local frame_count           -- emulated frame counter
	local start_frame           -- start frame count for current part
	local start_time            -- start time for current part
	local total_time            -- total time of parts so far this session
	local count                 -- current timecode number
	local show_counter          -- whether to show elapsed time since last timecode
	local show_total            -- whether to show the total time of parts

	local frame_mode            -- 0 to count frames, 1 to assume 60 Hz
	local hotkey_seq            -- input sequence to record timecode
	local hotkey_pressed        -- whether the hotkey was pressed on the last frame update
	local hotkey_cfg            -- configuration string for the hotkey

	local item_framemode        -- menu index of frame mode item
	local item_hotkey           -- menu index of hotkey item
	local commonui              -- common UI helpers
	local hotkey_poller         -- helper for configuring hotkey


	local function get_settings_path()
		return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/timecode'
	end


	local function set_default_hotkey()
		hotkey_seq = manager.machine.input:seq_from_tokens('KEYCODE_F12 NOT KEYCODE_LSHIFT NOT KEYCODE_RSHIFT NOT KEYCODE_LALT NOT KEYCODE_RALT')
		hotkey_cfg = nil
	end


	local function load_settings()
		-- set defaults
		frame_mode = 1
		set_default_hotkey()

		-- try to open configuration file
		local cfgname = get_settings_path() .. '/plugin.cfg'
		local cfgfile = io.open(cfgname, 'r')
		if not cfgfile then
			return -- probably harmless, configuration just doesn't exist yet
		end

		-- parse settings as JSON
		local json = require('json')
		local settings = json.parse(cfgfile:read('a'))
		cfgfile:close()
		if not settings then
			emu.print_error(string.format('Error loading timecode recorder settings: error parsing file "%s" as JSON', cfgname))
			return
		end

		-- recover frame mode
		local count_frames = settings.count_frames
		if count_frames ~= nil then
			frame_mode = count_frames and 0 or 1
		end

		-- recover hotkey assignment
		hotkey_cfg = settings.hotkey
		if hotkey_cfg then
			local seq = manager.machine.input:seq_from_tokens(hotkey_cfg)
			if seq then
				hotkey_seq = seq
			end
		end
	end


	local function save_settings()
		local path = get_settings_path()
		local attr = lfs.attributes(path)
		if not attr then
			lfs.mkdir(path)
		elseif attr.mode ~= 'directory' then
			emu.print_error(string.format('Error saving timecode recorder settings: "%s" is not a directory', path))
			return
		end
		local json = require('json')
		local settings = { count_frames = frame_mode == 0 }
		if hotkey_cfg then
			settings.hotkey = hotkey_cfg
		end
		local data = json.stringify(settings, { indent = true })
		local cfgname = path .. '/plugin.cfg'
		local cfgfile = io.open(cfgname, 'w')
		if not cfgfile then
			emu.print_error(string.format('Error saving timecode recorder settings: error opening file "%s" for writing', cfgname))
			return
		end
		cfgfile:write(data)
		cfgfile:close()
	end


	local function process_frame()
		if (not file) or manager.machine.paused then
			return
		end
		if write then
			write = false
			count = count + 1
			show_total = true

			-- time from beginning of playback in milliseconds, HH:MM:SS.fff and frames
			local curtime = manager.machine.time
			local sec_start = curtime.seconds
			local msec_start = (sec_start * 1000) + curtime.msec
			local msec_start_str = string.format('%015d', msec_start)
			local curtime_str = string.format(
					'%02d:%02d:%02d.%03d',
					sec_start // (60 * 60),
					(sec_start // 60) % 60,
					sec_start % 60,
					msec_start % 1000)
			local frame_start_str = string.format('%015d', (frame_mode == 0) and frame_count or (msec_start * 60 // 1000))

			-- elapsed from previous timecode in milliseconds, HH:MM:SS.fff and frames
			local elapsed = curtime - start_time
			local sec_elapsed = elapsed.seconds
			local msec_elapsed = (sec_elapsed * 1000) + elapsed.msec
			local msec_elapsed_str = string.format('%015d', msec_elapsed)
			local elapsed_str = string.format(
					'%02d:%02d:%02d.%03d',
					sec_elapsed // (60 * 60),
					(sec_elapsed // 60) % 60,
					sec_elapsed % 60,
					msec_elapsed % 1000)
			local frame_elapsed_str = string.format('%015d', (frame_mode == 0) and (frame_count - start_frame) or (msec_elapsed * 60 // 1000))

			-- update start of part
			start_frame = frame_count
			start_time = curtime

			local message
			local key
			if count == 1 then
				text = 'INTRO'
				show_counter = true
				message = string.format(_p('plugin-timecode', 'TIMECODE: Intro started at %s'), curtime_str)
				key = 'INTRO_START'
			elseif count == 2 then
				total_time = total_time + elapsed
				show_counter = false
				message = string.format(_p('plugin-timecode', 'TIMECODE: Intro duration %s'), elapsed_str)
				key = 'INTRO_STOP'
			elseif count == 3 then
				text = 'GAMEPLAY'
				show_counter = true
				message = string.format(_p('plugin-timecode', 'TIMECODE: Gameplay started at %s'), curtime_str)
				key = 'GAMEPLAY_START'
			elseif count == 4 then
				total_time = total_time + elapsed
				show_counter = false
				message = string.format(_p('plugin-timecode', 'TIMECODE: Gameplay duration %s'), elapsed_str)
				key = 'GAMEPLAY_STOP'
			elseif (count % 2) == 1 then
				local extrano = (count - 3) // 2
				text = string.format('EXTRA %d', extrano)
				show_counter = true
				message = string.format(_p('plugin-timecode', 'TIMECODE: Extra %d started at %s'), extrano, curtime_str)
				key = string.format('EXTRA_START_%03d', extrano)
			else
				local extrano = (count - 4) // 2
				total_time = total_time + elapsed
				show_counter = false
				message = string.format(_p('plugin-timecode', 'TIMECODE: Extra %d duration %s'), extrano, elapsed_str)
				key = string.format('EXTRA_STOP_%03d', extrano)
			end

			emu.print_info(message)
			manager.machine:popmessage(message)

			file:write(
					string.format(
						'%-19s %s %s %s %s %s %s\n',
						key,
						curtime_str, elapsed_str,
						msec_start_str, msec_elapsed_str,
						frame_start_str, frame_elapsed_str))
		end
		frame_count = frame_count + 1
	end


	local function process_frame_done()
		local machine = manager.machine
		if show_counter then
			-- show duration of current part
			local counter = (machine.time - start_time).seconds
			local counter_str = string.format(
					machine.paused and _p('plugin-timecode', ' %s%s%02d:%02d [paused] ') or _p('plugin-timecode', ' %s%s%02d:%02d '),
					text,
					(#text > 0) and ' ' or '',
					(counter // 60) % 60,
					counter % 60)
			machine.render.ui_container:draw_text('right', 0, counter_str, 0xf0f01010, 0xff000000)
		end
		if show_total then
			-- show total time for all parts so far
			local total = ((show_counter and (machine.time - start_time) or emu.attotime()) + total_time).seconds
			total_str = string.format(_p('plugin-timecode', 'TOTAL %02d:%02d '), (total // 60) % 60, total % 60)
			machine.render.ui_container:draw_text('left', 0, total_str, 0xf010f010, 0xff000000)
		end
		if file then
			local pressed = machine.input:seq_pressed(hotkey_seq)
			if (not hotkey_pressed) and pressed then
				write = true
			end
			hotkey_pressed = pressed
		end
	end


	local function start()
		file = nil
		show_counter = false
		show_total = false
		load_settings()

		-- only do timecode recording if we're doing input recording
		local options = manager.machine.options.entries
		local filename = options.record:value()
		if #filename > 0 then
			filename = filename .. '.timecode'
			emu.print_info(string.format('Record input timecode file: %s', filename))
			file = emu.file(options.input_directory:value(), 0x0e) -- FIXME: magic number for flags
			local openerr = file:open(filename)
			if openerr then
				-- TODO: this used to throw a fatal error and log the error description
				emu.print_error('Failed to open file for input timecode recording')
				file = nil
			else
				write = false
				text = ''
				frame_count = 0
				start_frame = 0
				start_time = emu.attotime()
				total_time = emu.attotime()
				count = 0
				show_counter = false
				show_total = false
				hotkey_pressed = false

				file:write('# ==========================================\n')
				file:write('# TIMECODE FILE FOR VIDEO PREVIEW GENERATION\n')
				file:write('# ==========================================\n')
				file:write('#\n')
				file:write('# VIDEO_PART:     code of video timecode\n')
				file:write('# START:          start time (hh:mm:ss.mmm)\n')
				file:write('# ELAPSED:        elapsed time (hh:mm:ss.mmm)\n')
				file:write('# MSEC_START:     start time (milliseconds)\n')
				file:write('# MSEC_ELAPSED:   elapsed time (milliseconds)\n')
				file:write('# FRAME_START:    start time (frames)\n')
				file:write('# FRAME_ELAPSED:  elapsed time (frames)\n')
				file:write('#\n')
				file:write('# VIDEO_PART======= START======= ELAPSED===== MSEC_START===== MSEC_ELAPSED=== FRAME_START==== FRAME_ELAPSED==\n')
			end
		end
	end


	local function stop()
		-- close the file if we're recording
		if file then
			file:close()
			file = nil
		end

		-- try to save settings
		save_settings()
	end


	local function menu_callback(index, event)
		if hotkey_poller then
			if hotkey_poller:poll() then
				if hotkey_poller.sequence then
					hotkey_seq = hotkey_poller.sequence
					hotkey_cfg = manager.machine.input:seq_to_tokens(hotkey_seq)
				end
				hotkey_poller = nil
				return true
			end
		elseif index == item_framemode then
			if (event == 'select') or (event == 'left') or (event == 'right') then
				frame_mode = (frame_mode ~= 0) and 0 or 1
				return true
			end
		elseif index == item_hotkey then
			if event == 'select' then
				if not commonui then
					commonui = require('commonui')
				end
				hotkey_poller = commonui.switch_polling_helper()
				return true
			elseif event == 'clear' then
				set_default_hotkey()
				return true
			end
		end
		return false
	end


	local function menu_populate()
		local result = { }
		table.insert(result, { _p('plugin-timecode', 'Timecode Recorder'), '', 'off' })
		table.insert(result, { '---', '', '' })

		local frame_mode_val = (frame_mode > 0) and _p('plugin-timecode', 'Assume 60 Hz') or _p('plugins-timecode', 'Count emulated frames')
		table.insert(result, { _p('plugin-timecode', 'Frame numbers'), frame_mode_val, (frame_mode > 0) and 'l' or 'r' })
		item_framemode = #result

		table.insert(result, { _p('plugin-timecode', 'Hotkey'), manager.machine.input:seq_name(hotkey_seq), hotkey_poller and 'lr' or '' })
		item_hotkey = #result

		if hotkey_poller then
			return hotkey_poller:overlay(result)
		else
			return result
		end
	end


	frame_subscription = emu.add_machine_frame_notifier(process_frame)
	emu.register_frame_done(process_frame_done)
	emu.register_prestart(start)
	stop_subscription = emu.add_machine_stop_notifier(stop)
	emu.register_menu(menu_callback, menu_populate, _p('plugin-timecode', 'Timecode Recorder'))
end

return exports
