-- Grab environment
local setmetatable = setmetatable
local pairs = pairs
local ipairs = ipairs
local table = table
local string = string
local awful = require("awful")
local wibox = require("wibox")
local beautiful = require("beautiful")
local timer = require("gears.timer")

local modutil = require("flex.util")
local basetag = require("flex.gauge.tag")
local tooltip = require("flex.float.tooltip")

-- Initialize tables and vars for module
local taglist = { filter = {}, mt = {}, queue = setmetatable({}, { __mode = "k" }) }

-- Generate default theme vars
local function default_style()
	local style = {
		tag = {},
		widget = basetag.rosybrown.new,
		show_tip = false,
		timeout = 0.05,
		separator = nil,
	}
	return modutil.table.merge(style, modutil.table.check(beautiful, "widget.taglist") or {})
end

-- Find all taglist vivsible clients
--------------------------------------------------------------------------------
local function visible_clients(t, ignored)
	local clients = {}

	for _, c in pairs(t:clients()) do
		local hidden = c.hidden
			or c.type == "splash"
			or c.type == "dock"
			or c.type == "desktop"
			or (c.class and awful.util.table.hasitem(ignored, c.class))

		if not hidden then
			table.insert(clients, c)
		end
	end

	return clients
end

-- Support functions
-- Get info about tag
local function get_state(t, ignored)
	local state = { focus = false, urgent = false, list = {} }
	local client_list = visible_clients(t, ignored)
	local client_count = 0

	for _, c in pairs(client_list) do
		state.focus = state.focus or client.focus == c
		state.urgent = state.urgent or c.urgent
		if not c.skip_taskbar then
			client_count = client_count + 1
			table.insert(state.list, { focus = client.focus == c, urgent = c.urgent, minimized = c.minimized })
		end
	end

	state.active = t.selected
	state.occupied = client_count > 0 and not (client_count == 1 and state.focus)
	state.text = string.upper(t.name)
	state.layout = awful.tag.getproperty(t, "layout")

	return state
end

-- Generate tooltip string
local function make_tip(t)
	return string.format("%s (%d apps)", t.name, #(t:clients()))
end

-- Find all tag to be shown
local function filtrate_tags(screen, filter)
	local tags = {}
	for _, t in ipairs(screen.tags) do
		if not awful.tag.getproperty(t, "hide") and filter(t) then
			table.insert(tags, t)
		end
	end
	return tags
end

-- Layout composition
local function base_pack(layout, widg, i, tags, style)
	layout:add(widg)
	if style.separator and i < #tags then
		layout:add(style.separator)
	end
end

-- Create a new taglist widget
function taglist.new(args, style)
	if not taglist.queue then
		taglist:init()
	end

	-- Initialize vars
	local cs = args.screen
	local layout = args.layout or wibox.layout.fixed.horizontal()
	local data = setmetatable({}, { __mode = "k" })
	local filter = args.filter or taglist.filter.all
	local hint = args.hint or make_tip
	local pack = args.pack or base_pack

	style = modutil.table.merge(default_style(), style or {})

	-- Set tooltip
	if not taglist.tp then
		taglist.tp = tooltip()
	end

	-- Update function
	local update = function(s)
		if s ~= cs then
			return
		end
		local tags = filtrate_tags(s, filter)

		-- Construct taglist
		layout:reset()
		for i, t in ipairs(tags) do
			local cache = data[t]
			local widg

			-- use existing widgets or create new one
			if cache then
				widg = cache
			else
				widg = style.widget(style.tag)
				if args.buttons then
					widg:buttons(modutil.base.buttons(args.buttons, t))
				end
				data[t] = widg

				-- set optional tooltip (what about removing?)
				if style.show_tip then
					taglist.tp:add_to_object(widg)
					widg:connect_signal("mouse::enter", function()
						taglist.tp:set_text(widg.tip)
					end)
				end
			end

			-- set tag state info to widget
			local state = get_state(t, args.ignored or {})
			widg:set_state(state)
			widg.tip = hint(t)

			-- add widget and separator to base layout
			pack(layout, widg, i, tags, style)
		end

		if taglist.queue[s] and taglist.queue[s].started then
			taglist.queue[s]:stop()
		end
	end

	-- Create timer to prevent multiply call
	taglist.queue[cs] = timer({ timeout = style.timeout })
	taglist.queue[cs]:connect_signal("timeout", function()
		update(cs)
	end)

	local uc = function(c)
		if taglist.queue[c.screen] then
			taglist.queue[c.screen]:again()
		end
	end
	local ut = function(t)
		if taglist.queue[t.screen] then
			taglist.queue[t.screen]:again()
		end
	end

	-- Signals setup
	local tag_signals = {
		"property::selected",
		"property::icon",
		"property::hide",
		"property::activated",
		"property::name",
		"property::screen",
		"property::index",
		"property::layout",
	}
	local client_signals = {
		"focus",
		"unfocus",
		"property::urgent",
		"tagged",
		"untagged",
		"unmanage",
	}

	for _, sg in ipairs(tag_signals) do
		awful.tag.attached_connect_signal(nil, sg, ut)
	end
	for _, sg in ipairs(client_signals) do
		client.connect_signal(sg, uc)
	end

	client.connect_signal("property::screen", function()
		update(cs)
	end) -- dirty

	update(cs) -- create taglist widget
	return layout -- return taglist widget
end

-- Filtering functions
-- @param t The awful.tag
-- @param args unused list of extra arguments
function taglist.filter.noempty(t) -- to include all nonempty tags on the screen.
	return #t:clients() > 0 or t.selected
end

function taglist.filter.all() -- to include all tags on the screen.
	return true
end

-- Config metatable to call taglist module as function
function taglist.mt:__call(...)
	return taglist.new(...)
end

return setmetatable(taglist, taglist.mt)
