module("luci.passwall2.api", package.seeall)
local com = require "luci.passwall2.com"
bin = require "nixio".bin
fs = require "nixio.fs"
sys = require "luci.sys"
uci = require"luci.model.uci".cursor()
util = require "luci.util"
datatypes = require "luci.cbi.datatypes"
jsonc = require "luci.jsonc"
i18n = require "luci.i18n"

appname = "passwall2"
curl_args = {"-skfL", "--connect-timeout 3", "--retry 3", "-m 60"}
command_timeout = 300
OPENWRT_ARCH = nil
DISTRIB_ARCH = nil

LOG_FILE = "/tmp/log/passwall2.log"
CACHE_PATH = "/tmp/etc/passwall2_tmp"

function log(...)
	local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
	local f, err = io.open(LOG_FILE, "a")
	if f and err == nil then
		f:write(result .. "\n")
		f:close()
	end
end

function exec_call(cmd)
	local process = io.popen(cmd .. '; echo -e "\n$?"')
	local lines = {}
	local result = ""
	local return_code
	for line in process:lines() do
		lines[#lines + 1] = line
	end
	process:close()
	if #lines > 0 then
		return_code = lines[#lines]
		for i = 1, #lines - 1 do
			result = result .. lines[i] .. ((i == #lines - 1) and "" or "\n")
		end
	end
	return tonumber(return_code), trim(result)
end

function base64Decode(text)
	local raw = text
	if not text then return '' end
	text = text:gsub("%z", "")
	text = text:gsub("%c", "")
	text = text:gsub("_", "/")
	text = text:gsub("-", "+")
	local mod4 = #text % 4
	text = text .. string.sub('====', mod4 + 1)
	local result = nixio.bin.b64decode(text)
	if result then
		return result:gsub("%z", "")
	else
		return raw
	end
end

function curl_base(url, file, args)
	if not args then args = {} end
	if file then
		args[#args + 1] = "-o " .. file
	end
	local cmd = string.format('curl %s "%s"', table_join(args), url)
	return exec_call(cmd)
end

function curl_proxy(url, file, args)
	--使用代理
	local socks_server = luci.sys.exec("[ -f /tmp/etc/passwall2/global_SOCKS_server ] && echo -n $(cat /tmp/etc/passwall2/global_SOCKS_server) || echo -n ''")
	if socks_server ~= "" then
		if not args then args = {} end
		local tmp_args = clone(args)
		tmp_args[#tmp_args + 1] = "-x socks5h://" .. socks_server
		return curl_base(url, file, tmp_args)
	end
	return nil, nil
end

function curl_logic(url, file, args)
	local return_code, result = curl_proxy(url, file, args)
	if not return_code or return_code ~= 0 then
		return_code, result = curl_base(url, file, args)
	end
	return return_code, result
end

function url(...)
	local url = string.format("admin/services/%s", appname)
	local args = { ... }
	for i, v in pairs(args) do
		if v ~= "" then
			url = url .. "/" .. v
		end
	end
	return require "luci.dispatcher".build_url(url)
end

function trim(s)
	return (s:gsub("^%s*(.-)%s*$", "%1"))
end

-- 分割字符串
function split(full, sep)
	if full then
		full = full:gsub("%z", "") -- 这里不是很清楚 有时候结尾带个\0
		local off, result = 1, {}
		while true do
			local nStart, nEnd = full:find(sep, off)
			if not nEnd then
				local res = string.sub(full, off, string.len(full))
				if #res > 0 then -- 过滤掉 \0
					table.insert(result, res)
				end
				break
			else
				table.insert(result, string.sub(full, off, nStart - 1))
				off = nEnd + 1
			end
		end
		return result
	end
	return {}
end

function is_exist(table, value)
	for index, k in ipairs(table) do
		if k == value then
			return true
		end
	end
	return false
end

function repeat_exist(table, value)
	local count = 0
	for index, k in ipairs(table) do
		if k:find("-") and k == value then
			count = count + 1
		end
	end
	if count > 1 then
		return true
	end
	return false
end

function get_args(arg)
	local var = {}
	for i, arg_k in pairs(arg) do
		if i > 0 then
			local v = arg[i + 1]
			if v then
				if repeat_exist(arg, v) == false then
					var[arg_k] = v
				end
			end
		end
	end
	return var
end

function get_function_args(arg)
	local var = nil
	if arg and #arg > 1 then
		local param = {}
		for i = 2, #arg do
			param[#param + 1] = arg[i]
		end
		var = get_args(param)
	end
	return var
end

function strToTable(str)
	if str == nil or type(str) ~= "string" then
		return {}
	end
	
	return loadstring("return " .. str)()
end

function is_normal_node(e)
	if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface") then
		return false
	end
	return true
end

function is_special_node(e)
	return is_normal_node(e) == false
end

function is_ip(val)
	if is_ipv6(val) then
		val = get_ipv6_only(val)
	end
	return datatypes.ipaddr(val)
end

function is_ipv6(val)
	local str = val
	local address = val:match('%[(.*)%]')
	if address then
		str = address
	end
	if datatypes.ip6addr(str) then
		return true
	end
	return false
end

function is_ipv6addrport(val)
	if is_ipv6(val) then
		local address, port = val:match('%[(.*)%]:([^:]+)$')
		if port then
			return datatypes.port(port)
		end
	end
	return false
end

function get_ipv6_only(val)
	local result = ""
	if is_ipv6(val) then
		result = val
		if val:match('%[(.*)%]') then
			result = val:match('%[(.*)%]')
		end
	end
	return result
end

function get_ipv6_full(val)
	local result = ""
	if is_ipv6(val) then
		result = val
		if not val:match('%[(.*)%]') then
			result = "[" .. result .. "]"
		end
	end
	return result
end

function get_ip_type(val)
	if is_ipv6(val) then
		return "6"
	elseif datatypes.ip4addr(val) then
		return "4"
	end
	return ""
end

function is_mac(val)
	return datatypes.macaddr(val)
end

function ip_or_mac(val)
	if val then
		if get_ip_type(val) == "4" then
			return "ip"
		end
		if is_mac(val) then
			return "mac"
		end
	end
	return ""
end

function iprange(val)
	if val then
		local ipStart, ipEnd = val:match("^([^/]+)-([^/]+)$")
		if (ipStart and datatypes.ip4addr(ipStart)) and (ipEnd and datatypes.ip4addr(ipEnd)) then
			return true
		end
	end
	return false
end

function get_domain_from_url(url)
	local domain = string.match(url, "//([^/]+)")
	if domain then
		return domain
	end
	return url
end

function get_valid_nodes()
	local nodes_ping = uci_get_type("global_other", "nodes_ping") or ""
	local nodes = {}
	uci:foreach(appname, "nodes", function(e)
		e.id = e[".name"]
		if e.type and e.remarks then
			if e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface") then
				e["remark"] = "%s：[%s] " % {e.type .. " " .. i18n.translatef(e.protocol), e.remarks}
				e["node_type"] = "special"
				nodes[#nodes + 1] = e
			end
			if e.port and e.address then
				local address = e.address
				if is_ip(address) or datatypes.hostname(address) then
					local type = e.type
					if (type == "sing-box" or type == "Xray") and e.protocol then
						local protocol = e.protocol
						if protocol == "vmess" then
							protocol = "VMess"
						elseif protocol == "vless" then
							protocol = "VLESS"
						else
							protocol = protocol:gsub("^%l",string.upper)
						end
						type = type .. " " .. protocol
					end
					if is_ipv6(address) then address = get_ipv6_full(address) end
					e["remark"] = "%s：[%s]" % {type, e.remarks}
					if nodes_ping:find("info") then
						e["remark"] = "%s：[%s] %s:%s" % {type, e.remarks, address, e.port}
					end
					e.node_type = "normal"
					nodes[#nodes + 1] = e
				end
			end
		end
	end)
	return nodes
end

function get_node_remarks(n)
	local remarks = ""
	if n then
		if n.protocol and (n.protocol == "_balancing" or n.protocol == "_shunt" or n.protocol == "_iface") then
			remarks = "%s：[%s] " % {n.type .. " " .. i18n.translatef(n.protocol), n.remarks}
		else
			local type2 = n.type
			if (n.type == "sing-box" or n.type == "Xray") and n.protocol then
				local protocol = n.protocol
				if protocol == "vmess" then
					protocol = "VMess"
				elseif protocol == "vless" then
					protocol = "VLESS"
				else
					protocol = protocol:gsub("^%l",string.upper)
				end
				type2 = type2 .. " " .. protocol
			end
			remarks = "%s：[%s]" % {type2, n.remarks}
		end
	end
	return remarks
end

function get_full_node_remarks(n)
	local remarks = get_node_remarks(n)
	if #remarks > 0 then
		if n.address and n.port then
			remarks = remarks .. " " .. n.address .. ":" .. n.port
		end
	end
	return remarks
end

function gen_uuid(format)
	local uuid = sys.exec("echo -n $(cat /proc/sys/kernel/random/uuid)")
	if format == nil then
		uuid = string.gsub(uuid, "-", "")
	end
	return uuid
end

function gen_short_uuid()
	return sys.exec("echo -n $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)")
end

function uci_get_type(type, config, default)
	local value = uci:get_first(appname, type, config, default) or sys.exec("echo -n $(uci -q get " .. appname .. ".@" .. type .."[0]." .. config .. ")")
	if (value == nil or value == "") and (default and default ~= "") then
		value = default
	end
	return value
end

function uci_get_type_id(id, config, default)
	local value = uci:get(appname, id, config, default) or sys.exec("echo -n $(uci -q get " .. appname .. "." .. id .. "." .. config .. ")")
	if (value == nil or value == "") and (default and default ~= "") then
		value = default
	end
	return value
end

function chmod_755(file)
	if file and file ~= "" then
		if not fs.access(file, "rwx", "rx", "rx") then
			fs.chmod(file, 755)
		end
	end
end

function get_customed_path(e)
	return uci_get_type("global_app", e .. "_file")
end

function finded_com(e)
	local bin = get_app_path(e)
	if not bin then return end
	local s = luci.sys.exec('echo -n $(type -t -p "%s" | head -n1)' % { bin })
	if s == "" then
		s = nil
	end
	return s
end

function finded(e)
	return luci.sys.exec('echo -n $(type -t -p "/bin/%s" -p "/usr/bin/%s" "%s" | head -n1)' % {e, e, e})
end

function is_finded(e)
	return finded(e) ~= "" and true or false
end

function clone(org)
	local function copy(org, res)
		for k,v in pairs(org) do
			if type(v) ~= "table" then
				res[k] = v;
			else
				res[k] = {};
				copy(v, res[k])
			end
		end
	end
 
	local res = {}
	copy(org, res)
	return res
end

local function get_bin_version_cache(file, cmd)
	sys.call("mkdir -p /tmp/etc/passwall2_tmp")
	if fs.access(file) then
		chmod_755(file)
		local md5 = sys.exec("echo -n $(md5sum " .. file .. " | awk '{print $1}')")
		if fs.access("/tmp/etc/passwall2_tmp/" .. md5) then
			return sys.exec("echo -n $(cat /tmp/etc/passwall2_tmp/%s)" % md5)
		else
			local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd))
			if version and version ~= "" then
				sys.call("echo '" .. version .. "' > " .. "/tmp/etc/passwall2_tmp/" .. md5)
				return version
			end
		end
	end
	return ""
end

function get_app_path(app_name)
	if com[app_name] then
		local def_path = com[app_name].default_path
		local path = uci_get_type("global_app", app_name:gsub("%-","_") .. "_file")
		path = path and (#path>0 and path or def_path) or def_path
		return path
	end
end

function get_app_version(app_name, file)
	if file == nil then file = get_app_path(app_name) end
	return get_bin_version_cache(file, com[app_name].cmd_version)
end

function is_file(path)
	if path and #path > 1 then
		if sys.exec('[ -f "%s" ] && echo -n 1' % path) == "1" then
			return true
		end
	end
	return nil
end

function is_dir(path)
	if path and #path > 1 then
		if sys.exec('[ -d "%s" ] && echo -n 1' % path) == "1" then
			return true
		end
	end
	return nil
end

function get_final_dir(path)
	if is_dir(path) then
		return path
	else
		return get_final_dir(fs.dirname(path))
	end
end

function get_free_space(dir)
	if dir == nil then dir = "/" end
	if sys.call("df -k " .. dir .. " >/dev/null 2>&1") == 0 then
		return tonumber(sys.exec("echo -n $(df -k " .. dir .. " | awk 'NR>1' | awk '{print $4}')"))
	end
	return 0
end

function get_file_space(file)
	if file == nil then return 0 end
	if fs.access(file) then
		return tonumber(sys.exec("echo -n $(du -k " .. file .. " | awk '{print $1}')"))
	end
	return 0
end

function _unpack(t, i)
	i = i or 1
	if t[i] ~= nil then return t[i], _unpack(t, i + 1) end
end

function table_join(t, s)
	if not s then
		s = " "
	end
	local str = ""
	for index, value in ipairs(t) do
		str = str .. t[index] .. (index == #t and "" or s)
	end
	return str
end

function exec(cmd, args, writer, timeout)
	local os = require "os"
	local nixio = require "nixio"

	local fdi, fdo = nixio.pipe()
	local pid = nixio.fork()

	if pid > 0 then
		fdo:close()

		if writer or timeout then
			local starttime = os.time()
			while true do
				if timeout and os.difftime(os.time(), starttime) >= timeout then
					nixio.kill(pid, nixio.const.SIGTERM)
					return 1
				end

				if writer then
					local buffer = fdi:read(2048)
					if buffer and #buffer > 0 then
						writer(buffer)
					end
				end

				local wpid, stat, code = nixio.waitpid(pid, "nohang")

				if wpid and stat == "exited" then return code end

				if not writer and timeout then nixio.nanosleep(1) end
			end
		else
			local wpid, stat, code = nixio.waitpid(pid)
			return wpid and stat == "exited" and code
		end
	elseif pid == 0 then
		nixio.dup(fdo, nixio.stdout)
		fdi:close()
		fdo:close()
		nixio.exece(cmd, args, nil)
		nixio.stdout:close()
		os.exit(1)
	end
end

function parseURL(url)
	if not url or url == "" then
		return nil
	end
	local pattern = "^(%w+)://"
	local protocol = url:match(pattern)

	if not protocol then
		--error("Invalid URL: " .. url)
		return nil
	end

	local auth_host_port = url:sub(#protocol + 4)
	local auth_pattern = "^([^@]+)@"
	local auth = auth_host_port:match(auth_pattern)
	local username, password

	if auth then
		username, password = auth:match("^([^:]+):([^:]+)$")
		auth_host_port = auth_host_port:sub(#auth + 2)
	end

	local host, port = auth_host_port:match("^([^:]+):(%d+)$")

	if not host or not port then
		--error("Invalid URL: " .. url)
		return nil
	end

	return {
		protocol = protocol,
		username = username,
		password = password,
		host = host,
		port = tonumber(port)
	}
end

function compare_versions(ver1, comp, ver2)
	local table = table

	if not ver1 then ver1 = "" end
	if not ver2 then ver2 = "" end

	local av1 = util.split(ver1, "[%.%-]", nil, true)
	local av2 = util.split(ver2, "[%.%-]", nil, true)

	local max = table.getn(av1)
	local n2 = table.getn(av2)
	if (max < n2) then max = n2 end

	for i = 1, max, 1 do
		local s1 = tonumber(av1[i] or 0) or 0
		local s2 = tonumber(av2[i] or 0) or 0

		if comp == "~=" and (s1 ~= s2) then return true end
		if (comp == "<" or comp == "<=") and (s1 < s2) then return true end
		if (comp == ">" or comp == ">=") and (s1 > s2) then return true end
		if (s1 ~= s2) then return false end
	end

	return not (comp == "<" or comp == ">")
end

local function auto_get_arch()
	local arch = nixio.uname().machine or ""
	if not OPENWRT_ARCH and fs.access("/usr/lib/os-release") then
		OPENWRT_ARCH = sys.exec("echo -n $(grep 'OPENWRT_ARCH' /usr/lib/os-release | awk -F '[\\042\\047]' '{print $2}')")
		if OPENWRT_ARCH == "" then OPENWRT_ARCH = nil end
	end
	if not DISTRIB_ARCH and fs.access("/etc/openwrt_release") then
		DISTRIB_ARCH = sys.exec("echo -n $(grep 'DISTRIB_ARCH' /etc/openwrt_release | awk -F '[\\042\\047]' '{print $2}')")
		if DISTRIB_ARCH == "" then DISTRIB_ARCH = nil end
	end

	if arch:match("^i[%d]86$") then
		arch = "x86"
	elseif arch:match("armv5") then  -- armv5l
		arch = "armv5"
	elseif arch:match("armv6") then
		arch = "armv6"
	elseif arch:match("armv7") then  -- armv7l
		arch = "armv7"
	end

	if OPENWRT_ARCH or DISTRIB_ARCH then
		if arch == "mips" then
			if OPENWRT_ARCH and OPENWRT_ARCH:match("mipsel") == "mipsel"
			or DISTRIB_ARCH and DISTRIB_ARCH:match("mipsel") == "mipsel" then
				arch = "mipsel"
			end
		elseif arch == "armv7" then
			if OPENWRT_ARCH and not OPENWRT_ARCH:match("vfp") and not OPENWRT_ARCH:match("neon")
			or DISTRIB_ARCH and not DISTRIB_ARCH:match("vfp") and not DISTRIB_ARCH:match("neon") then
				arch = "armv5"
			end
		end
	end

	return util.trim(arch)
end

local default_file_tree = {
	x86_64  = "amd64",
	x86     = "386",
	aarch64 = "arm64",
	mips    = "mips",
	mipsel  = "mipsle",
	armv5   = "arm.*5",
	armv6   = "arm.*6[^4]*",
	armv7   = "arm.*7",
	armv8   = "arm64"
}

function get_api_json(url)
	local jsonc = require "luci.jsonc"
	local return_code, content = curl_logic(url, nil, curl_args)
	if return_code ~= 0 or content == "" then return {} end
	return jsonc.parse(content) or {}
end

local function check_path(app_name)
	local path = get_app_path(app_name) or ""
	if path == "" then
		return {
			code = 1,
			error = i18n.translatef("You did not fill in the %s path. Please save and apply then update manually.", app_name)
		}
	end
	return {
		code = 0,
		app_path = path
	}
end

function to_check(arch, app_name)
	local result = check_path(app_name)
	if result.code ~= 0 then
		return result
	end

	if not arch or arch == "" then arch = auto_get_arch() end

	local file_tree = com[app_name].file_tree[arch] or default_file_tree[arch] or ""

	if file_tree == "" then
		return {
			code = 1,
			error = i18n.translate("Can't determine ARCH, or ARCH not supported.")
		}
	end

	local local_version = get_app_version(app_name)
	local match_file_name = string.format(com[app_name].match_fmt_str, file_tree)
	local json = get_api_json(com[app_name]:get_url())

	if #json > 0 then
		json = json[1]
	end

	if json.tag_name == nil then
		return {
			code = 1,
			error = i18n.translate("Get remote version info failed.")
		}
	end

	local remote_version = json.tag_name
	if com[app_name].remote_version_str_replace then
		remote_version = remote_version:gsub(com[app_name].remote_version_str_replace, "")
	end
	local has_update = compare_versions(local_version:match("[^v]+"), "<", remote_version:match("[^v]+"))

	if not has_update then
		return {
			code = 0,
			local_version = local_version,
			remote_version = remote_version
		}
	end

	local asset = {}
	for _, v in ipairs(json.assets) do
		if v.name and v.name:match(match_file_name) then
			asset = v
			break
		end
	end
	if not asset.browser_download_url then
		return {
			code = 1,
			local_version = local_version,
			remote_version = remote_version,
			html_url = json.html_url,
			data = asset,
			error = i18n.translate("New version found, but failed to get new version download url.")
		}
	end

	return {
		code = 0,
		has_update = true,
		local_version = local_version,
		remote_version = remote_version,
		html_url = json.html_url,
		data = asset
	}
end

function to_download(app_name, url, size)
	local result = check_path(app_name)
	if result.code ~= 0 then
		return result
	end

	if not url or url == "" then
		return {code = 1, error = i18n.translate("Download url is required.")}
	end

	sys.call("/bin/rm -f /tmp/".. app_name .."_download.*")

	local tmp_file = util.trim(util.exec("mktemp -u -t ".. app_name .."_download.XXXXXX"))

	if size then
		local kb1 = get_free_space("/tmp")
		if tonumber(size) > tonumber(kb1) then
			return {code = 1, error = i18n.translatef("%s not enough space.", "/tmp")}
		end
	end

	local return_code, result = curl_logic(url, tmp_file, curl_args)
	result = return_code == 0

	if not result then
		exec("/bin/rm", {"-f", tmp_file})
		return {
			code = 1,
			error = i18n.translatef("File download failed or timed out: %s", url)
		}
	end

	return {code = 0, file = tmp_file, zip = com[app_name].zipped }
end

function to_extract(app_name, file, subfix)
	local result = check_path(app_name)
	if result.code ~= 0 then
		return result
	end

	if not file or file == "" or not fs.access(file) then
		return {code = 1, error = i18n.translate("File path required.")}
	end

	local tools_name
	if com[app_name].zipped then
		if not com[app_name].zipped_suffix or com[app_name].zipped_suffix == "zip" then
			tools_name = "unzip"
		end
		if com[app_name].zipped_suffix and com[app_name].zipped_suffix == "tar.gz" then
			tools_name = "tar"
		end
		if tools_name then
			if sys.exec("echo -n $(command -v %s)" % { tools_name }) == "" then
				exec("/bin/rm", {"-f", file})
				return {
					code = 1,
					error = i18n.translate("Not installed %s, Can't unzip!" % { tools_name })
				}
			end
		end
	end

	sys.call("/bin/rm -rf /tmp/".. app_name .."_extract.*")

	local new_file_size = get_file_space(file)
	local tmp_free_size = get_free_space("/tmp")
	if tmp_free_size <= 0 or tmp_free_size <= new_file_size then
		return {code = 1, error = i18n.translatef("%s not enough space.", "/tmp")}
	end

	local tmp_dir = util.trim(util.exec("mktemp -d -t ".. app_name .."_extract.XXXXXX"))

	local output = {}

	if tools_name then
		if tools_name == "unzip" then
			local bin = sys.exec("echo -n $(command -v unzip)")
			exec(bin, {"-o", file, app_name, "-d", tmp_dir}, function(chunk) output[#output + 1] = chunk end)
		elseif tools_name == "tar" then
			local bin = sys.exec("echo -n $(command -v tar)")
			if com[app_name].zipped_suffix == "tar.gz" then
				exec(bin, {"-zxf", file, "-C", tmp_dir}, function(chunk) output[#output + 1] = chunk end)
				sys.call("/bin/mv -f " .. tmp_dir .. "/*/" .. com[app_name].name:lower() .. " " .. tmp_dir)
			end
		end
	end

	local files = util.split(table.concat(output))

	exec("/bin/rm", {"-f", file})

	return {code = 0, file = tmp_dir}
end

function to_move(app_name,file)
	local result = check_path(app_name)
	if result.code ~= 0 then
		return result
	end

	local app_path = result.app_path
	local bin_path = file
	local cmd_rm_tmp = "/bin/rm -rf /tmp/" .. app_name .. "_download.*"
	if fs.stat(file, "type") == "dir" then
		bin_path = file .. "/" .. com[app_name].name:lower()
		cmd_rm_tmp = "/bin/rm -rf /tmp/" .. app_name .. "_extract.*"
	end

	if not file or file == "" then
		sys.call(cmd_rm_tmp)
		return {code = 1, error = i18n.translate("Client file is required.")}
	end

	local new_version = get_app_version(app_name, bin_path)
	if new_version == "" then
		sys.call(cmd_rm_tmp)
		return {
			code = 1,
			error = i18n.translate("The client file is not suitable for current device.")..app_name.."__"..bin_path
		}
	end

	local flag = sys.call('pgrep -af "passwall2/.*'.. app_name ..'" >/dev/null')
	if flag == 0 then
		sys.call("/etc/init.d/passwall2 stop")
	end

	local old_app_size = 0
	if fs.access(app_path) then
		old_app_size = get_file_space(app_path)
	end
	local new_app_size = get_file_space(bin_path)
	local final_dir = get_final_dir(app_path)
	local final_dir_free_size = get_free_space(final_dir)
	if final_dir_free_size > 0 then
		final_dir_free_size = final_dir_free_size + old_app_size
		if new_app_size > final_dir_free_size then
			sys.call(cmd_rm_tmp)
			return {code = 1, error = i18n.translatef("%s not enough space.", final_dir)}
		end
	end

	result = exec("/bin/mv", { "-f", bin_path, app_path }, nil, command_timeout) == 0

	sys.call(cmd_rm_tmp)
	if flag == 0 then
		sys.call("/etc/init.d/passwall2 restart >/dev/null 2>&1 &")
	end

	if not result or not fs.access(app_path) then
		return {
			code = 1,
			error = i18n.translatef("Can't move new file to path: %s", app_path)
		}
	end

	return {code = 0}
end

function cacheFileCompareToLogic(file, str)
	local result = nil
	if file and str then
		local file_str = ""
		if fs.access(file) then
			file_str = sys.exec("cat " .. file)
		end

		if file_str ~= str then
			sys.call("rm -f " .. file)
			result = false
		else
			result = true
		end

		local f_out = io.open(file, "w")
		if f_out then
			f_out:write(str)
			f_out:close()
		end
	end
	return result
end

function is_js_luci()
	return sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
end

function set_apply_on_parse(map)
	if is_js_luci() == true then
		map.apply_on_parse = false
		map.on_after_apply = function(self)
			if self.redirect then
				os.execute("sleep 1")
				luci.http.redirect(self.redirect)
			end
		end
	end
end

function luci_types(id, m, s, type_name, option_prefix)
	local rewrite_option_table = {}
	for key, value in pairs(s.fields) do
		if key:find(option_prefix) == 1 then
			if not s.fields[key].not_rewrite then
				if s.fields[key].rewrite_option then
					if not rewrite_option_table[s.fields[key].rewrite_option] then
						rewrite_option_table[s.fields[key].rewrite_option] = 1
					else
						rewrite_option_table[s.fields[key].rewrite_option] = rewrite_option_table[s.fields[key].rewrite_option] + 1
					end
				end

				s.fields[key].cfgvalue = function(self, section)
					if self.rewrite_option then
						return m:get(section, self.rewrite_option)
					else
						if self.option:find(option_prefix) == 1 then
							return m:get(section, self.option:sub(1 + #option_prefix))
						end
					end
				end
				s.fields[key].write = function(self, section, value)
					if s.fields["type"]:formvalue(id) == type_name then
						if self.rewrite_option then
							m:set(section, self.rewrite_option, value)
						else
							if self.option:find(option_prefix) == 1 then
								m:set(section, self.option:sub(1 + #option_prefix), value)
							end
						end
					end
				end
				s.fields[key].remove = function(self, section)
					if s.fields["type"]:formvalue(id) == type_name then
						if self.rewrite_option and rewrite_option_table[self.rewrite_option] == 1 then
							m:del(section, self.rewrite_option)
						else
							if self.option:find(option_prefix) == 1 then
								m:del(section, self.option:sub(1 + #option_prefix))
							end
						end
					end
				end
			end

			local deps = s.fields[key].deps
			if #deps > 0 then
				for index, value in ipairs(deps) do
					deps[index]["type"] = type_name
				end
			else
				s.fields[key]:depends({ type = type_name })
			end
		end
	end
end
