local options = {}
local builtin_standards = require("luacheck.builtin_standards")
local standards = require("luacheck.standards")
local utils = require("luacheck.utils")
local boolean = utils.has_type("boolean")
local array_of_strings = utils.array_of("string")

function options.split_std(std)
	local parts = utils.split(std, "+")

	if parts[1]:match("^%s*$") then
		parts.add = true
		table.remove(parts, 1)
	end

	for i, part in ipairs(parts) do
		parts[i] = utils.strip(part)

		if not builtin_standards[parts[i]] then return end
	end

	return parts
end

local function std_or_array_of_strings(x)
	return (type(x) == "string" and options.split_std(x)) or standards.validate_std_table(x)
end

local function field_map(x)
	return standards.validate_std_table({globals = x})
end

local function number_or_false(x)
	return x == false or type(x) == "number"
end

function options.add_order(option_set)
	local opts = {}

	for option in pairs(option_set) do
		if type(option) == "string" then table.insert(opts, option) end
	end

	table.sort(opts)
	utils.update(option_set, opts)
end

options.nullary_inline_options = {
	global = boolean,
	unused = boolean,
	redefined = boolean,
	unused_args = boolean,
	unused_secondaries = boolean,
	self = boolean,
	compat = boolean,
	allow_defined = boolean,
	allow_defined_top = boolean,
	module = boolean,
}
options.variadic_inline_options = {
	globals = field_map,
	read_globals = field_map,
	new_globals = field_map,
	new_read_globals = field_map,
	not_globals = array_of_strings,
	ignore = array_of_strings,
	enable = array_of_strings,
	only = array_of_strings,
}
options.all_options = {
	std = std_or_array_of_strings,
	max_line_length = number_or_false,
	max_code_line_length = number_or_false,
	max_string_line_length = number_or_false,
	max_comment_line_length = number_or_false,
	inline = boolean,
}
utils.update(options.all_options, options.nullary_inline_options)
utils.update(options.all_options, options.variadic_inline_options)
options.add_order(options.all_options)

-- Returns true if opts is valid option_set.
-- Otherwise returns false and, optionally, name of the problematic option.
function options.validate(option_set, opts)
	if opts == nil then return true end

	local ok, is_valid, invalid_opt = pcall(function()
		assert(type(opts) == "table")

		for _, option in ipairs(option_set) do
			if opts[option] ~= nil then
				if not option_set[option](opts[option]) then return false, option end
			end
		end

		return true
	end)
	return ok and is_valid, invalid_opt
end

-- Option stack is an array of options with options closer to end
-- overriding options closer to beginning.
-- Extracts sequence of active std tables from an option stack.
local function get_std_tables(opts_stack)
	local base_std
	local add_stds = {}
	local no_compat = false

	for _, opts in utils.ripairs(opts_stack) do
		if opts.compat and not no_compat then
			base_std = builtin_standards.max

			break
		elseif opts.compat == false then
			no_compat = true
		end

		if opts.std then
			if type(opts.std) == "table" then
				base_std = opts.std

				break
			else
				local parts = options.split_std(opts.std)

				for _, part in ipairs(parts) do
					table.insert(add_stds, builtin_standards[part])
				end

				if not parts.add then
					base_std = {}

					break
				end
			end
		end
	end

	table.insert(add_stds, 1, base_std or builtin_standards._G)
	return add_stds
end

-- Returns index of the last option table in a stack that uses given option,
-- or zero if the option isn't used anywhere.
local function index_of_last_option_usage(opts_stack, option_name)
	for index, opts in utils.ripairs(opts_stack) do
		if opts[option_name] then return index end
	end

	return 0
end

local function split_field(field_name)
	return utils.split(field_name, "%.")
end

local function field_comparator(field1, field2)
	local parts1 = field1[1]
	local parts2 = field2[1]

	for i = 1, math.max(#parts1, #parts2) do
		local part1 = parts1[i]
		local part2 = parts2[i]

		if not part1 then return true elseif not part2 then return false end

		if part1 ~= part2 then return part1 < part2 end
	end

	return false
end

-- Combine all stds and global related options into one final definition table.
-- A definition table may have fields `read_only` (boolean), `other_fields` (boolean),
-- and `fields` (maps field names to definition tables).
-- Std table format is similar, except at the top level there are two fields
-- `globals` and `read_globals` mapping to top-level field tables. Also in field tables
-- it's possible to use field names in array part as a shortcut:
-- `{fields = {"foo"}}` is equivalent to `{fields = {foo = {}}}` or `{fields = {foo = {other_fields = true}}}`
-- in top level fields tables.
local function get_final_std(opts_stack)
	local final_std = {}
	local std_tables = get_std_tables(opts_stack)

	for _, std_table in ipairs(std_tables) do
		standards.add_std_table(final_std, std_table)
	end

	local last_new_globals = index_of_last_option_usage(opts_stack, "new_globals")
	local last_new_read_globals = index_of_last_option_usage(opts_stack, "new_read_globals")

	for index, opts in ipairs(opts_stack) do
		local globals = (index >= last_new_globals) and (opts.new_globals or opts.globals)
		local read_globals = (index >= last_new_read_globals) and (opts.new_read_globals or opts.read_globals)
		local new_fields = {}

		if globals then
			for _, global in ipairs(globals) do
				table.insert(new_fields, {split_field(global), false})
			end
		end

		if read_globals then
			for _, read_global in ipairs(read_globals) do
				table.insert(new_fields, {split_field(read_global), true})
			end
		end

		if globals and read_globals then
			-- If there are both globals and read-only globals defined in one options table,
			-- it's important that more general definitions are applied first,
			-- otherwise they will completely overwrite more specific definitions.
			-- E.g. `globals x` should be applied before `read globals x.y`.
			table.sort(new_fields, field_comparator)
		end

		for _, field in ipairs(new_fields) do
			standards.overwrite_field(final_std, field[1], field[2])
		end

		standards.add_std_table(final_std, {globals = globals, read_globals = read_globals}, true, true)

		if opts.not_globals then
			for _, not_global in ipairs(opts.not_globals) do
				standards.remove_field(final_std, split_field(not_global))
			end
		end
	end

	standards.finalize(final_std)
	return final_std
end

local function get_scalar_opt(opts_stack, option, default)
	for _, opts in utils.ripairs(opts_stack) do
		if opts[option] ~= nil then return opts[option] end
	end

	return default
end

local line_length_suboptions = {"max_code_line_length", "max_string_line_length", "max_comment_line_length"}

local function get_max_line_opts(opts_stack)
	local res = {max_line_length = 120}

	for _, opt_name in ipairs(line_length_suboptions) do
		res[opt_name] = res.max_line_length
	end

	for _, opts in ipairs(opts_stack) do
		if opts.max_line_length ~= nil then
			res.max_line_length = opts.max_line_length

			for _, opt_name in ipairs(line_length_suboptions) do
				res[opt_name] = opts.max_line_length
			end
		end

		for _, opt_name in ipairs(line_length_suboptions) do
			if opts[opt_name] ~= nil then res[opt_name] = opts[opt_name] end
		end
	end

	return res
end

local function anchor_pattern(pattern, only_start)
	if not pattern then return end

	if pattern:sub(1, 1) == "^" or pattern:sub(-1) == "$" then
		return pattern
	else
		return "^" .. pattern .. (only_start and "" or "$")
	end
end

-- Returns {pair of normalized patterns for code and name}.
-- `pattern` can be:
--    string containing '/': first part matches warning code, second - variable name;
--    string containing letters: matches variable name;
--    otherwise: matches warning code.
-- Unless anchored by user, pattern for name is anchored from both sides
-- and pattern for code is only anchored at the beginning.
local function normalize_pattern(pattern)
	local code_pattern, name_pattern
	local slash_pos = pattern:find("/")

	if slash_pos then
		code_pattern = pattern:sub(1, slash_pos - 1)
		name_pattern = pattern:sub(slash_pos + 1)
	elseif pattern:find("[_a-zA-Z]") then
		name_pattern = pattern
	else
		code_pattern = pattern
	end

	return {anchor_pattern(code_pattern, true), anchor_pattern(name_pattern)}
end

-- From most specific to less specific, pairs {option, pattern}.
-- Applying macros in order is required to get deterministic results
-- and get sensible results when intersecting macros are used.
-- E.g. unused = false, unused_args = true should leave unused args enabled.
local macros = {
	{"unused_args", "21[23]"},
	{"global", "1"},
	{"unused", "[23]"},
	{"redefined", "4"},
}

-- Returns array of rules which should be applied in order.
-- A rule is a table {{pattern*}, type}.
-- `pattern` is a non-normalized pattern.
-- `type` can be "enable", "disable" or "only".
local function get_rules(opts_stack)
	local rules = {}
	local used_macros = {}

	for _, opts in utils.ripairs(opts_stack) do
		for _, macro_info in ipairs(macros) do
			local option, pattern = macro_info[1], macro_info[2]

			if not used_macros[option] then
				if opts[option] ~= nil then
					table.insert(rules, {{pattern}, opts[option] and "enable" or "disable"})
					used_macros[option] = true
				end
			end
		end

		if opts.ignore then table.insert(rules, {opts.ignore, "disable"}) end

		if opts.only then table.insert(rules, {opts.only, "only"}) end

		if opts.enable then table.insert(rules, {opts.enable, "enable"}) end
	end

	return rules
end

local function normalize_patterns(rules)
	local res = {}

	for i, rule in ipairs(rules) do
		res[i] = {{}, rule[2]}

		for j, pattern in ipairs(rule[1]) do
			res[i][1][j] = normalize_pattern(pattern)
		end
	end

	return res
end

local scalar_options = {
	unused_secondaries = true,
	self = true,
	inline = true,
	module = false,
	allow_defined = false,
	allow_defined_top = false,
}

-- Returns normalized options.
-- Normalized options have fields:
--    std: normalized std table, see `luacheck.standards` module;
--    unused_secondaries, self, inline, module, allow_defined, allow_defined_top: booleans;
--    max_line_length: number or false;
--    rules: see get_rules.
function options.normalize(opts_stack)
	local res = {}
	res.std = get_final_std(opts_stack)

	for option, default in pairs(scalar_options) do
		res[option] = get_scalar_opt(opts_stack, option, default)
	end

	local max_line_opts = get_max_line_opts(opts_stack)
	utils.update(res, max_line_opts)
	res.rules = normalize_patterns(get_rules(opts_stack))
	return res
end

return options