--
-- last-fm.lua: "now playing" feature using the last.fm API
--
-- Dependencies: lua-cjson (from luarocks e.g.)
--
-- I call this style closure-oriented programming
--
-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--

local cjson = require "cjson"

-- Setup configuration to load last.fm API credentials from
local user, api_key
xC.setup_config {
	user    = {
		type = "string",
		comment = "last.fm username",
		on_change = function (v) user    = v end
	},
	api_key = {
		type = "string",
		comment = "last.fm API key",
		on_change = function (v) api_key = v end
	},
}

-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

-- Generic error reporting
local report_error = function (buffer, error)
	buffer:log ("last-fm error: " .. error)
end

-- Process data return by the server and extract the now playing song
local process = function (buffer, data, action)
	-- There's no reasonable Lua package to parse HTTP that I could find
	local s, e, v, status, message = string.find (data, "(%S+) (%S+) .+\r\n")
	if not s then return "server returned unexpected data" end
	if status ~= "200" then return status .. " " .. message end

	local s, e = string.find (data, "\r\n\r\n")
	if not s then return "server returned unexpected data" end

	local parser = cjson.new ()
	data = parser.decode (string.sub (data, e + 1))
	if not data.recenttracks or not data.recenttracks.track then
		return "invalid response" end

	-- Need to make some sense of the XML automatically converted to JSON
	local text_of = function (node)
		if type (node) ~= "table" then return node end
		return node["#text"] ~= "" and node["#text"] or nil
	end

	local name, artist, album
	for i, track in ipairs (data.recenttracks.track) do
		if track["@attr"] and track["@attr"].nowplaying then
			if track.name   then name   = text_of (track.name)   end
			if track.artist then artist = text_of (track.artist) end
			if track.album  then album  = text_of (track.album)  end
		end
	end

	if not name then
		action (false)
	else
		local np = "\"" .. name .. "\""
		if artist then np = np .. " by "   .. artist end
		if album  then np = np .. " from " .. album  end
		action (np)
	end
end

-- Set up the connection and make the request
local on_connected = function (buffer, c, host, action)
	-- Buffer data in the connection object
	c.data = ""
	c.on_data = function (data)
		c.data = c.data .. data
	end

	-- And process it after we receive everything
	c.on_eof = function ()
		error = process (buffer, c.data, action)
		if error then report_error (buffer, error) end
		c:close ()
	end
	c.on_error = function (e)
		report_error (buffer, e)
	end

	-- Make the unencrypted HTTP request
	local url = "/2.0/?method=user.getrecenttracks&user=" .. user ..
		"&limit=1&api_key=" .. api_key .. "&format=json"
	c:send ("GET " .. url .. " HTTP/1.1\r\n")
	c:send ("User-agent: last-fm.lua\r\n")
	c:send ("Host: " .. host .. "\r\n")
	c:send ("Connection: close\r\n")
	c:send ("\r\n")
end

-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

-- Avoid establishing more than one connection at a time
local running

-- Initiate a connection to last.fm servers
async, await = xC.async, coroutine.yield
local make_request = function (buffer, action)
	if not user or not api_key then
		report_error (buffer, "configuration is incomplete")
		return
	end

	if running then running:cancel () end
	running = async.go (function ()
		local c, host, e = await (async.dial ("ws.audioscrobbler.com", 80))
		if e then
			report_error (buffer, e)
		else
			on_connected (buffer, c, host, action)
		end
		running = nil
	end)
end

-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

local now_playing

local tell_song = function (buffer)
	if now_playing == nil then
		buffer:log ("last-fm: I don't know what you're listening to")
	elseif not now_playing then
		buffer:log ("last-fm: not playing anything right now")
	else
		buffer:log ("last-fm: now playing: " .. now_playing)
	end
end

local send_song = function (buffer)
	if not now_playing then
		tell_song (buffer)
	else
		buffer:execute ("/me is listening to " .. now_playing)
	end
end

-- Hook input to simulate new commands
xC.hook_input (function (hook, buffer, input)
	if input == "/np" then
		make_request (buffer, function (np)
			now_playing = np
			send_song (buffer)
		end)
	elseif input == "/np?" then
		make_request (buffer, function (np)
			now_playing = np
			tell_song (buffer)
		end)
	elseif input == "/np!" then
		send_song (buffer)
	else
		return input
	end
end)