-- (c) linuxkitty, use this file in any way you watn, but please keep my name if you repost it
-- Standard awesome library
gears = require("gears")
awful = require("awful")
require("awful.autofocus")
-- Widget and layout library
wibox = require("wibox")
-- Theme handling library
beautiful = require("beautiful")
-- Notification library
naughty = require("naughty")

--------------------------
-- Definitions          --
--------------------------

function debug_notify(object)
	naughty.notify({
		 preset = naughty.config.presets.critical,
		 text = gears.debug.dump_return(object) })
end

local terminal = "x-terminal-emulator"
local editor = os.getenv("EDITOR") or "vi"
local editor_cmd = terminal .. " -e " .. editor
local modkey = "Mod4" -- Mod4 is "Windows" key
local temp_group_name = "t"

local globalkeys = {} -- mutable
local clientkeys = {} -- mutable
theme = {} -- mutable
local tags_table = {} -- mutable, key is tag name

local current_tag = nil -- mutable
local last_tag = nil	-- mutable

function table_range_inclusive(start, finish)
	local ret = {}
	for i = start, finish do
		ret[i] = i
	end
	return ret
end

function table_get_length(t)
	local count = 0
	for _ in pairs(t) do count = count + 1 end
	return count
end

function table_map(t, fn)
	local ret = {}
	for i, v in pairs(t) do
		ret[i] = fn(v)
	end
	return ret
end

function list_table_init(table_that_is_also_a_list)
	local ret = {}
	local last = nil
	local started = false
	for _, v in pairs(table_that_is_also_a_list)  do
		if started
		then table.insert(ret, last)
		else started = true
		end
		last = v
	end
	return ret
end

function list_table_last(table_that_is_also_a_list)
	local last = nil
	for _, v in pairs(table_that_is_also_a_list) do
		last = v
	end
	return last
end

function join_two_tables(a, b)
	local ret = {}
	for i, v in pairs(a) do
		table.insert(ret, v)
	end
	for i, v in pairs(b) do
		table.insert(ret, v)
	end
	return ret
end

function make_default_modifiers(mods)
	if type(mods) == "table"
	then return join_two_tables({modkey}, mods)
	else return {modkey, mods}
	end
end

function make_key(key)

	local key_code = key[1]
	local modifiers = nil
	local actual_key = nil

	if type(key_code) == "string"
	then
		actual_key = key_code
		modifiers = { modkey, nil }
	else
		actual_key = list_table_last(key_code)
		modifiers = make_default_modifiers(list_table_init(key_code))
	end

	local awfulkey = awful.key(modifiers, actual_key, key[2], key[3])
	local descriptor = {
		modifiers = modifiers,
		actual_key = actual_key,
		fn = key[2],
		desc = key[3],
		awfulkey = awfulkey,
	}

	return descriptor
end

function table_any(t, predicate)
	for i, v in pairs(t) do
		if predicate(v) then return true end
	end
	return false
end

function table_equals1(a, b)
	if a == nil or b == nil
	then return a == b
	end

	for i, v in pairs(a) do
		if b[i] ~= v then return false end
	end
	for i, v in pairs(b) do
		if a[i] ~= v then return false end
	end
	return true
end

function key_exists_in(t, pre_raw, c)
	local pre = make_default_modifiers(pre_raw)

	function predicate(val)
		return (table_equals1(val.modifiers, pre) and (val.actual_key == c))
	end

	return table_any(t, predicate)
end

function append_table_key(t, key)
	table.insert(t, make_key(key))
	return t
end

function append_table_keys(t, keys)
	for i, k in pairs(keys) do
		append_table_key(t, k)
	end
	return t
end

function append_global_key(key)
	return append_table_key(globalkeys, key)
end

function append_global_keys(keys)
	return append_table_keys(globalkeys, keys)
end

function client_toggle_fullscreen(c)
	c.fullscreen = not c.fullscreen
	c:raise()
end

function key_table_to_awful_key_table(t)
	function get_awful(v)
		return v.awfulkey
	end

	-- Note: below doesn't work on some versions of awesome. Don't wanna know why
	-- return gears.table.join(unpack(table_map(t, get_awful)))

	local ret = {}
	for _, k in pairs(t) do
		local a = get_awful(k)
		for _, v in pairs(a) do
			table.insert(ret, v)
		end
	end

	return ret
end

function minimize_client(c)
	c.minimized = true
	adjust_border_sizes()
end

function toggle_floating_client(c)
	awful.client.floating.toggle(c)
	adjust_border_sizes()
end

local digits = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }
local alphabet = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" }
local alphanum = join_two_tables(digits, alphabet)
local sub_desktop_names = { "1", "2", "3", "4", "5", "6", "7", "8", "9" }
local group_names = alphabet

local special_keys =
	{ "equal", "minus", "Up", "Down", "Left", "Right", "KP_Add", "KP_Subtract", "Delete", "Print", "Return" }

local groups_table =
	(function ()
		local ret = {}
		for i, a in pairs(group_names) do
			local obj = {
				name = a,
				last_subdesktop = "1",
			}
			ret[a] = obj
		end
		return ret
	end)()

local taglist =
	(function ()
		local ret = {}
		for i, a in pairs(group_names) do
			for k, v in pairs(sub_desktop_names) do
				if v == "1" then
					table.insert(ret, a)
				else
					table.insert(ret, a .. "/" .. v)
				end
			end
		end
		return ret
	end)()

function hide_panel()
	local screen = awful.screen.focused()
	local wib = scree.mywibox
	wib.visible = false
end

function show_panel()
	local screen = awful.screen.focused()
	local wib = scree.mywibox
	wib.visible = true
end

function on_screen_history_update()
	local screen = awful.screen.focused()
	local new = screen.selected_tag

	if not (current_tag == new) then
		last_tag = current_tag
		current_tag = new
	end

	local name = get_current_tag_name()
	local group_name, desktop_name = destructure_tag_name(name)
	local g = get_group_by_name(group_name)
	g.last_subdesktop = desktop_name
end

function foreach_screen_fn(s)
	local mytextclock = wibox.widget.textclock()

	awful.tag(taglist, s, awful.layout.layouts[1])

	init_tags_table_on_screen(s)

	s:connect_signal("tag::history::update", on_screen_history_update)

	-- Create a taglist widget
	s.mytaglist = awful.widget.taglist(s, awful.widget.taglist.filter.noempty, taglist_buttons)

	-- Create a tasklist widget
	s.mytasklist = awful.widget.tasklist(s, awful.widget.tasklist.filter.focused, tasklist_buttons)

	-- Create the wibox
	s.mywibox = awful.wibar({ position = "top", screen = s })

	-- Add widgets to the wibox
	s.mywibox:setup {
	  layout = wibox.layout.align.horizontal,
	  { -- Left widgets
		 layout = wibox.layout.fixed.horizontal,
		 s.mytaglist,
	  },
	  s.mytasklist, -- Middle widget
	  { -- Right widgets
		 layout = wibox.layout.fixed.horizontal,
		 wibox.widget.systray(),
		 mytextclock,
	  },
	}
end

function spawn_check(ret, onerror)
	if onerror == nil then
		onerror = function()
		naughty.notify({ preset = naughty.config.presets.critical,
						  title = "Spawn error",
						  text = ret })
		end
	end

	local t = tostring(type(ret))
	if t == 'number' then
		return true
	elseif t == 'string' then
		onerror(ret)
		return false
	else
		naughty.notify({ preset = naughty.config.presets.critical,
						title = "Spawn error: unknown return type",
						text = t })
		return false
	end
end

function spawn_normal(command)
	spawn_check(awful.spawn(command))
end

function spawn_with_shell_cb(stdout, stderr, exitreason, exitcode)
	if exitcode ~= 0 then
		naughty.notify({ preset = naughty.config.presets.critical,
						title = "Shell spawn failed with: " .. tostring(exitcode),
						text = stderr })
	end
end

function spawn_with_shell(command)
	spawn_check(awful.spawn.easy_async_with_shell(command, spawn_with_shell_cb))
end

function append_dynamic_keys()
	function onerror (ret)
		naughty.notify({ preset = naughty.config.presets.low,
						title = "Spawn dynamic error",
						text = ret })
	end

	function spawn_dynamic(pre, c)
		local prefix = ""
		if pre ~= nil
		then prefix = pre .. "-"
		end

		local cmd = "awesomewm-key-" .. prefix .. c
		local lower = cmd:lower()

		return function()
			spawn_check(awful.spawn(lower, { focus = true }), onerror)
		end
	end

	local keys = join_two_tables(alphanum, special_keys)

	function check(pre, c)
		return not (key_exists_in(globalkeys, pre, c) or key_exists_in(clientkeys, pre, c))
	end

	function add(k, pre, c)
		if check(pre, c) then
			append_global_key({k, spawn_dynamic(pre, c)})
		end
	end

	for _,c in pairs(keys) do
		add(c, nil, c)
		add({"Control", c}, "Control", c)
		add({"Shift", c}, "Shift", c)
	end
end

function on_geometry_hook(client)
	client.maximized = false
end

function append_sub_desktop_keys()
	local r = table_range_inclusive(1, 9)

	function sw(i)
		function switcher()
			switch_to_sub_desktop(i)
		end
		return {tostring(i), switcher}
	end
	local smap = table_map(r, sw)
	append_global_keys(smap)

	function mv(i)
		function mover(c)
			move_to_sub_desktop(c, i)
		end
		return {{"Shift", tostring(i)}, mover}
	end
	local mmap = table_map(r, mv)
	append_table_keys(clientkeys, mmap)
end

function append_groups_keys()
	local r = alphabet

	function sw(i)
		function switcher()
			switch_to_group(i)
		end
		return {{"Control", i}, switcher}
	end
	local gmap = table_map(r, sw)
	append_global_keys(gmap)

	function mv(i)
		function mover(c)
			move_to_group(c, i)
		end
		return {{"Control", "Shift", i}, mover}
	end
	local cmap = table_map(r, mv)
	append_table_keys(clientkeys, cmap)
end

function on_unmanage_hook(t)
	local cur = get_current_tag()
	adjust_border_sizes()
end

function on_manage_hook(c)
	adjust_border_sizes()

	-- Set the windows at the slave,
	-- i.e. put it at the end of others instead of setting it master.
	-- if not awesome.startup then awful.client.setslave(c) end
	if awesome.startup and
		not c.size_hints.user_position
	and not c.size_hints.program_position then
		-- Prevent clients from being unreachable after screen count changes.
		awful.placement.no_offscreen(c)
	end
end

function on_mouse_enter_hook(c)
	if (awful.layout.get(c.screen) ~= awful.layout.suit.magnifier
		and awful.client.focus.filter(c)) then
		client.focus = c
	end
end

function count_visible_clients_on(t) -- t is tag
	local cl = t:clients()
	local re = 0
	local list = {}
	if cl ~= nil then
		for i, c in pairs(cl) do
			if not c.minimized and not c.floating then
				re = re + 1
				table.insert(list, c)
			end
		end
	end
	return re, list
end

function should_be_borderless(t) -- t is tag
	if t.layout == awful.layout.suit.max
	then return true, (t:clients())
	else
		local count, list = count_visible_clients_on(t)
		return (count <= 1), list
	end
end

function client_remove_borders(c)
	c.border_width = 0
end

function client_add_borders(c)
	c.border_width = beautiful.border_width
end

function adjust_border_sizes()
	local should_be, list = should_be_borderless(get_current_tag())
	if should_be
	then
		for i, c in pairs(list) do
			client_remove_borders(c)
		end
	else
		for i, c in pairs(list) do
			client_add_borders(c)
		end
	end
end

function check_floating(c)
	if (not c.ontop) and c.floating and c.first_tag ~= nil and c.first_tag.layout ~= awful.layout.suit.floating then
		c.ontop = true
	else
		c.ontop = false
	end
end

function on_focus_hook(c)
	c.border_color = beautiful.border_focus
	check_floating(c)
end

function on_unfocus_hook(c)
	c.border_color = beautiful.border_normal
	check_floating(c)
end

function restore_minimized()
	local c = awful.client.restore()
	-- Focus restored client
	if c then
		client.focus = c
		c:raise()
	end

	adjust_border_sizes()
end

function tag_get_left_name(tag)
	local name = tag.name
	local group_name, desktop_name = destructure_tag_name(name)
	local index = tonumber(desktop_name)
	local left_index = index - 1
	if left_index < 1
	then left_index = 9
	end

	local left = tostring(left_index)
	return group_make_desktop_fullname(group_name, left)
end

function tag_get_left(tag)
	local name = tag_get_left_name(tag)
	local tag = get_tag_by_name(name)
	return tag
end

function tag_get_right_name(tag)
	local name = tag.name
	local group_name, desktop_name = destructure_tag_name(name)
	local index = tonumber(desktop_name)
	local right_index = index + 1
	if right_index > 9
	then right_index = 1
	end

	local right = tostring(right_index)
	return group_make_desktop_fullname(group_name, right)
end

function tag_get_right(tag)
	local name = tag_get_right_name(tag)
	local tag = get_tag_by_name(name)
	return tag
end

function client_move_to_tag(c, target)
	c:move_to_tag(target)
	adjust_border_sizes()
end

function client_move_left(c)
	local t = c.first_tag or nil
	if t == nil then return end
	local tag = tag_get_left(t)
	client_move_to_tag(c, tag)
	switch_to_tag(tag)
end

function client_move_right(c)
	local t = c.first_tag or nil
	if t == nil then return end
	local tag = tag_get_right(t)
	client_move_to_tag(c, tag)
	switch_to_tag(tag)
end

function view_left_tag()
	local t = get_current_tag()
	local tag = tag_get_left(t)
	switch_to_tag(tag)
end

function view_right_tag()
	local t = get_current_tag()
	local tag = tag_get_right(t)
	switch_to_tag(tag)
end

function switch_to_last_tag()
	switch_to_tag(last_tag)
end

function switch_to_last_window()
	awful.client.focus.history.previous()
	if client.focus then
		client.focus:raise()
	end
end

function destructure_tag_name(tag_name)
	local len = string.len(tag_name)
	local group = string.sub(tag_name, 1, 1)
	local sub_desktop = "1"

	if len == 3
	then sub_desktop = string.sub(tag_name, 3, 3)
	end

	return group, sub_desktop
end

function get_current_tag()
	return current_tag
end

function get_current_tag_name()
	return get_current_tag().name
end

function get_tag_by_name(name)
	local tag_obj = tags_table[name]
	return tag_obj.body
end

function get_current_group_name()
	local tag_name = get_current_tag_name()
	local group, sub_desktop = destructure_tag_name(tag_name)
	return group
end

function group_last_sub_fullname(group)
	return group_make_desktop_fullname(group.name, group.last_subdesktop)
end

function group_last_tag(group)
	local fullname = group_last_sub_fullname(group)
	return get_tag_by_name(fullname)
end

function group_get_subdesktops_as_tags(group)
	local group_name = group.name
	local ret = {}
	for i, d in pairs(sub_desktop_names) do
		local fullname = group_make_desktop_fullname(group_name, d)
		local tag = get_tag_by_name(fullname)
		ret[i] = tag
	end
	return ret
end

function get_group_by_name(group_name)
	return groups_table[group_name]
end

function group_get_free_tag(group)
	local all_tags = group_get_subdesktops_as_tags(group)
	for i, t in pairs(all_tags) do
		if #t:clients() == 0 then
			return t
		end
	end
	return nil
end

function switch_to_tag(tag)
	tag:view_only()
	adjust_border_sizes()
end

function switch_to_tag_name(tag_name)
	local tag = get_tag_by_name(tag_name)
	switch_to_tag(tag)
end

function switch_to_sub_desktop(i)
	local group_name   = get_current_group_name()
	local desktop_name = tostring(i)
	local fullname     = group_make_desktop_fullname(group_name, desktop_name)
	switch_to_tag_name(fullname)
end

function move_to_sub_desktop(client, i)
	local group_name   = get_current_group_name()
	local desktop_name = tostring(i)
	local fullname     = group_make_desktop_fullname(group_name, desktop_name)
	local tag          = get_tag_by_name(fullname)
	client_move_to_tag(client, tag)
	switch_to_tag(tag)
end

function group_make_desktop_fullname(group_name, desktop_name)
	if desktop_name == "1"
	then return group_name
	else return (group_name .. "/" .. desktop_name)
	end
end

function group_get_last_dekstop_fullname(group)
	local name = group.last_subdesktop
	return group_make_desktop_fullname(group.name, name)
end

function group_name_get_last_dekstop_fullname(name)
	local g = get_group_by_name(name)
	return group_get_last_dekstop_fullname(g)
end

function switch_to_group(name)
	local last_fullname = group_name_get_last_dekstop_fullname(name)
	switch_to_tag_name(last_fullname)
end

function move_to_group_noswitch(client, group_name)
	local last_fullname = group_name_get_last_dekstop_fullname(group_name)
	local tag = get_tag_by_name(last_fullname)
	client_move_to_tag(client, tag)
	return tag
end

function move_to_group(client, group_name)
	local tag = move_to_group_noswitch(client, group_name)
	return switch_to_tag(tag)
end

function reset_theme_defaults()
	theme.font = "monospace 15"

	-- Note: breaks notifications on some awesome versions
	-- theme.bg_normal	 = "#000000"

	theme.bg_focus      = theme.bg_normal
	theme.bg_urgent     = "#FF0000"
	theme.bg_minimize   = "#000055"
	theme.bg_minimize   = "#FFFFFF"
	theme.bg_systray    = theme.bg_normal

	theme.taglist_bg_normal     = theme.bg_normal
	theme.taglist_bg_focus      = theme.bg_normal
	theme.taglist_bg_occupied   = theme.taglist_bg_normal
	theme.taglist_fg_focus      = "#FFFF00"
	theme.taglist_fg_normal     = theme.bg_normal
	theme.taglist_fg_occupied   = "#FFFFFF"

	theme.tasklist_fg_normal = "#007700"
	theme.tasklist_fg_focus  = theme.tasklist_fg_normal

	theme.fg_normal   = "#aaaaaa"
	theme.fg_focus    = "#ffffff"
	theme.fg_urgent   = "#ffffff"
	theme.fg_minimize = "#999999"

	theme.useless_gap   = 0
	theme.border_width  = 3
	theme.border_normal = "#000000"
	theme.border_focus  = "#0000FF"
	theme.border_marked = "#91231c"
end

function reset_theme()
	theme = {}
	reset_theme_defaults()
end

function init_theme()
	beautiful.init(theme)
end

function reinit_theme()
	reset_theme()
	init_theme()
end

-- depends on screen connect
function init_tags_table_on_screen(screen)
	local tags = screen.tags
	local index = screen.index

	tags_table[index] = {}

	for i, t in pairs(tags) do
	  local name = t.name
	  local group_name, desktop_name = destructure_tag_name(name)
	  local group = get_group_by_name(group_name)
	  local obj = {
		 name	= name,
		 body	= t,
		 group  = group,
		 screen = screen,
		 screen_index = screen_index,
	  }
	  tags_table[name] = obj
	end
end

local clientbuttons = awful.util.table.join(
	awful.button({ }, 1, function (c) client.focus = c; c:raise() end),
	awful.button({ modkey }, 1, awful.mouse.client.move),
	awful.button({ modkey }, 3, awful.mouse.client.resize))

function get_temporary_group()
	return get_group_by_name(temp_group_name)
end

function switch_to_last_temporary_tag()
	local temp_group        = get_temporary_group()
	local tag               = group_last_tag(temp_group)
	switch_to_tag(tag)
end

function switch_to_new_temporary_tag()
	local temp_group        = get_temporary_group()
	local free_tag          = group_get_free_tag(temp_group)
	switch_to_tag(free_tag)
end

function move_to_new_temporary_tag(client)
	local temp_group        = get_temporary_group()
	local tag               = group_get_free_tag(temp_group)
	client_move_to_tag(client, tag)
	switch_to_tag(tag)
end

function toggle_keep_on_top(client)
	client.ontop = not client.ontop
end

function kill_client(client)
	client:kill()
end

function scroll_layout()
	awful.layout.inc(1)
	adjust_border_sizes()
end

--------------------------
-- Procedural           --
--------------------------

append_table_keys(clientkeys,
	{
		{{"Shift", "w"}, kill_client},
		{"F11", client_toggle_fullscreen},
		{{"Shift", "f"}, toggle_floating_client},
		{"m", minimize_client},
		{{"Shift", "h"}, client_move_left},
		{{"Shift", "l"}, client_move_right},
		{"u", toggle_keep_on_top},
		{{"Shift", "a"}, move_to_new_temporary_tag},
	}
)

append_global_keys({
	  {"t", function () awful.spawn(terminal) end},
	  {{"Shift", "t"}, function () switch_to_new_temporary_tag() ; awful.spawn(terminal) end},
	  {"x", scroll_layout},
	  {"j", function () awful.client.focus.byidx( 1) end},
	  {"k", function () awful.client.focus.byidx(-1) end},
	  {{"Shift", "j"}, function () awful.client.swap.byidx( 1) end},
	  {{"Shift", "k"}, function () awful.client.swap.byidx(-1) end},
	  {"h", view_left_tag},
	  {"l", view_right_tag},
	  {"Tab", switch_to_last_tag},
	  {{"Shift", "Tab"}, switch_to_last_window},
	  {"]", function () awful.tag.incmwfact( 0.05) end},
	  {"[", function () awful.tag.incmwfact(-0.05) end},
	  {{"Shift", "m"}, restore_minimized},
	  {"a", switch_to_new_temporary_tag},
})

append_sub_desktop_keys()
append_groups_keys()
append_dynamic_keys()

--------------------------
-- Effects              --
--------------------------

reinit_theme()

local awful_clientkeys = key_table_to_awful_key_table(clientkeys)

-- All clients will match this rule.
awful.rules.rules = {
	{ rule = { },
		properties = {
			titlebars_enabled = false,
			border_width	  = 2,
			border_color	  = beautiful.border_normal,
			size_hints_honor  = false, -- fixes empty space at bottom of the screen
			keys			  = awful_clientkeys,
			buttons			= clientbuttons,
			screen			= awful.screen.preferred,
			placement		 = awful.placement.no_overlap + awful.placement.no_offscreen,
		},
	},
}

-- Table of layouts to cover with awful.layout.inc, order matters.
awful.layout.layouts = {
	awful.layout.suit.tile,
	awful.layout.suit.max,
}

awful.screen.connect_for_each_screen(foreach_screen_fn)

root.keys(key_table_to_awful_key_table(globalkeys));

client.connect_signal("manage", on_manage_hook)
client.connect_signal("unmanage", on_unmanage_hook)

-- Enable sloppy focus, so that focus follows mouse.
client.connect_signal("mouse::enter", on_mouse_enter_hook)

client.connect_signal("property::geometry", on_geometry_hook)

client.connect_signal("focus", on_focus_hook)
client.connect_signal("unfocus", on_unfocus_hook)