-- Grab environment
local io = io
local table = table
local ipairs = ipairs
local string = string
local awful = require("awful")
local beautiful = require("beautiful")

local modutil = require("flex.util")
local gears = require("gears")

-- Initialize tables and vars for module
local dfparser = {}
local cache = {}

dfparser.terminal = "kitty"

local all_icon_folders = { "apps", "actions", "devices", "places", "categories", "status" }
local all_icon_sizes = {
	"128x128",
	"96x96",
	"72x72",
	"64x64",
	"48x48",
	"36x36",
	"32x32",
	"24x24",
	"22x22",
	"16x16",
	"scalable",
}

-- Generate default theme vars
local function default_style()
	local style = {
		icons = { custom_only = false, scalable_only = false, df_icon = nil, theme = nil },
		-- list of path to check desktop files
		desktop_file_dirs = {
			-- On NixOS systems
			"/run/current-system/sw/share/applications/", -- For nix applications
			"~/.local/state/home-manager/gcroots/current-home/home-path/share/applications/", -- For home-manager applications
			-- '/usr/share/applications/',
			-- '/usr/local/share/applications/',
			"~/.local/share/applications/",
			"~/.local/share/flatpak/exports/share/applications/", -- user installed flatpak apps
			"/var/lib/flatpak/exports/share/applications/", -- system wide installed flatpak apps
		},
		wm_name = nil,
	}

	return modutil.table.merge(style, modutil.table.check(beautiful, "service.dfparser") or {})
end

-- Support functions
-- Cache functions
local function check_cached(req)
	for k, v in pairs(cache) do
		local eq = #req == #k
		for ck, cv in pairs(k) do
			eq = eq and cv == req[ck]
		end
		if eq then
			return v
		end
	end
	return nil
end

-- Check whether the icon format is supported
local function is_format(icon_file, icon_formats)
	for _, f in ipairs(icon_formats) do
		if icon_file:match("%." .. f) then
			return true
		end
	end
	return false
end

-- Find all possible locations of the icon
local function all_icon_path(style)
	local icon_theme_paths = {}

	-- add user icon theme
	if style.theme then
		table.insert(icon_theme_paths, style.theme .. "/")
		local parent_theme = style.theme

		-- Look in parent icon themes
		while parent_theme do
			local index = parent_theme:find("/", 1, true)
			if index then
				parent_theme = parent_theme:sub(index + 1)
				table.insert(icon_theme_paths, parent_theme .. "/")
			else
				parent_theme = nil
			end
		end
	end

	-- add fallback theme
	if not style.custom_only then
		table.insert(icon_theme_paths, "/run/current-system/sw/share/icons/hicolor/")
	end -- Standard NixOS Path

	-- seach only svg icons if need
	local current_icon_sizes = style.scalable_only and { "scalable" } or all_icon_sizes

	-- form all avalible icon dirs
	local icon_path = {}

	for _, icon_theme_directory in ipairs(icon_theme_paths) do
		for _, size in ipairs(current_icon_sizes) do
			for _, folder in ipairs(all_icon_folders) do
				table.insert(icon_path, icon_theme_directory .. size .. "/" .. folder .. "/")
			end
		end
	end

	-- lowest priority fallbacks
	if not style.custom_only then
		table.insert(icon_path, "/run/current-system/sw/share/pixmaps/") -- Standard NixOS Path
		table.insert(icon_path, "/run/current-system/sw/share/icons/") -- Standard NixOS Path
	end

	return icon_path
end

-- Lookup an icon in different folders of the filesystem
-- @param icon_file Short or full name of the icon
-- @param style.default_icon Return if no icon was found
-- @param style.icon_theme Full path to custom icon theme
-- @param style.custom_only Seach only custom theme icons, ignore system
-- @param style.scalable_only Seach only svg type icons
-- @return full name of the icon
function dfparser.lookup_icon(icon_file, style)
	style = modutil.table.merge(default_style().icons, style or {})

	local df_icon
	if style.df_icon and gears.filesystem.file_readable(style.df_icon) then
		df_icon = style.df_icon
	end

	-- No icon name
	if not icon_file or icon_file == "" then
		return df_icon
	end

	-- Handle full path icons
	local icon_formats = style.scalable_only and { "svg" } or { "svg", "png", "gif" }

	if icon_file:sub(1, 1) == "/" then
		if is_format(icon_file, icon_formats) then
			return gears.filesystem.file_readable(icon_file) and icon_file or df_icon
		else
			icon_file = string.match(icon_file, "([%a%d%-]+)%.")
			if not icon_file then
				return df_icon
			end
		end
	end

	-- Find all possible locations to search
	local icon_path = all_icon_path(style)

	-- Icon searching
	for _, directory in ipairs(icon_path) do
		-- check if icon format specified and supported
		if is_format(icon_file, icon_formats) and awful.util.file_readable(directory .. icon_file) then
			return directory .. icon_file
		else
			-- check if icon format specified but not supported
			if
				string.match(icon_file, "%.")
				and not string.match(icon_file, "%w+%.%w+%.") -- ignore gnome naming style
				and not is_format(icon_file, icon_formats)
			then
				icon_file = string.match(icon_file, "[%a%d%-]+")
			end

			-- icon is probably specified without path and format,
			-- like 'firefox'. Try to add supported extensions to
			-- it and see if such file exists.
			for _, format in ipairs(icon_formats) do
				local possible_file = directory .. icon_file .. "." .. format
				if awful.util.file_readable(possible_file) then
					return possible_file
				end
			end
		end
	end

	return df_icon
end

-- Main parsing functions
-- Parse a .desktop file
-- @param file The .desktop file
-- @param style Arguments for dfparser.lookup_icon
-- @return A table with file entries
local function parse(file, style)
	local program = { show = true, file = file }
	local desktop_entry = false

	-- Parse the .desktop file.
	-- We are interested in [Desktop Entry] group only.
	for line in io.lines(file) do
		if not desktop_entry and line == "[Desktop Entry]" then
			desktop_entry = true
		else
			if line:sub(1, 1) == "[" and line:sub(-1) == "]" then
				-- A declaration of new group - stop parsing
				break
			end

			-- Grab the values
			for key, value in line:gmatch("(%w+)%s*=%s*(.+)") do
				program[key] = value
			end
		end
	end

	-- In case [Desktop Entry] was not found
	if not desktop_entry then
		return nil
	end

	-- Don't show program if NoDisplay attribute is false
	if program.NoDisplay and string.lower(program.NoDisplay) == "true" then
		program.show = false
	end

	-- Only show the program if there is no OnlyShowIn attribute
	-- or if it's equal to given wm name
	if program.OnlyShowIn ~= nil and style.wm_name and not program.OnlyShowIn:match(style.wm_name) then
		program.show = false
	end

	-- Look up for a icon.
	if program.Icon then
		program.icon_path = dfparser.lookup_icon(program.Icon, style.icons)
	end

	-- Split categories into a table. Categories are written in one
	-- line separated by semicolon.
	if program.Categories then
		program.categories = {}

		for category in program.Categories:gmatch("[^;]+") do
			table.insert(program.categories, category)
		end
	end

	if program.Exec then
		-- Substitute Exec special codes as specified in
		-- http://standards.freedesktop.org/desktop-entry-spec/1.1/ar01s06.html
		if program.Name == nil then
			program.Name = "[" .. file:match("([^/]+)%.desktop$") .. "]"
		end

		local cmdline = program.Exec:gsub("%%c", program.Name)
		cmdline = cmdline:gsub("%%[fuFU]", "")
		cmdline = cmdline:gsub("%%k", program.file)

		if program.icon_path then
			cmdline = cmdline:gsub("%%i", "--icon " .. program.icon_path)
		else
			cmdline = cmdline:gsub("%%i", "")
		end

		if program.Terminal == "true" then
			cmdline = dfparser.terminal .. " -e " .. cmdline
		end

		program.cmdline = cmdline
	end

	return program
end

-- Parse a directory with .desktop files
-- @param dir The directory
-- @param style Arguments for dfparser.lookup_icon
-- @return A table with all .desktop entries
local function parse_dir(dir, style)
	local req = awful.util.table.join({ path = dir }, style.icons)
	local programs = {}
	local cached = check_cached(req)

	if not cached then
		local files = modutil.read.output("find " .. dir .. ' -maxdepth 1 -name "*.desktop" 2>/dev/null')

		for file in string.gmatch(files, "[^\n]+") do
			local program = parse(file, style)
			if program then
				table.insert(programs, program)
			end
		end

		cache[req] = programs
	else
		programs = cached
	end

	return programs
end

-- Create a new application menu
-- @param style.icons Arguments for dfparser.lookup_icon
-- @param style.desktop_file_dirs Table containing all .desktop file directories
-- @return Applications menu table
function dfparser.menu(style)
	style = modutil.table.merge(default_style(), style or {})

	-- Categories list
	local categories = {
		{ app_type = "AudioVideo", name = "Multimedia", icon_name = "applications-multimedia" },
		{ app_type = "Development", name = "Development", icon_name = "applications-development" },
		{ app_type = "Education", name = "Education", icon_name = "applications-science" },
		{ app_type = "Game", name = "Games", icon_name = "applications-games" },
		{ app_type = "Graphics", name = "Graphics", icon_name = "applications-graphics" },
		{ app_type = "Office", name = "Office", icon_name = "applications-office" },
		{ app_type = "Network", name = "Internet", icon_name = "applications-internet" },
		{ app_type = "Settings", name = "Settings", icon_name = "applications-utilities" },
		{ app_type = "System", name = "System Tools", icon_name = "applications-system" },
		{ app_type = "Utility", name = "Accessories", icon_name = "applications-accessories" },
	}

	-- Find icons for categories
	for _, v in ipairs(categories) do
		v.icon = dfparser.lookup_icon(v.icon_name, style)
	end

	-- Find all visible menu items
	local prog_list = {}
	for _, path in ipairs(style.desktop_file_dirs) do
		local programs = parse_dir(path, style)

		for _, prog in ipairs(programs) do
			if prog.show and prog.Name and prog.cmdline then
				table.insert(prog_list, prog)
			end
		end
	end

	-- Sort menu items by category and create submenu
	local appmenu = {}
	for _, menu_category in ipairs(categories) do
		local catmenu = {}

		for i = #prog_list, 1, -1 do
			if prog_list[i].categories then
				for _, item_category in ipairs(prog_list[i].categories) do
					if item_category == menu_category.app_type then
						table.insert(catmenu, { prog_list[i].Name, prog_list[i].cmdline, prog_list[i].icon_path })
						table.remove(prog_list, i)
					end
				end
			end
		end
		if #catmenu > 0 then
			table.insert(appmenu, { menu_category.name, catmenu, menu_category.icon })
		end
	end

	-- Collect all items without category to "Other" submenu
	if #prog_list > 0 then
		local catmenu = {}

		for _, prog in ipairs(prog_list) do
			table.insert(catmenu, { prog.Name, prog.cmdline, prog.icon_path })
		end

		table.insert(appmenu, { "Other", catmenu, dfparser.lookup_icon("applications-other") })
	end

	return appmenu
end

-- Get list of icons linked with process name
-- @param style.icons Arguments for dfparser.lookup_icon
-- @param style.desktop_file_dirs Table containing all .desktop file directories
-- @return Icon list
function dfparser.icon_list(style)
	style = modutil.table.merge(default_style(), style or {})
	local list = {}

	for _, path in ipairs(style.desktop_file_dirs) do
		local programs = parse_dir(path, style)

		for _, prog in ipairs(programs) do
			if prog.Icon and prog.Exec then
				local key = string.match(prog.Exec, "[%a%d%.%-_/]+")
				if key then
					if string.find(key, "/") then
						key = string.match(key, "[%a%d%.%-_]+$")
					end
					list[key] = prog.icon_path
				end
			end
		end
	end

	return list
end

-- Generate table with avaliable programs
-- without sorting by categories
-- @param style.icons Arguments for dfparser.lookup_icon
-- @param style.desktop_file_dirs Table containing all .desktop file directories
function dfparser.program_list(style)
	style = modutil.table.merge(default_style(), style or {})
	local prog_list = {}

	for _, path in ipairs(style.desktop_file_dirs) do
		local programs = parse_dir(path, style)

		for _, prog in ipairs(programs) do
			if prog.show and prog.Name and prog.cmdline then
				table.insert(prog_list, prog)
			end
		end
	end

	return prog_list
end

return dfparser
