-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Check for and download one newer alpha build without installing it
-- @module core.update

local core = require("core")
local json = require("core.lib.json")
local status_items = require("core.status.items")
local style = require("core.style")
local TextDocument = require("core.text.document")

local update = {}

local DESCRIPTOR_MAX_SIZE = 64 * 1024
local ARCHIVE_MAX_SIZE = 256 * 1024 * 1024
local active = false
local available_descriptor = nil
local available_directory = nil
local available_archive_url = nil
local status_mode = "hidden"
local status_percent = 0
local status_item = nil
local pending_update = nil
local backup_result = nil
local manual_document = nil
local manual_text = nil
local set_status
local can_apply

--- Render one local directory as a consistent clickable Markdown link
-- @param path string local directory
-- @return string link
local function markdown_directory_link(path)
	local normalized = system.fix_slashes(path, "/")
	-- Pattern breakdown:
	--   each Markdown bracket in the visible path receives one escape character
	local label = normalized:gsub("([%[%]])", "\\%1")
	-- Pattern breakdown:
	--   each parenthesis in the link target receives one escape character
	local target = normalized:gsub("([()])", "\\%1")

	return "[" .. label .. "](" .. target .. ")"
end

--- Encode the small pending file with its format visibly first
-- @param pending table updater restart details
-- @return string text pretty JSON
local function encode_pending(pending)
	local json = require("core.lib.json")

	return table.concat({
		"{",
		'\t"format": ' .. json.encode(pending.format) .. ",",
		'\t"install_dir": ' .. json.encode(pending.install_dir) .. ",",
		'\t"user_dir": ' .. json.encode(pending.user_dir) .. ",",
		'\t"pending_file": ' .. json.encode(pending.pending_file) .. ",",
		'\t"pending_dir": ' .. json.encode(pending.pending_dir) .. ",",
		'\t"backup_file": ' .. json.encode(pending.backup_file) .. ",",
		'\t"arguments": ' .. json.encode(pending.arguments),
		"}",
		"",
	}, "\n")
end

--- Select the optional developer URL before the packaged URL
-- @param packaged_url string|nil URL stored in the installed manifest
-- @param local_url string|nil optional developer URL
-- @return string|nil update_url
local function select_update_url(packaged_url, local_url)
	local update_url = type(local_url) == "string" and local_url or packaged_url
	if type(update_url) ~= "string" or update_url:match("^%s*$") then
		return nil
	end

	return update_url
end

--- Encode the fixed alpha update-check request
-- @param hash string installation identifier
-- @param version string installed build
-- @param os_name string platform name
-- @return string text
local function encode_check_request(hash, version, os_name)
	return require("core.lib.json").encode({
		action = "check",
		hash = hash,
		version = version,
		os = os_name,
	})
end

--- Open the prepared manual-update instructions as rendered Markdown
local function open_manual_update()
	if not manual_text then
		return
	end

	if not manual_document then
		manual_document = TextDocument()
		manual_document.filename = "manual-update.md"
		manual_document.save_disabled = true
		manual_document.save_disabled_reason = "generated update instructions"
		manual_document:insert(1, 1, manual_text)
		manual_document:set_selection(1, 1)
		manual_document:clean()
		manual_document:reset_format()
	end

	core.workspace_view:open_doc(manual_document, {
		open_mode = require("core.open_mode").preview,
	})
end

--- Prepare clear instructions when the pending build cannot finish an update
-- @param result table pending update details
-- @param reason string missing updater feature
local function prepare_manual_update(result, reason)
	manual_text = table.concat({
		"# Manual update is needed",
		"",
		"The downloaded build cannot finish the automatic update on its own.",
		"Your current *Lx* has not been changed.",
		"",
		"Current *Lx*:",
		markdown_directory_link(EXE_DIR),
		"",
		"Unpacked update:",
		markdown_directory_link(result.path),
		"",
		"## Why",
		"",
		reason,
		"",
		"## Install it manually",
		"",
		"1. Close *Lx*",
		"2. Back up your current *Lx* folder",
		"3. Delete everything in your current *Lx* folder except your user files",
		"4. Move everything from unpacked update folder to the *Lx* folder",
		"5. Start *Lx* again",
	}, "\n")
	manual_document = nil
	set_status("manual")
--	core.warning("[update] Manual update needed. Click the red status item for instructions")
end

--- Return the short text for the current update status
-- @return string text
local function get_status_text()
	if status_mode == "available" then
		return "Update available"
	end
	if status_mode == "downloading" then
		return string.format("Download: %d%%", status_percent)
	end
	if status_mode == "preparing" then
		return string.format("Preparing update: %d%%", status_percent)
	end
	if status_mode == "ready" then
		return "Restart to update"
	end
	if status_mode == "prepared" then
		return "Update prepared"
	end
	if status_mode == "manual" then
		return "Manual update needed"
	end

	return ""
end

--- Restart Lx and show the changelog when it exists
-- @return boolean started
function update.restart()
	local changes_file = system.join_path(EXE_DIR, "CHANGES.md")
	if system.is_file(changes_file) then
		return core.restart({ "--preview", changes_file })
	end

	return core.restart()
end

--- Keep the optional updater item at the far right of the status bar
-- @param mode string new status mode
-- @param percent integer|nil completed download percentage
set_status = function(mode, percent)
	status_mode = mode
	status_percent = percent or 0

	if not status_item then
		status_item = status_items.register({
			id = "alpha_update",
			side = "right",
			order = 20000,
			priority = 2000,
			separator = "separator",
			get_tokens = function()
				return { get_status_text() }
			end,
			get_color = function()
				if status_mode == "available" then
					return style.warning or style.accent or style.text
				end
				if status_mode == "downloading" or status_mode == "preparing" then
					return style.changed or style.warning or style.accent or style.text
				end
				if status_mode == "manual" then
					return style.error or style.text
				end
				return style.success or style.text
			end,
			get_tooltip = function()
				if status_mode == "available" then
					return "A newer build is available; Click to download"
				end
				if status_mode == "downloading" then
					return "Downloading and verifying newer build"
				end
				if status_mode == "preparing" then
					return "Checking and preparing downloaded update"
				end
				if status_mode == "prepared" then
					return "Downloaded update was checked and unpacked"
				end
				if status_mode == "manual" then
					return "Lx must be updated manually; Click for instructions"
				end
				if status_mode == "ready" then
					return "The update is downloaded; Click to restart and install it"
				end

				return "Update is downloaded and will be installed after restarting Lx"
			end,
			on_click = false,
		})
	end

	local on_click = false
	if status_mode == "available" then
		on_click = function()
			update.download()
		end
	elseif status_mode == "ready" then
		on_click = function()
			core.restart()
		end
	elseif status_mode == "manual" then
		on_click = open_manual_update
	end

	status_item:update({ on_click = on_click })
	core.redraw = true
end

--- Write one informational update line without interrupting the status bar
-- @param format string message format
local function log(format, ...)
	core.log_quiet("[update] " .. format, ...)
end

--- Write one complete update failure to the log without showing a popup
-- @param format string message format
local function log_error(format, ...)
	core.log_quiet_level("error", "[update] " .. format, ...)
end

--- Write one update warning without interrupting the status bar
-- @param format string message format
local function log_warning(format, ...)
	core.log_quiet_level("warning", "[update] " .. format, ...)
end

--- Parse and validate the small update descriptor
-- @param text string descriptor bytes
-- @return table|nil descriptor
-- @return string|nil error
local function parse_descriptor(text)
	local decoded_ok, decoded = pcall(json.decode, text)

	if not decoded_ok then
		return nil, "descriptor JSON is invalid: " .. tostring(decoded)
	end
	if type(decoded) ~= "table" then
		return nil, "descriptor root must be an object"
	end
	if decoded.format ~= 1 then
		return nil, "unsupported descriptor format"
	end
	if type(decoded.latest) ~= "table" then
		return nil, "descriptor latest build is missing"
	end

	local latest = decoded.latest
	local descriptor = {
		format = decoded.format,
		build = latest.build,
		created = latest.created,
		archive = latest.archive,
		archive_url = latest.archive_url,
		size = latest.size,
		sha256 = latest.sha256,
	}

	if type(descriptor.build) ~= "string"
		or not descriptor.build:match("^%d%d%d%d%d%d%d%d%-%d%d%d%d$")
	then
		return nil, "descriptor build is invalid"
	end
	if type(descriptor.created) ~= "string" or descriptor.created == "" then
		return nil, "descriptor creation time is missing"
	end
	if type(descriptor.archive) ~= "string"
		or not descriptor.archive:match("^lx%-alpha%-%d%d%d%d%d%d%d%d%.zip$")
	then
		return nil, "descriptor archive name is invalid"
	end
	if type(descriptor.archive_url) ~= "string" or not descriptor.archive_url:match("^https?://") then
		return nil, "descriptor archive URL is invalid"
	end
	if type(descriptor.size) ~= "number"
		or descriptor.size % 1 ~= 0
		or descriptor.size < 1
		or descriptor.size > ARCHIVE_MAX_SIZE
	then
		return nil, "descriptor archive size is invalid"
	end
	if type(descriptor.sha256) ~= "string" or not descriptor.sha256:match("^[0-9a-fA-F]+$")
		or #descriptor.sha256 ~= 64
	then
		return nil, "descriptor SHA-256 is invalid"
	end

	descriptor.sha256 = descriptor.sha256:lower()

	return descriptor
end

--- Read update details from the installed root manifest
-- @return table|nil manifest
-- @return string|nil error
-- @return boolean|nil unpackaged
local function read_installed_manifest()
	local manifest_path = system.join_path(EXE_DIR, "manifest.json")
	if not system.is_file(manifest_path) then
		return nil, "unpackaged build has no manifest", true
	end

	local manifest, read_error = system.read_file(manifest_path, 1024 * 1024)
	if not manifest then
		return nil, read_error or "installed manifest is unavailable"
	end

	local decoded_ok, decoded = pcall(json.decode, manifest)
	if not decoded_ok then
		return nil, "installed manifest JSON is invalid: " .. tostring(decoded)
	end
	if type(decoded) ~= "table" then
		return nil, "installed manifest root must be an object"
	end

	local build = decoded.build
	if type(build) ~= "string" or not build:match("^%d%d%d%d%d%d%d%d%-%d%d%d%d$") then
		return nil, "installed manifest build is invalid"
	end
	if decoded.update_url ~= nil and type(decoded.update_url) ~= "string" then
		return nil, "installed manifest update URL is invalid"
	end

	return {
		build = build,
		update_url = decoded.update_url,
	}
end

--- Create the durable client update directory
-- @return string|nil path
-- @return string|nil error
local function ensure_update_directory()
	local path = system.join_path(USER_DIR, "update")
	local created, create_error = system.mkdir(path)
	if not created then
		return nil, create_error or "update directory could not be created"
	end

	return path
end

--- Generate one random hash without using machine or user information
-- @return string hash
local function generate_hash()
	return system.random_hash()
end

--- Read or create the anonymous installation hash
-- @return string|nil hash
-- @return string|nil error
local function read_or_create_hash()
	local path = system.join_path(USER_DIR, "hash")
	local existing = system.read_file(path, 64)
	if existing and system.check_random_hash(existing) then
		return existing
	end

	local hash = generate_hash()
	local written, write_error = system.write_file_atomic(path, hash)
	if not written then
		return nil, write_error or "installation hash could not be saved"
	end

	return hash
end

--- Wait cooperatively for one native download and log its complete result
-- @param request userdata native download request
-- @param label string short operation label
-- @param on_progress function|nil optional byte progress callback
-- @param warning boolean|nil whether failure is non-critical
-- @param friendly_failure string|nil plain explanation shown before technical details
-- @return boolean success
-- @return integer|string size_or_error
-- @return integer status
local function await_download(request, label, on_progress, warning, friendly_failure)
	while not request:ready() do
		if on_progress then
			on_progress(request:progress())
		end
		coroutine.yield(0.05)
	end

	local completed, size_or_error, status = request:result()
	if not completed then
		local message
		if friendly_failure then
			local status_text = status and status > 0
				and string.format("; HTTP status: %d", status)
				or ""
			message = string.format(
				"%s Details: %s%s",
				friendly_failure,
				tostring(size_or_error),
				status_text
			)
		elseif status and status > 0 then
			message = string.format(
				"%s failed: %s; HTTP status: %d",
				label,
				tostring(size_or_error),
				status
			)
		else
			message = string.format("%s failed: %s", label, tostring(size_or_error))
		end

		if warning then
			log_warning("%s", message)
		else
			log_error("%s", message)
		end

		return false, size_or_error, status
	end

	log("%s completed: %d bytes; HTTP status: %d", label, size_or_error, status)

	return true, size_or_error, status
end

--- Execute one descriptor check without downloading the update archive
-- @param installed_build string current installed build
-- @param update_directory string client update directory
-- @param installation_hash string anonymous installation hash
-- @param update_url string selected update base URL
local function run_check(installed_build, update_directory, installation_hash, update_url)
	local descriptor_path = system.join_path(update_directory, "latest.json.download")
	os.remove(descriptor_path)

	log("Checking alpha updates; installed build: %s", installed_build)

	local descriptor_request = web.download(update_url, descriptor_path, {
		method = "POST",
		body = encode_check_request(installation_hash, installed_build, PLATFORM),
		max_size = DESCRIPTOR_MAX_SIZE,
	})
	local descriptor_ok = await_download(
		descriptor_request,
		"Descriptor download",
		nil,
		true,
		"Could not check for updates. Lx will try again next time."
	)
	if not descriptor_ok then
		return
	end

	local descriptor_text, read_error = system.read_file(descriptor_path, DESCRIPTOR_MAX_SIZE)
	os.remove(descriptor_path)
	if not descriptor_text then
		log_error("Descriptor read failed: %s", tostring(read_error))

		return
	end

	local descriptor, descriptor_error = parse_descriptor(descriptor_text)
	if not descriptor then
		log_error("Descriptor rejected: %s", descriptor_error)

		return
	end

	log(
		"Descriptor accepted: build %s; created %s; archive %s",
		descriptor.build,
		descriptor.created,
		descriptor.archive
	)
	if descriptor.build <= installed_build then
		log("No newer alpha build is available")

		return
	end

	available_descriptor = descriptor
	available_directory = update_directory
	available_archive_url = descriptor.archive_url
	set_status("available")
	log_warning("Alpha build %s is available for download", descriptor.build)
end

--- Download and verify the update selected by the latest descriptor check
-- @param descriptor table validated update descriptor
-- @param update_directory string client update directory
-- @param archive_url string complete archive URL returned by the update service
-- @return boolean ready
local function run_download(descriptor, update_directory, archive_url)
	local update_path = system.join_path(update_directory, "pending.zip")
	local temporary_path = system.join_path(update_directory, "pending.zip.download")
	os.remove(temporary_path)
	log("Downloading alpha build %s; expected size: %d bytes", descriptor.build, descriptor.size)

	local archive_request = web.download(
		archive_url,
		temporary_path,
		{
			max_size = ARCHIVE_MAX_SIZE,
			size = descriptor.size,
			sha256 = descriptor.sha256,
		}
	)
	local last_percent = -1
	local archive_ok = await_download(archive_request, "Alpha archive download", function(received)
		local percent = math.min(100, math.floor(received * 100 / descriptor.size))
		if percent ~= last_percent then
			last_percent = percent
			set_status("downloading", percent)
		end
	end)
	if not archive_ok then
		return false
	end

	local renamed, rename_error = os.rename(temporary_path, update_path)
	if not renamed then
		log_error("Downloaded archive could not be finalized: %s", tostring(rename_error))

		return false
	end

	log("Alpha build %s is ready at %s", descriptor.build, update_path)

	return true
end

--- Start one non-blocking alpha check
-- @return boolean started
function update.check()
	if active then
		log("An update check is already running")

		return false
	end

	local installed_manifest, manifest_error, unpackaged = read_installed_manifest()
	if not installed_manifest then
		if unpackaged then
			log_warning("Update check skipped: %s", manifest_error)
		else
			log_error("Update check skipped: %s", tostring(manifest_error))
		end

		return false
	end

	local update_url = select_update_url(
		installed_manifest.update_url,
		rawget(_G, "LOCAL_UPDATE_URL")
	)
	if not update_url then
		log("Update URL absent; update check skipped")

		return false
	end

	local update_directory, directory_error = ensure_update_directory()
	if not update_directory then
		log_error("Update check skipped: %s", tostring(directory_error))

		return false
	end

	local update_path = system.join_path(update_directory, "pending.zip")
	if system.is_file(update_path) then
		available_directory = update_directory
		set_status("ready")
		log("Update package is already downloaded: %s", update_path)

		return true
	end

	local installation_hash, hash_error = read_or_create_hash()
	if not installation_hash then
		log_error("Update check skipped: %s", tostring(hash_error))

		return false
	end

	active = true
	core.add_coroutine(function()
		local completed, check_error = xpcall(
			run_check,
			debug.traceback,
			installed_manifest.build,
			update_directory,
			installation_hash,
			update_url
		)
		active = false

		if not completed then
			log_error("Update check crashed: %s", tostring(check_error))
		end
	end, update)

	return true
end

--- Check and unpack the pending update without changing installed files
-- @param automatic boolean|nil continue through backup and installation
-- @return boolean started
function update.stage_pending(automatic)
	if active then
		return false
	end

	local update_directory = system.join_path(USER_DIR, "update")
	local pending_path = system.join_path(update_directory, "pending.zip")
	local pending_directory = system.join_path(update_directory, "pending")
	local stage = require("core.update.stage")
	if not system.is_file(pending_path) then
		local prepared = select(1, stage.find_pending(pending_directory))
		if prepared then
			pending_update = prepared
			set_status("prepared")
			log("Prepared update found: %s", prepared.path)
			local supported, support_error = can_apply(prepared)
			if not supported then
				prepare_manual_update(prepared, support_error)

				return true
			end
			if automatic then
				update.create_backup(true)
			end

			return true
		end

		core.warning("[update] Nothing to stage: %s does not exist", pending_path)

		return false
	end

	active = true
	set_status("preparing", 0)
	log("Preparing downloaded update: %s", pending_path)

	core.add_coroutine(function()
		local completed, result, stage_error = xpcall(function()
			return stage.unpack(pending_path, pending_directory, function(_, current, total)
				if current == total or current % 32 == 0 then
					set_status("preparing", math.floor(current * 100 / total))
					coroutine.yield()
				end
			end)
		end, debug.traceback)
		active = false

		if not completed then
			set_status("ready")
			core.error("[update] Could not prepare the update: %s", tostring(result))

			return
		end
		if not result then
			set_status("ready")
			core.error("[update] Could not prepare the update: %s", tostring(stage_error))

			return
		end

		set_status("prepared")
		pending_update = result
		core.success(
			"[update] Update prepared: %d files; %s",
			result.files,
			result.path
		)
		local supported, support_error = can_apply(result)
		if not supported then
			prepare_manual_update(result, support_error)

			return
		end
		if automatic then
			update.create_backup(true)
		end
	end, stage)

	return true
end

--- Create the required rollback backup as its own testable step
-- @param automatic boolean|nil continue through installation
-- @return boolean started
function update.create_backup(automatic)
	if active then
		core.warning("[update] Another update step is already running")

		return false
	end

	local backup = require("core.update.backup")
	active = true
	backup_result = nil
	core.notice("[update] Creating backup... please wait")

	core.add_coroutine(function()
		local completed, result, backup_error = xpcall(function()
			return backup.create(function(_, _, current, total)
				if current == total or current % 32 == 0 then
					coroutine.yield()
				end
			end)
		end, debug.traceback)
		active = false

		if not completed then
			core.error("[update] Backup creation crashed: %s", tostring(result))

			return
		end
		if not result then
			core.error("[update] Backup creation failed: %s", tostring(backup_error))

			return
		end

		backup_result = result
		core.success(
			"[update] Backup created: %d Lx files; %d user files; %s",
			result.installed_files,
			result.user_files,
			result.path
		)
		if automatic then
			update.install()
		end
	end, backup)

	return true
end

--- Return whether one pending manifest can apply itself
-- @param result table pending update details
-- @return boolean supported
-- @return string|nil reason
can_apply = function(result)
	if result.format ~= 1 then
		return false, "The manifest format is not supported"
	end

	local required = {
		["lx.exe"] = false,
		["data/core/boot.lua"] = false,
		["data/core/update/apply.lua"] = false,
	}
	for _, file in ipairs(result.manifest.files or {}) do
		if required[file.path] ~= nil then
			required[file.path] = true
		end
	end
	for path, present in pairs(required) do
		if not present then
			return false, "The new build does not contain " .. path
		end
	end

	return true
end

--- Hand control to the pending Lx and close the current process
-- @return boolean started
function update.install()
	if active then
		core.warning("[update] Another update step is already running")

		return false
	end

	local update_directory = system.join_path(USER_DIR, "update")
	local stage = require("core.update.stage")
	if not pending_update then
		pending_update = select(1, stage.find_pending(
			system.join_path(update_directory, "pending")
		))
	end
	if not pending_update then
		core.warning("[update] Nothing is prepared. Run update:stage first")

		return false
	end

	local supported, support_error = can_apply(pending_update)
	if not supported then
		prepare_manual_update(pending_update, support_error)

		return false
	end
	if not backup_result then
		core.warning("[update] No backup was created in this session. Run update:create-backup first")

		return false
	end

	for _, document in ipairs(core.docs) do
		if document:is_dirty() then
			core.error("[update] Save or close edited documents before installing the update")

			return false
		end
	end

	local arguments = {}
	for index = 2, #(ARGS or {}) do
		arguments[#arguments + 1] = ARGS[index]
	end

	local pending_info_file = system.join_path(update_directory, "pending.json")
	local pending_path = system.join_path(update_directory, "pending.zip")
	local pending_written, pending_error = system.write_file_atomic(
		pending_info_file,
		encode_pending({
			format = 1,
			install_dir = EXE_DIR,
			user_dir = USER_DIR,
			pending_file = pending_path,
			pending_dir = pending_update.path,
			backup_file = backup_result.path,
			arguments = arguments,
		})
	)
	if not pending_written then
		core.error("[update] Could not prepare installation: %s", tostring(pending_error))

		return false
	end

	local pending_executable = system.join_path(pending_update.path, "lx.exe")
	local launched, launch_error = system.spawn(pending_executable, {
		"--new-instance",
		"--apply-update",
		pending_info_file,
	})
	if not launched then
		core.error("[update] Could not start the pending Lx: %s", tostring(launch_error))

		return false
	end

	core.quit(true)

	return true
end

--- Remove all temporary updater work while preserving backups and the hash
-- @return boolean started
function update.cleanup()
	if active then
		core.warning("[update] Another update step is already running")

		return false
	end

	local update_directory = system.join_path(USER_DIR, "update")
	local directories = {
		system.join_path(update_directory, "pending"),
		system.join_path(update_directory, "replaced"),
	}
	local files = {
		system.join_path(update_directory, "pending.zip"),
		system.join_path(update_directory, "pending.zip.download"),
		system.join_path(update_directory, "pending.json"),
	}
	active = true
	core.notice("[update] Cleaning update files...")

	core.add_coroutine(function()
		local last_error
		for _ = 1, 300 do
			local clean = true
			for _, directory in ipairs(directories) do
				local removed, remove_error = system.remove_tree(directory)
				if not removed then
					clean = false
					last_error = remove_error
				end
			end

			-- Remove pending.json last so interrupted cleanup is retried at startup
			if clean then
				for _, file in ipairs(files) do
					if system.is_file(file) then
						local removed, remove_error = os.remove(file)
						if not removed then
							clean = false
							last_error = remove_error
							break
						end
					end
				end
			end

			if clean then
				active = false
				pending_update = nil
				backup_result = nil
				manual_text = nil
				manual_document = nil
				set_status("hidden")
				core.success("[update] Update files cleaned")

				return
			end

			coroutine.yield(0.1)
		end

		active = false
		core.warning("[update] Update files could not be cleaned: %s", tostring(last_error))
	end, update)

	return true
end

--- Start downloading the update found by the latest completed check
-- @return boolean started
function update.download()
	if active or not available_descriptor or not available_directory then
		return false
	end

	local descriptor = available_descriptor
	local update_directory = available_directory
	local archive_url = available_archive_url
	active = true
	set_status("downloading", 0)

	core.add_coroutine(function()
		local completed, ready_or_error = xpcall(
			run_download,
			debug.traceback,
			descriptor,
			update_directory,
			archive_url
		)
		active = false

		if not completed then
			log_error("Update download crashed: %s", tostring(ready_or_error))
			set_status("available")
		elseif ready_or_error then
			set_status("ready")
		else
			set_status("available")
		end
	end, update)

	return true
end

update._parse_descriptor = parse_descriptor
update._select_update_url = select_update_url
update._can_apply = can_apply
update._encode_pending = encode_pending
update._read_or_create_hash = read_or_create_hash
update._markdown_directory_link = markdown_directory_link
update._encode_check_request = encode_check_request

return update
