-- Playerctl signals
--
-- Provides:
-- metadata
--      title (string)
--      artist  (string)
--      album_path (string)
--      album (string)
--      player_name (string)
-- position
--      interval_sec (number)
--      length_sec (number)
-- playback_status
--      playing (boolean)
-- volume
--      volume (number)
-- loop_status
--      loop_status (string)
-- shuffle
--      shuffle (bool)
-- no_players
--      (No parameters)

local awful = require("awful")
local gobject = require("gears.object")
local gtable = require("gears.table")
local gtimer = require("gears.timer")
local gstring = require("gears.string")
local beautiful = require("beautiful")
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
local setmetatable = setmetatable
local tonumber = tonumber
local ipairs = ipairs
local type = type
local capi = { awesome = awesome }

local playerctl = { mt = {} }

function playerctl:disable()
    self._private.metadata_timer:stop()
    self._private.metadata_timer = nil
    awful.spawn.with_shell("killall playerctl")
end

function playerctl:pause(player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " pause")
    else
        awful.spawn.with_shell(self._private.cmd .. "pause")
    end
end

function playerctl:play(player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " play")
    else
        awful.spawn.with_shell(self._private.cmd .. "play")
    end
end

function playerctl:stop(player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " stop")
    else
        awful.spawn.with_shell(self._private.cmd .. "stop")
    end
end

function playerctl:play_pause(player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " play-pause")
    else
        awful.spawn.with_shell(self._private.cmd .. "play-pause")
    end
end

function playerctl:previous(player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " previous")
    else
        awful.spawn.with_shell(self._private.cmd .. "previous")
    end
end

function playerctl:next(player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " next")
    else
        awful.spawn.with_shell(self._private.cmd .. "next")
    end
end

function playerctl:set_loop_status(loop_status, player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " loop " .. loop_status)
    else
        awful.spawn.with_shell(self._private.cmd .. "loop " .. loop_status)
    end
end

function playerctl:cycle_loop_status(player)
    local function set_loop_status(loop_status)
        if loop_status == "None" then
            self:set_loop_status("Track")
        elseif loop_status == "Track" then
            self:set_loop_status("Playlist")
        elseif loop_status == "Playlist" then
            self:set_loop_status("None")
        end
    end

    if player ~= nil then
        awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " loop", function(stdout)
            set_loop_status(stdout)
        end)
    else
        set_loop_status(self._private.loop_status)
    end
end

function playerctl:set_position(position, player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " position " .. position)
    else
        awful.spawn.with_shell(self._private.cmd .. "position " .. position)
    end
end

function playerctl:set_shuffle(shuffle, player)
    shuffle = shuffle and "on" or "off"

    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " shuffle " .. shuffle)
    else
        awful.spawn.with_shell(self._private.cmd .. "shuffle " .. shuffle)
    end
end

function playerctl:cycle_shuffle(player)
    if player ~= nil then
        awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " shuffle", function(stdout)
            local shuffle = stdout == "on" and true or false
            self:set_shuffle(not self._private.shuffle)
        end)
    else
        self:set_shuffle(not self._private.shuffle)
    end
end

function playerctl:set_volume(volume, player)
    if player ~= nil then
        awful.spawn.with_shell("playerctl --player=" .. player .. " volume " .. volume)
    else
        awful.spawn.with_shell(self._private.cmd .. "volume " .. volume)
    end
end

local function emit_player_metadata(self)
    local metadata_cmd = self._private.cmd .. "metadata --format 'title_{{title}}artist_{{artist}}art_url_{{mpris:artUrl}}player_name_{{playerName}}album_{{album}}' -F"

    awful.spawn.with_line_callback(metadata_cmd, {
        stdout = function(line)
            local title = gstring.xml_escape(line:match('title_(.*)artist_')) or ""
            local artist = gstring.xml_escape(line:match('artist_(.*)art_url_')) or ""
            local art_url = line:match('art_url_(.*)player_name_') or ""
            local player_name = line:match('player_name_(.*)album_') or ""
            local album = gstring.xml_escape(line:match('album_(.*)')) or ""

            art_url = art_url:gsub('%\n', '')
            if player_name == "spotify" then
                art_url = art_url:gsub("open.spotify.com", "i.scdn.co")
            end

            if self._private.metadata_timer
                and self._private.metadata_timer.started
            then
                self._private.metadata_timer:stop()
            end

            self._private.metadata_timer = gtimer {
                timeout = self.debounce_delay,
                autostart = true,
                single_shot = true,
                callback = function()
                    if title and title ~= "" then
                        if art_url ~= "" then
                            local art_path = os.tmpname()
                            helpers.filesystem.save_image_async_curl(art_url, art_path, function()
                                self:emit_signal("metadata", title, artist, art_path, album, player_name)
                                capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, art_path)
                            end)
                        else
                            self:emit_signal("metadata", title, artist, "", album, player_name)
                            capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, "")
                        end
                    else
                        self:emit_signal("no_players")
                        capi.awesome.emit_signal("bling::playerctl::no_players")
                    end
                end
            }

            collectgarbage("collect")
        end,
    })
end

local function emit_player_position(self)
    local position_cmd = self._private.cmd .. "position"
    local length_cmd = self._private.cmd .. "metadata mpris:length"

    awful.widget.watch(position_cmd, self.interval, function(_, interval)
        awful.spawn.easy_async_with_shell(length_cmd, function(length)
            local length_sec = tonumber(length) -- in microseconds
            local interval_sec = tonumber(interval) -- in seconds
            if length_sec and interval_sec then
                if interval_sec >= 0 and length_sec > 0 then
                    self:emit_signal("position", interval_sec, length_sec / 1000000)
                    capi.awesome.emit_signal("bling::playerctl::position", interval_sec, length_sec / 1000000)
                end
            end
        end)
        collectgarbage("collect")
    end)
end

local function emit_player_playback_status(self)
    local status_cmd = self._private.cmd .. "status -F"

    awful.spawn.with_line_callback(status_cmd, {
        stdout = function(line)
            if line:find("Playing") then
                self:emit_signal("playback_status", true)
                capi.awesome.emit_signal("bling::playerctl::status", true)
            else
                self:emit_signal("playback_status", false)
                capi.awesome.emit_signal("bling::playerctl::status", false)
            end
        end,
    })
end

local function emit_player_volume(self)
    local volume_cmd = self._private.cmd .. "volume -F"

    awful.spawn.with_line_callback(volume_cmd, {
        stdout = function(line)
            self:emit_signal("volume", tonumber(line))
        end,
    })
end

local function emit_player_loop_status(self)
    local loop_status_cmd = self._private.cmd .. "loop -F"

    awful.spawn.with_line_callback(loop_status_cmd, {
        stdout = function(line)
            self._private.loop_status = line
            self:emit_signal("loop_status", line:lower())
        end,
    })
end

local function emit_player_shuffle(self)
    local shuffle_cmd = self._private.cmd .. "shuffle -F"

    awful.spawn.with_line_callback(shuffle_cmd, {
        stdout = function(line)
            if line:find("On") then
                self._private.shuffle = true
                self:emit_signal("shuffle", true)
            else
                self._private.shuffle = false
                self:emit_signal("shuffle", false)
            end
        end,
    })
end

local function parse_args(self, args)
    if args.player then
        self._private.cmd = self._private.cmd .. "--player="

        if type(args.player) == "string" then
            self._private.cmd = self._private.cmd .. args.player .. " "
        elseif type(args.player) == "table" then
            for index, player in ipairs(args.player) do
                self._private.cmd = self._private.cmd .. player
                if index < #args.player then
                    self._private.cmd = self._private.cmd .. ","
                else
                    self._private.cmd = self._private.cmd .. " "
                end
            end
        end
    end

    if args.ignore then
        self._private.cmd = self._private.cmd .. "--ignore-player="

        if type(args.ignore) == "string" then
            self._private.cmd = self._private.cmd .. args.ignore .. " "
        elseif type(args.ignore) == "table" then
            for index, player in ipairs(args.ignore) do
                self._private.cmd = self._private.cmd .. player
                if index < #args.ignore then
                    self._private.cmd = self._private.cmd .. ","
                else
                    self._private.cmd = self._private.cmd .. " "
                end
            end
        end
    end
end

local function new(args)
    args = args or {}

    local ret = gobject{}
    gtable.crush(ret, playerctl, true)

    ret.interval = args.interval or beautiful.playerctl_position_update_interval or 1
    ret.debounce_delay = args.debounce_delay or beautiful.playerctl_debounce_delay or 0.35

    ret._private = {}
    ret._private.metadata_timer = nil
    ret._private.cmd = "playerctl "
    parse_args(ret, args)

    emit_player_metadata(ret)
    emit_player_position(ret)
    emit_player_playback_status(ret)
    emit_player_volume(ret)
    emit_player_loop_status(ret)
    emit_player_shuffle(ret)

    return ret
end

function playerctl.mt:__call(...)
    return new(...)
end

-- On startup instead of on playerctl object init to make it
-- possible to have more than one of these running
awful.spawn.with_shell("killall playerctl")

return setmetatable(playerctl, playerctl.mt)