-- Volume Control
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local naughty = require("naughty")

-- compatibility fallbacks for 3.5:
local timer = gears.timer or timer
local spawn = awful.spawn or awful.util.spawn
local watch = awful.spawn and awful.spawn.with_line_callback

local function exec(command, callback)
    awful.spawn.easy_async(command, callback or function() end)
end


------------------------------------------
-- Private utility functions
------------------------------------------

local function substitute(template, context)
  if type(template) == "string" then
    return (template:gsub("%${([%w_]+)}", function(key)
      return tostring(context[key] or "default")
    end))
  else
    -- function / functor:
    return template(context)
  end
end

local function new(self, ...)
    local instance = setmetatable({}, {__index = self})
    return instance:init(...) or instance
end

local function class(base)
    return setmetatable({new = new}, {
        __call = new,
        __index = base,
    })
end

------------------------------------------
-- Volume control interface
------------------------------------------

local vcontrol = class()

function vcontrol:init(args)
    self.callbacks = {}
    self.cmd = "amixer"
    self.device = args.device or nil
    self.cardid  = args.cardid or nil
    self.channel = args.channel or "Master"
    self.step = args.step or '5%'

    self.timer = timer({ timeout = args.timeout or 0.5 })
    self.timer:connect_signal("timeout", function() self:get() end)
    self.timer:start()

    if args.listen and watch then
        self.listener = watch({'stdbuf', '-oL', 'alsactl', 'monitor'}, {
          stdout = function(line) self:get() end,
        })
        awesome.connect_signal("exit", function()
            awesome.kill(self.listener, awesome.unix_signal.SIGTERM)
        end)
    end
end

function vcontrol:register(callback)
    if callback then
        table.insert(self.callbacks, callback)
    end
end

function vcontrol:action(action)
    if self[action]                   then self[action](self)
    elseif type(action) == "function" then action(self)
    elseif type(action) == "string"   then spawn(action)
    end
end

function vcontrol:update(status)
    local volume = status:match("(%d?%d?%d)%%")
    local state  = status:match("%[(o[nf]*)%]")
    if volume and state then
        local volume = tonumber(volume)
        local state = state:lower()
        local muted = state == "off"
        for _, callback in ipairs(self.callbacks) do
            callback(self, {
                volume = volume,
                state = state,
                muted = muted,
                on = not muted,
            })
        end
    end
end

function vcontrol:mixercommand(args, callback)
    local args = awful.util.table.join(
      {self.cmd},
      (self.cmd == "amixer") and {"-M"} or {},
      self.device and {"-D", self.device} or {},
      self.cardid and {"-c", self.cardid} or {},
      args)
    exec(args, callback or function(output)
        self:update(output)
    end)
end

function vcontrol:get()
    self:mixercommand({ "get", self.channel })
end

function vcontrol:up()
    self:mixercommand({ "set", self.channel, self.step .. "+" })
end

function vcontrol:down()
    self:mixercommand({ "set", self.channel, self.step .. "-" })
end

function vcontrol:toggle()
    self:mixercommand({ "set", self.channel, "toggle" })
end

function vcontrol:mute()
    self:mixercommand({ "set", "Master", "mute" })
end

function vcontrol:unmute()
    self:mixercommand({ "set", "Master", "unmute" })
end

function vcontrol:list_sinks(callback)
    exec("env LC_ALL=C pactl list sinks", function(output)
        local sinks = {}
        local sink
        for line in output:gmatch("[^\r\n]+") do
            if line:match("Sink #%d+") then
                sink = {}
                table.insert(sinks, sink)
            else
                local k, v = line:match("^%s*(%S+):%s*(.-)%s*$")
                if k and v then sink[k:lower()] = v end
            end
        end
        callback(sinks)
    end)
end

function vcontrol:set_default_sink(name, callback)
    exec({"pactl set-default-sink", name}, callback)
end

------------------------------------------
-- Volume control widget
------------------------------------------

-- derive so that users can still call up/down/mute etc
local vwidget = class(vcontrol)

function vwidget:init(args)
    vcontrol.init(self, args)

    self.lclick = args.lclick or "toggle"
    self.mclick = args.mclick or "pavucontrol"
    self.rclick = args.rclick or self.show_menu

    self.font = args.font        or nil
    self.widget = args.widget    or (self:create_widget(args)  or self.widget)
    self.tooltip = args.tooltip and (self:create_tooltip(args) or self.tooltip)

    self:register(args.callback or self.update_widget)
    self:register(args.tooltip and self.update_tooltip)

    self.widget:buttons(awful.util.table.join(
        awful.button({}, 1, function() self:action(self.lclick) end),
        awful.button({}, 2, function() self:action(self.mclick) end),
        awful.button({}, 3, function() self:action(self.rclick) end),
        awful.button({}, 4, function() self:up() end),
        awful.button({}, 5, function() self:down() end)
    ))

    self:get()
end

-- text widget
function vwidget:create_widget(args)
    self.widget_text = args.widget_text or {
        on  = '% 3d%% ',
        off = '% 3dM ',
    }
    self.widget = wibox.widget.textbox()
    if self.font then
      self.widget.font = self.font
    end
end

function vwidget:create_menu(callback)
    self:list_sinks(function(sinks)
        local sinks_submenu = {}
        for i, sink in ipairs(sinks) do
            table.insert(sinks_submenu, {sink.description, function()
                self:set_default_sink(sink.name)
            end})
        end
        callback(awful.menu { items = {
            { "mute", function() self:mute() end },
            { "unmute", function() self:unmute() end },
            { "Default Sink", sinks_submenu },
            { "pavucontrol", function() self:action("pavucontrol") end },
        } })
    end)
end

function vwidget:show_menu()
    if self.menu then
        self.menu:hide()
    else
        self:create_menu(function(menu)
            self.menu = menu
            self.menu:show()
            self.menu.wibox:connect_signal("property::visible", function()
                self.menu = nil
            end)
        end)
    end
end

function vwidget:update_widget(setting)
    self.widget:set_markup(
        self.widget_text[setting.state]:format(setting.volume))
end

-- tooltip
function vwidget:create_tooltip(args)
    self.tooltip_text = args.tooltip_text or [[
Volume: ${volume}% ${state}
Channel: ${channel}
Device: ${device}
Card: ${card}]]
    self.tooltip = args.tooltip and awful.tooltip({objects={self.widget}})
end

function vwidget:update_tooltip(setting)
    self.tooltip:set_text(substitute(self.tooltip_text, {
        volume  = setting.volume,
        state   = setting.state,
        device  = self.device,
        card    = self.card,
        channel = self.channel,
    }))
end

-- provide direct access to the control class
vwidget.control = vcontrol
return vwidget
