local F = minetest.formspec_escape

-- hashed node pos -> sound handle
local played_sounds = {}

-- all of these can be set via the formspec
local meta_keys = {
	-- SimpleSoundSpec
	"sss.name",
	"sss.gain",
	"sss.pitch",
	"sss.fade",
	-- sound parameters
	"sparam.gain",
	"sparam.pitch",
	"sparam.fade",
	"sparam.start_time",
	"sparam.loop",
	"sparam.pos",
	"sparam.object",
	"sparam.to_player",
	"sparam.exclude_player",
	"sparam.max_hear_distance",
	-- fade
	"fade.step",
	"fade.gain",
	-- other
	"ephemeral",
}

local function get_all_metadata(meta)
	return {
		sss = {
			name  = meta:get_string("sss.name"),
			gain  = meta:get_string("sss.gain"),
			pitch = meta:get_string("sss.pitch"),
			fade  = meta:get_string("sss.fade"),
		},
		sparam = {
			gain              = meta:get_string("sparam.gain"),
			pitch             = meta:get_string("sparam.pitch"),
			fade              = meta:get_string("sparam.fade"),
			start_time       = meta:get_string("sparam.start_time"),
			loop              = meta:get_string("sparam.loop"),
			pos               = meta:get_string("sparam.pos"),
			object            = meta:get_string("sparam.object"),
			to_player         = meta:get_string("sparam.to_player"),
			exclude_player    = meta:get_string("sparam.exclude_player"),
			max_hear_distance = meta:get_string("sparam.max_hear_distance"),
		},
		fade = {
			gain = meta:get_string("fade.gain"),
			step = meta:get_string("fade.step"),
		},
		ephemeral = meta:get_string("ephemeral"),
	}
end

local function log_msg(msg)
	minetest.log("action", msg)
	minetest.chat_send_all(msg)
end

local function try_call(f, ...)
	local function log_on_err(success, errmsg, ...)
		if not success then
			log_msg("[soundstuff:jukebox] Call failed: "..errmsg)
		else
			return errmsg, ...
		end
	end

	return log_on_err(pcall(f, ...))
end

local function show_formspec(pos, player)
	local meta = minetest.get_meta(pos)

	local md = get_all_metadata(meta)

	local pos_hash = minetest.hash_node_position(pos)
	local sound_handle = played_sounds[pos_hash]

	local fs = {}
	local function fs_add(str)
		table.insert(fs, str)
	end

	fs_add([[
		formspec_version[6]
		size[14,12]
	]])

	-- SimpleSoundSpec
	fs_add(string.format([[
		container[0.5,0.5]
		box[-0.1,-0.1;4.2,3.2;#EBEBEB20]
		style[*;font=mono,bold]
		label[0,0.25;SimpleSoundSpec]
		style[*;font=mono]
		field[0.00,1.00;4,0.75;sss.name;name;%s]
		field[0.00,2.25;1,0.75;sss.gain;gain;%s]
		field[1.25,2.25;1,0.75;sss.pitch;pitch;%s]
		field[2.50,2.25;1,0.75;sss.fade;fade;%s]
		container_end[]
		field_close_on_enter[sss.name;false]
		field_close_on_enter[sss.gain;false]
		field_close_on_enter[sss.pitch;false]
		field_close_on_enter[sss.fade;false]
	]], F(md.sss.name), F(md.sss.gain), F(md.sss.pitch), F(md.sss.fade)))

	-- sound parameter table
	fs_add(string.format([[
		container[5.5,0.5]
		box[-0.1,-0.1;4.2,10.7;#EBEBEB20]
		style[*;font=mono,bold]
		label[0,0.25;sound parameter table]
		style[*;font=mono]
		field[0.00,1;1,0.75;sparam.gain;gain;%s]
		field[1.25,1;1,0.75;sparam.pitch;pitch;%s]
		field[2.50,1;1,0.75;sparam.fade;fade;%s]
		field[0,2.25;4,0.75;sparam.start_time;start_time;%s]
		field[0,3.50;4,0.75;sparam.loop;loop;%s]
		field[0,4.75;4,0.75;sparam.pos;pos;%s]
		field[0,6.00;4,0.75;sparam.object;object;%s]
		field[0,7.25;4,0.75;sparam.to_player;to_player;%s]
		field[0,8.50;4,0.75;sparam.exclude_player;exclude_player;%s]
		field[0,9.75;4,0.75;sparam.max_hear_distance;max_hear_distance;%s]
		container_end[]
		field_close_on_enter[sparam.gain;false]
		field_close_on_enter[sparam.pitch;false]
		field_close_on_enter[sparam.fade;false]
		field_close_on_enter[sparam.start_time;false]
		field_close_on_enter[sparam.loop;false]
		field_close_on_enter[sparam.pos;false]
		field_close_on_enter[sparam.object;false]
		field_close_on_enter[sparam.to_player;false]
		field_close_on_enter[sparam.exclude_player;false]
		field_close_on_enter[sparam.max_hear_distance;false]
		tooltip[sparam.object;Get a name with the Branding Iron.]
	]], F(md.sparam.gain), F(md.sparam.pitch), F(md.sparam.fade),
			F(md.sparam.start_time), F(md.sparam.loop), F(md.sparam.pos),
			F(md.sparam.object), F(md.sparam.to_player), F(md.sparam.exclude_player),
			F(md.sparam.max_hear_distance)))

	-- fade
	fs_add(string.format([[
		container[10.75,3]
		box[-0.1,-0.1;3.2,3.2;#EBEBEB20]
		style[*;font=mono,bold]
		label[0,0.25;fade]
		style[*;font=mono]
		field[0.00,1;1,0.75;fade.step;step;%s]
		field[1.25,1;1,0.75;fade.gain;gain;%s]
	]], F(md.fade.step), F(md.fade.gain)))
	if not sound_handle then
		fs_add([[
			box[0,2;3,0.75;#363636FF]
			label[0.25,2.375;no sound]
		]])
	else
		fs_add([[
			button[0,2;3,0.75;btn_fade;Fade]
		]])
	end
	fs_add([[
		container_end[]
		field_close_on_enter[fade.step;false]
		field_close_on_enter[fade.gain;false]
	]])

	-- ephemeral
	fs_add(string.format([[
		checkbox[0.5,5;ephemeral;ephemeral;%s]
	]], md.ephemeral))

	-- play/stop and release buttons
	if not sound_handle then
		fs_add([[
			container[10.75,0.5]
			button[0,0;3,0.75;btn_play;Play]
			container_end[]
		]])
	else
		fs_add([[
			container[10.75,0.5]
			button[0,0;3,0.75;btn_stop;Stop]
			button[0,1;3,0.75;btn_release;Release]
			container_end[]
		]])
	end

	-- save and quit button
	fs_add([[
		button_exit[10.75,11;3,0.75;btn_save_quit;Save & Quit]
	]])

	minetest.show_formspec(player:get_player_name(), "soundstuff:jukebox@"..pos:to_string(),
			table.concat(fs))
end

minetest.register_node("soundstuff:jukebox", {
	description = "Jukebox\nAllows to play arbitrary sounds.",
	tiles = {"soundstuff_jukebox.png"},
	groups = {dig_immediate = 2},

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		-- SimpleSoundSpec
		meta:set_string("sss.name", "")
		meta:set_string("sss.gain", "")
		meta:set_string("sss.pitch", "")
		meta:set_string("sss.fade", "")
		-- sound parameters
		meta:set_string("sparam.gain", "")
		meta:set_string("sparam.pitch", "")
		meta:set_string("sparam.fade", "")
		meta:set_string("sparam.start_time", "")
		meta:set_string("sparam.loop", "")
		meta:set_string("sparam.pos", pos:to_string())
		meta:set_string("sparam.object", "")
		meta:set_string("sparam.to_player", "")
		meta:set_string("sparam.exclude_player", "")
		meta:set_string("sparam.max_hear_distance", "")
		-- fade
		meta:set_string("fade.gain", "")
		meta:set_string("fade.step", "")
		-- other
		meta:set_string("ephemeral", "")

		meta:mark_as_private(meta_keys)
	end,

	on_rightclick = function(pos, _node, clicker, _itemstack, _pointed_thing)
		show_formspec(pos, clicker)
	end,
})

minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname:sub(1, 19) ~= "soundstuff:jukebox@" then
		return false
	end

	local pos = vector.from_string(formname, 20)
	if not pos or pos ~= pos:round() then
		minetest.log("error", "[soundstuff:jukebox] Invalid formname.")
		return true
	end

	local meta = minetest.get_meta(pos)

	for _, k in ipairs(meta_keys) do
		if fields[k] then
			meta:set_string(k, fields[k])
		end
	end
	meta:mark_as_private(meta_keys)

	local pos_hash = minetest.hash_node_position(pos)
	local sound_handle = played_sounds[pos_hash]

	if not sound_handle then
		if fields.btn_play then
			local md = get_all_metadata(meta)

			local sss = {
				name = md.sss.name,
				gain  = tonumber(md.sss.gain),
				pitch = tonumber(md.sss.pitch),
				fade  = tonumber(md.sss.fade),
			}
			local sparam = {
				gain  = tonumber(md.sparam.gain),
				pitch = tonumber(md.sparam.pitch),
				fade  = tonumber(md.sparam.fade),
				start_time = tonumber(md.sparam.start_time),
				loop = minetest.is_yes(md.sparam.loop),
				pos = vector.from_string(md.sparam.pos),
				object = testtools.get_branded_object(md.sparam.object),
				to_player = md.sparam.to_player,
				exclude_player = md.sparam.exclude_player,
				max_hear_distance = tonumber(md.sparam.max_hear_distance),
			}
			local ephemeral = minetest.is_yes(md.ephemeral)

			log_msg(string.format(
					"[soundstuff:jukebox] Playing sound: minetest.sound_play(%s, %s, %s)",
					string.format("{name=\"%s\", gain=%s, pitch=%s, fade=%s}",
							sss.name, sss.gain, sss.pitch, sss.fade),
					string.format("{gain=%s, pitch=%s, fade=%s, start_time=%s, loop=%s, pos=%s, "
						.."object=%s, to_player=\"%s\", exclude_player=\"%s\", max_hear_distance=%s}",
							sparam.gain, sparam.pitch, sparam.fade, sparam.start_time,
							sparam.loop, sparam.pos, sparam.object and "<objref>",
							sparam.to_player, sparam.exclude_player,
							sparam.max_hear_distance),
					tostring(ephemeral)))

			sound_handle = try_call(minetest.sound_play, sss, sparam, ephemeral)

			played_sounds[pos_hash] = sound_handle
			show_formspec(pos, player)
		end

	else
		if fields.btn_stop then
			log_msg("[soundstuff:jukebox] Stopping sound: minetest.sound_stop(<handle>)")

			try_call(minetest.sound_stop, sound_handle)

		elseif fields.btn_release then
			log_msg("[soundstuff:jukebox] Releasing handle.")

			played_sounds[pos_hash] = nil
			show_formspec(pos, player)

		elseif fields.btn_fade then
			local md = get_all_metadata(meta)

			local step = tonumber(md.fade.step)
			local gain = tonumber(md.fade.gain)

			log_msg(string.format(
					"[soundstuff:jukebox] Fading sound: minetest.sound_fade(<handle>, %s, %s)",
					step, gain))

			try_call(minetest.sound_fade, sound_handle, step, gain)
		end
	end

	return true
end)
