local util = require("luasnip.util.util")
local str_util = require("luasnip.util.str")
local tbl_util = require("luasnip.util.table")
local ext_util = require("luasnip.util.ext_opts")
local types = require("luasnip.util.types")
local key_indexer = require("luasnip.nodes.key_indexer")
local session = require("luasnip.session")
local feedkeys = require("luasnip.util.feedkeys")
local snippet_string = require("luasnip.nodes.util.snippet_string")

local function subsnip_init_children(parent, children)
	for _, child in ipairs(children) do
		if child.type == types.snippetNode then
			child.snippet = parent.snippet
			child:resolve_child_ext_opts()
		end
		child:resolve_node_ext_opts()
		child:subsnip_init()
	end
end

local function init_child_positions_func(
	key,
	node_children_key,
	child_func_name
)
	-- maybe via load()?
	return function(node, position_so_far)
		node[key] = vim.deepcopy(position_so_far)
		local pos_depth = #position_so_far + 1

		for indx, child in ipairs(node[node_children_key]) do
			position_so_far[pos_depth] = indx
			child[child_func_name](child, position_so_far)
		end
		-- undo changes to position_so_far.
		position_so_far[pos_depth] = nil
	end
end

local function make_args_absolute(args, parent_insert_position, target)
	for i, arg in ipairs(args) do
		if type(arg) == "number" then
			-- the arg is a number, should be interpreted relative to direct
			-- parent.
			local t = vim.deepcopy(parent_insert_position)
			table.insert(t, arg)
			target[i] = { absolute_insert_position = t }
		else
			-- insert node, absolute_indexer, or key itself, node's
			-- absolute_insert_position may be nil, check for that during
			-- usage.
			target[i] = arg
		end
	end
end

local function wrap_args(args)
	-- stylua: ignore
	if type(args) ~= "table" or
	  (type(args) == "table" and args.absolute_insert_position) or
	  key_indexer.is_key(args) then
		-- args is one single arg, wrap it.
		return { args }
	else
		return args
	end
end

-- includes child, does not include parent.
local function get_nodes_between(parent, child, static)
	local nodes = {}

	-- special case for nodes without absolute_position (which is only
	-- start_node).
	if child.pos == -1 then
		-- no nodes between, only child.
		nodes[1] = child
		return nodes
	end

	local child_pos = child.absolute_position

	local indx = #parent.absolute_position + 1
	local prev = parent
	while child_pos[indx] do
		local next = prev:resolve_position(child_pos[indx], static)
		nodes[#nodes + 1] = next
		prev = next
		indx = indx + 1
	end

	return nodes
end

-- assumes that children of child are not even active.
-- If they should also be left, do that separately.
-- Does not leave the parent.
local function leave_nodes_between(parent, child, no_move)
	local nodes = get_nodes_between(parent, child)
	if #nodes == 0 then
		return
	end

	-- reverse order, leave child first.
	for i = #nodes, 2, -1 do
		-- this only happens for nodes where the parent will also be left
		-- entirely (because we stop at nodes[2], and handle nodes[1]
		-- separately)
		nodes[i]:input_leave(no_move)
		nodes[i - 1]:input_leave_children()
	end
	nodes[1]:input_leave(no_move)
end

local function enter_nodes_between(parent, child, no_move)
	local nodes = get_nodes_between(parent, child)
	if #nodes == 0 then
		return
	end

	for i = 1, #nodes - 1 do
		-- only enter children for nodes before the last (lowest) one.
		nodes[i]:input_enter(no_move)
		nodes[i]:input_enter_children()
	end
	nodes[#nodes]:input_enter(no_move)
end

local function select_node(node)
	local node_begin, node_end = node.mark:pos_begin_end_raw()
	feedkeys.select_range(node_begin, node_end)
end

local function print_dict(dict)
	print(vim.inspect(dict, {
		process = function(item, path)
			if path[#path] == "node" or path[#path] == "dependent" then
				return "node@" .. vim.inspect(item.absolute_position)
			elseif path[#path] ~= vim.inspect.METATABLE then
				return item
			end
		end,
	}))
end

local function init_node_opts(opts)
	local in_node = {}
	if not opts then
		opts = {}
	end

	-- copy once here, the opts might be reused.
	in_node.node_ext_opts =
		ext_util.complete(vim.deepcopy(opts.node_ext_opts or {}))

	if opts.merge_node_ext_opts == nil then
		in_node.merge_node_ext_opts = true
	else
		in_node.merge_node_ext_opts = opts.merge_node_ext_opts
	end

	in_node.key = opts.key

	in_node.node_callbacks = opts.node_callbacks or {}

	return in_node
end

local function snippet_extend_context(arg, extend)
	if type(arg) == "string" then
		arg = { trig = arg }
	end

	-- both are table or nil now.
	return vim.tbl_extend("keep", arg or {}, extend or {})
end

local function wrap_context(context)
	if type(context) == "string" then
		return { trig = context }
	else
		return context
	end
end

local function linkable_node(node)
	-- node.type has to be one of insertNode, exitNode.
	return vim.tbl_contains({ types.insertNode, types.exitNode }, node.type)
end

-- mainly used internally, by binarysearch_pos.
-- these are the nodes that are definitely not linkable, there are nodes like
-- dynamicNode or snippetNode that might be linkable, depending on their
-- content. Could look into that to make this more complete, but that does not
-- feel appropriate (higher runtime), most cases should be served well by this
-- heuristic.
local function non_linkable_node(node)
	return vim.tbl_contains({ types.textNode, types.functionNode }, node.type)
end
-- return whether a node is certainly (not) interactive.
-- Coincindentially, the same nodes as (non-)linkable ones, but since there is a
-- semantic difference, use separate names.
local interactive_node = linkable_node
local non_interactive_node = non_linkable_node

local function prefer_nodes(prefer_func, reject_func)
	return function(cmp_mid_to, cmp_mid_from, mid_node)
		local reject_mid = reject_func(mid_node)
		local prefer_mid = prefer_func(mid_node)

		-- if we can choose which node to continue in, prefer the one that
		-- may be linkable/interactive.
		if cmp_mid_to == 0 and reject_mid then
			return true, false
		elseif cmp_mid_from == 0 and reject_mid then
			return false, true
		elseif (cmp_mid_to == 0 or cmp_mid_from == 0) and prefer_mid then
			return false, false
		else
			return cmp_mid_to >= 0, cmp_mid_from < 0
		end
	end
end

-- functions for resolving conflicts, if `pos` is on the boundary of two nodes.
-- Return whether to continue behind or before mid (in that order).
-- At most one of those may be true, of course.
local binarysearch_preference = {
	outside = function(cmp_mid_to, cmp_mid_from, _)
		return cmp_mid_to >= 0, cmp_mid_from <= 0
	end,
	inside = function(cmp_mid_to, cmp_mid_from, _)
		return cmp_mid_to > 0, cmp_mid_from < 0
	end,
	linkable = prefer_nodes(linkable_node, non_linkable_node),
	interactive = prefer_nodes(interactive_node, non_interactive_node),
}
-- `nodes` is a list of nodes ordered by their occurrence in the buffer.
-- `pos` is a row-column-tuble, byte-columns, and we return the node the LEFT
-- EDGE(/side) of `pos` is inside.
-- This convention is chosen since a snippet inserted at `pos` will move the
-- character at `pos` to the right.
-- The exact meaning of "inside" can be influenced with `respect_rgravs` and
-- `boundary_resolve_mode`:
-- * if `respect_rgravs` is true, "inside" emulates the shifting-behaviour of
--   extmarks:
--   First of all, we compare the left edge of `pos` with the left/right edges
--   of from/to, depending on rgrav.
--   If the left edge is <= left/right edge of from, and < left/right edge of
--   to, `pos` is inside the node.
--
-- * if `respect_rgravs` is false, pos has to be fully inside a node to be
--   considered inside it. If pos is on the left endpoint, it is considered to be
--   left of the node, and likewise for the right endpoint.
--
-- * `boundary_resolve_mode` changes how a position on the boundary of a node
-- is treated:
-- * for `"prefer_linkable/interactive"`, we assume that the nodes in `nodes` are
-- contiguous, and prefer falling into the previous/next node if `pos` is on
-- mid's boundary, and mid is not linkable/interactie.
-- This way, we are more likely to return a node that can handle a new
-- snippet/is interactive.
-- * `"prefer_outside"` makes sense when the nodes are not contiguous, and we'd
-- like to find a position between two nodes.
-- This mode makes sense for finding the snippet a new snippet should be
-- inserted in, since we'd like to prefer inserting before/after a snippet, if
-- the position is ambiguous.
--
-- In general:
-- These options are useful for making this function more general: When
-- searching in the contiguous nodes of a snippet, we'd like this routine to
-- return any of them (obviously the one pos is inside/or on the border of, and
-- we'd like to prefer returning a node that can be linked), but in no case
-- fail.
-- However! when searching the top-level snippets with the intention of finding
-- the snippet/node a new snippet should be expanded inside, it seems better to
-- shift an existing snippet to the right/left than expand the new snippet
-- inside it (when the expand-point is on the boundary).
local function binarysearch_pos(
	nodes,
	pos,
	respect_rgravs,
	boundary_resolve_mode
)
	local left = 1
	local right = #nodes

	-- actual search-routine from
	-- https://github.com/Roblox/Wiki-Lua-Libraries/blob/master/StandardLibraries/BinarySearch.lua
	if #nodes == 0 then
		return nil, 1
	end
	while true do
		local mid = left + math.floor((right - left) / 2)
		local mid_mark = nodes[mid].mark
		local ok, mid_from, mid_to = pcall(mid_mark.pos_begin_end_raw, mid_mark)

		if not ok then
			-- error while running this procedure!
			-- return false (because I don't know how to do this with `error`
			-- and the offending node).
			-- (returning data instead of a message in `error` seems weird..)
			return false, mid
		end

		if respect_rgravs then
			-- if rgrav is set on either endpoint, the node considers its
			-- endpoint to be the right, not the left edge.
			-- We only want to work with left edges but since the right edge is
			-- the left edge of the next column, this is not an issue :)
			-- TODO: does this fail with multibyte characters???
			if mid_mark:get_rgrav(-1) then
				mid_from[2] = mid_from[2] + 1
			end
			if mid_mark:get_rgrav(1) then
				mid_to[2] = mid_to[2] + 1
			end
		end

		local cmp_mid_to = util.pos_cmp(pos, mid_to)
		local cmp_mid_from = util.pos_cmp(pos, mid_from)

		local cont_behind_mid, cont_before_mid =
			boundary_resolve_mode(cmp_mid_to, cmp_mid_from, nodes[mid])

		if cont_behind_mid then
			-- make sure right-left becomes smaller.
			left = mid + 1
			if left > right then
				return nil, mid + 1
			end
		elseif cont_before_mid then
			-- continue search on left side
			right = mid - 1
			if left > right then
				return nil, mid
			end
		else
			-- greater-equal than mid_from, smaller or equal to mid_to => left edge
			-- of pos is inside nodes[mid] :)
			return nodes[mid], mid
		end
	end
end

-- a and b have to be in the same snippet, return their first (as seen from
-- them) common parent.
local function first_common_node(a, b)
	local a_pos = a.absolute_position
	local b_pos = b.absolute_position

	-- last as seen from root.
	local i = 0
	local last_common = a.parent.snippet
	-- invariant: last_common is parent of both a and b.
	while (a_pos[i + 1] ~= nil) and a_pos[i + 1] == b_pos[i + 1] do
		last_common = last_common:resolve_position(a_pos[i + 1])
		i = i + 1
	end

	return last_common
end

-- roots at depth 0, children of root at depth 1, their children at 2, ...
local function snippettree_depth(snippet)
	local depth = 0
	while snippet.parent_node ~= nil do
		snippet = snippet.parent_node.parent.snippet
		depth = depth + 1
	end
	return depth
end

-- find the first common snippet a and b have on their respective unique paths
-- to the snippet-roots.
-- if no common ancestor exists (ie. a and b are roots of their buffers'
-- forest, or just in different trees), return nil.
-- in both cases, the paths themselves are returned as well.
-- The common ancestor is included in the paths, except if there is none.
-- Instead of storing the snippets in the paths, they are represented by the
-- node which contains the next-lower snippet in the path (or `from`/`to`, if it's
-- the first node of the path)
-- This is a bit complicated, but this representation contains more information
-- (or, more easily accessible information) than storing snippets: the
-- immediate parent of the child along the path cannot be easily retrieved if
-- the snippet is stored, but the snippet can be easily retrieved if the child
-- is stored (.parent.snippet).
-- And, so far this is pretty specific to refocus, and thus modeled so there is
-- very little additional work in that method.
-- At most one of a,b may be nil.
local function first_common_snippet_ancestor_path(a, b)
	local a_path = {}
	local b_path = {}

	-- general idea: we find the depth of a and b, walk upward with the deeper
	-- one until we find its first ancestor with the same depth as the less
	-- deep snippet, and then follow both paths until they arrive at the same
	-- snippet (or at the root of their respective trees).
	-- if either is nil, we treat it like it's one of the roots (the code will
	-- behave correctly this way, and return an empty path for the nil-node,
	-- and the correct path for the non-nil one).
	local a_depth = a ~= nil and snippettree_depth(a) or 0
	local b_depth = b ~= nil and snippettree_depth(b) or 0

	-- bit subtle: both could be 0, but one could be nil.
	-- deeper should not be nil! (this allows us to do the whole walk for the
	-- non-nil node in the first for-loop, as opposed to needing some special
	-- handling).
	local deeper, deeper_path, other, other_path
	if b == nil or (a ~= nil and a_depth > b_depth) then
		deeper = a
		other = b
		deeper_path = a_path
		other_path = b_path
	else
		-- we land here if `b ~= nil and (a == nil or a_depth >= b_depth)`, so
		-- exactly what we want.
		deeper = b
		other = a
		deeper_path = b_path
		other_path = a_path
	end

	for _ = 1, math.abs(a_depth - b_depth) do
		table.insert(deeper_path, deeper.parent_node)
		deeper = deeper.parent_node.parent.snippet
	end
	-- here: deeper and other are at the same depth.
	-- If we walk upwards one step at a time, they will meet at the same
	-- parent, or hit their respective roots.

	-- deeper can't be nil, if other is, we are done here and can return the
	-- paths (and there is no shared node)
	if other == nil then
		return nil, a_path, b_path
	end
	-- beyond here, deeper and other are not nil.

	while deeper ~= other do
		if deeper.parent_node == nil then
			-- deeper is at depth 0 => other as well => both are roots.
			return nil, a_path, b_path
		end

		table.insert(deeper_path, deeper.parent_node)
		table.insert(other_path, other.parent_node)

		-- walk one step towards root.
		deeper = deeper.parent_node.parent.snippet
		other = other.parent_node.parent.snippet
	end

	-- either one will do here.
	return deeper, a_path, b_path
end

-- removes focus from `from` and upwards up to the first common ancestor
-- (node!) of `from` and `to`, and then focuses nodes between that ancestor and
-- `to`.
-- Requires that `from` is currently entered/focused, and that no snippet
-- between `to` and its root is invalid.
local function refocus(from, to)
	if from == nil and to == nil then
		-- absolutely nothing to do, should not happen.
		return
	end
	-- pass nil if from/to is nil.
	-- if either is nil, first_common_node is nil, and the corresponding list empty.
	local first_common_snippet, from_snip_path, to_snip_path =
		first_common_snippet_ancestor_path(
			from and from.parent.snippet,
			to and to.parent.snippet
		)

	-- we want leave/enter_path to be s.t. leaving/entering all nodes between
	-- each entry and its snippet (and the snippet itself) will leave/enter all
	-- nodes between the first common snippet (or the root-snippet) and
	-- from/to.
	-- Then, the nodes between the first common node and the respective
	-- entrypoints (also nodes) into the first common snippet will have to be
	-- left/entered, which is handled by final_leave_/first_enter_/common_node.

	-- from, to are not yet in the paths.
	table.insert(from_snip_path, 1, from)
	table.insert(to_snip_path, 1, to)

	-- determine how far to leave: if there is a common snippet, only up to the
	-- first common node of from and to, otherwise leave the one snippet, and
	-- enter the other completely.
	local final_leave_node, first_enter_node, common_node
	if first_common_snippet then
		-- there is a common snippet => there is a common node => we have to
		-- set final_leave_node, first_enter_node, and common_node.
		final_leave_node = from_snip_path[#from_snip_path]
		first_enter_node = to_snip_path[#to_snip_path]
		common_node = first_common_node(first_enter_node, final_leave_node)

		-- Also remove these last nodes from the lists, their snippet is not
		-- supposed to be left entirely.
		from_snip_path[#from_snip_path] = nil
		to_snip_path[#to_snip_path] = nil
	end

	-- now do leave/enter, set no_move on all operations.
	-- if one of from/to was nil, there are no leave/enter-operations done for
	-- it (from/to_snip_path is {}, final_leave/first_enter_* is nil).

	-- leave_children on all from-nodes except the original from.
	if #from_snip_path > 0 then
		local ok1, ok2
		if from.type == types.exitNode then
			ok1 = pcall(from.input_leave, from, true)
			ok2 = true
		else
			-- we know that the first node is from.
			ok1 = pcall(leave_nodes_between, from.parent.snippet, from, true)
			-- leave_nodes_between does not affect snippet, so that has to be left
			-- here.
			-- snippet does not have input_leave_children, so only input_leave
			-- needs to be called.
			ok2 = pcall(
				from.parent.snippet.input_leave,
				from.parent.snippet,
				true
			)
		end
		if not ok1 or not ok2 then
			from.parent.snippet:remove_from_jumplist()
		end
	end
	for i = 2, #from_snip_path do
		local node = from_snip_path[i]
		local ok1, ok2, ok3
		ok1 = pcall(node.input_leave_children, node)

		if node.type == types.exitNode then
			ok2 = pcall(node.input_leave, node, true)
			ok3 = true
		else
			ok2 = pcall(leave_nodes_between, node.parent.snippet, node, true)
			ok3 = pcall(
				node.parent.snippet.input_leave,
				node.parent.snippet,
				true
			)
		end

		if not ok1 or not ok2 or not ok3 then
			from.parent.snippet:remove_from_jumplist()
		end
	end

	-- this leave, and the following enters should be safe: the path to `to`
	-- was verified via extmarks_valid (precondition).
	if common_node and final_leave_node then
		-- if the final_leave_node is from, its children are not active (which
		-- stems from the requirement that from is the currently active node),
		-- and so don't have to be left.
		if final_leave_node ~= from then
			final_leave_node:input_leave_children()
		end
		leave_nodes_between(common_node, final_leave_node, true)
	end

	if common_node and first_enter_node then
		-- In general we assume that common_node is active when we are here.
		-- This may not be the case if we are currently inside the i(0) or
		-- i(-1), since the snippet might be the common node and in this case,
		-- it is inactive.
		-- This means that, if we want to enter a non-exitNode, we have to
		-- explicitly activate the snippet for all jumps to behave correctly.
		-- (if we enter a i(0)/i(-1), this is not necessary, of course).
		if
			final_leave_node.type == types.exitNode
			and first_enter_node.type ~= types.exitNode
		then
			common_node:input_enter(true)
		end
		-- symmetrically, entering an i(0)/i(-1) requires leaving the snippet.
		if
			final_leave_node.type ~= types.exitNode
			and first_enter_node.type == types.exitNode
		then
			common_node:input_leave(true)
		end

		enter_nodes_between(common_node, first_enter_node, true)

		-- if the `first_enter_node` is already `to` (occurs if `to` is in the
		-- common snippet of to and from), we should not enter its children.
		-- (we only want to `input_enter` to.)
		if first_enter_node ~= to then
			first_enter_node:input_enter_children()
		end
	end

	-- same here, input_enter_children has to be called manually for the
	-- to-nodes of the path we are entering (since enter_nodes_between does not
	-- call it for the child-node).

	for i = #to_snip_path, 2, -1 do
		local node = to_snip_path[i]
		if node.type ~= types.exitNode then
			node.parent.snippet:input_enter(true)
			enter_nodes_between(node.parent.snippet, node, true)
		else
			node:input_enter(true)
		end
		node:input_enter_children()
	end
	if #to_snip_path > 0 then
		if to.type ~= types.exitNode then
			to.parent.snippet:input_enter(true)
		else
			to.parent.snippet:input_leave(true)
		end
		enter_nodes_between(to.parent.snippet, to, true)
	end

	-- it may be that we only leave nodes in this process (happens if to is a
	-- parent of from).
	-- If that is the case, we will not explicitly focus on to, and it may be
	-- that focus is even lost if it was focused previously (leave may trigger
	-- update, update may change focus)
	-- To prevent this, just call focus here, which is pretty close to a noop
	-- if to is already focused.
	if to then
		to:focus()
	end
end

local function generic_extmarks_valid(node, child)
	-- valid if
	-- - extmark-extents match.
	-- - current choice is valid
	local ok1, self_from, self_to =
		pcall(node.mark.pos_begin_end_raw, node.mark)
	local ok2, child_from, child_to =
		pcall(child.mark.pos_begin_end_raw, child.mark)

	if
		not ok1
		or not ok2
		or util.pos_cmp(self_from, child_from) ~= 0
		or util.pos_cmp(self_to, child_to) ~= 0
	then
		return false
	end
	return child:extmarks_valid()
end

-- returns: * the smallest known snippet `pos` is inside.
--          * the list of other snippets inside the snippet of this smallest
--            node
--          * the index this snippet would be at if inserted into that list
--          * the node of this snippet pos is on.
local function snippettree_find_undamaged_node(pos, opts)
	local prev_parent, child_indx, found_parent
	local prev_parent_children =
		session.snippet_roots[vim.api.nvim_get_current_buf()]

	while true do
		-- false: don't respect rgravs.
		-- Prefer inserting the snippet outside an existing one.
		found_parent, child_indx = binarysearch_pos(
			prev_parent_children,
			pos,
			opts.tree_respect_rgravs,
			opts.tree_preference
		)
		if found_parent == false then
			-- if the procedure returns false, there was an error getting the
			-- position of a node (in this case, that node is a snippet).
			-- The position of the offending snippet is returned in child_indx,
			-- and we can remove it here.
			prev_parent_children[child_indx]:remove_from_jumplist()
			-- remove_from_jumplist modified prev_parent_children, don't need
			-- to re-assign since we have a pointer to that table.
		elseif found_parent ~= nil and not found_parent:extmarks_valid() then
			-- found snippet damaged (the idea to sidestep the damaged snippet,
			-- even if no error occurred _right now_, is to ensure that we can
			-- input_enter all the nodes along the insertion-path correctly).
			found_parent:remove_from_jumplist()
			-- continue again with same parent, but one less snippet in its
			-- children => shouldn't cause endless loop.
		elseif found_parent == nil then
			break
		else
			prev_parent = found_parent
			-- can index prev_parent, since found_parent is not nil, and
			-- assigned to prev_parent.
			prev_parent_children = prev_parent.child_snippets
		end
	end

	local node
	if prev_parent then
		-- if found, find node to insert at, prefer receiving a linkable node.
		node = prev_parent:node_at(pos, opts.snippet_mode)
	end

	return prev_parent, prev_parent_children, child_indx, node
end

local function root_path(node, static)
	local path = {}

	while node do
		local node_snippet
		if node.parent == nil then
			-- node is snippet.
			node_snippet = node
		else
			node_snippet = node.parent.snippet
		end

		local snippet_node_path = get_nodes_between(node_snippet, node, static)
		-- get_nodes_between gives parent -> node, but we need
		-- node -> parent => insert back to front.
		for i = #snippet_node_path, 1, -1 do
			table.insert(path, snippet_node_path[i])
		end
		-- parent not in get_nodes_between.
		table.insert(path, node_snippet)

		node = node_snippet.parent_node
	end

	return path
end

-- adjust rgravs of siblings of the node with indx child_from_indx in nodes.
local function nodelist_adjust_rgravs(
	nodes,
	child_from_indx,
	child_endpoint,
	direction,
	rgrav,
	nodes_adjacent
)
	-- only handle siblings, not the node with child_from_indx itself.
	local i = child_from_indx
	local node = nodes[i]
	while node do
		local direction_node_endpoint = node.mark:get_endpoint(direction)
		if util.pos_equal(direction_node_endpoint, child_endpoint) then
			-- both endpoints of node are on top of child_endpoint (we wouldn't
			-- be in the loop with `node` if the -direction-endpoint didn't
			-- match), so update rgravs of the entire subtree to match rgrav
			node:subtree_set_rgrav(rgrav)
		else
			-- either assume that they are adjacent, or check.
			if
				nodes_adjacent
				or util.pos_equal(
					node.mark:get_endpoint(-direction),
					child_endpoint
				)
			then
				-- only the -direction-endpoint matches child_endpoint, adjust its
				-- position and break the loop (don't need to look at any other
				-- siblings).
				node:subtree_set_pos_rgrav(child_endpoint, direction, rgrav)
			end
			break
		end

		i = i + direction
		node = nodes[i]
	end
end

local function find_node_dependents(node)
	local node_position = node.absolute_insert_position
	local dict = node:get_snippet().dependents_dict
	local nodes = {}

	-- this might also be called from a node which does not possess a position!
	-- (for example, a functionNode may be depended upon via its key)
	if node_position then
		node_position[#node_position + 1] = "dependents"
		vim.list_extend(nodes, dict:find_all(node_position, "dependent") or {})
		node_position[#node_position] = nil
	end

	vim.list_extend(
		nodes,
		dict:find_all({ node, "dependents" }, "dependent") or {}
	)

	if node.key then
		vim.list_extend(
			nodes,
			dict:find_all({ "key", node.key, "dependents" }, "dependent") or {}
		)
	end

	return nodes
end

local function node_subtree_do(node, opts)
	-- provide default-values.
	if not opts.pre then
		opts.pre = util.nop
	end
	if not opts.post then
		opts.post = util.nop
	end

	node:subtree_do(opts)
end

local function collect_dependents(node, which, static)
	local dependents_set = {}

	if which.own then
		for _, dep in ipairs(find_node_dependents(node)) do
			dependents_set[dep] = true
		end
	end
	if which.parents then
		-- find dependents of all ancestors without duplicates.
		local path_to_root = root_path(node, static)
		-- remove `node` from path (its dependents are included if `which.own`
		-- is set)
		table.remove(path_to_root, 1)
		for _, ancestor in ipairs(path_to_root) do
			for _, dep in ipairs(find_node_dependents(ancestor)) do
				dependents_set[dep] = true
			end
		end
	end
	if which.children then
		-- only collects children in same snippet as node.
		node_subtree_do(node, {
			pre = function(st_node)
				-- don't update for self.
				if st_node == node then
					return
				end

				for _, dep in ipairs(find_node_dependents(st_node)) do
					dependents_set[dep] = true
				end
			end,
			static = static,
		})
	end

	return tbl_util.set_to_list(dependents_set)
end

local function str_args(args)
	return args
		and vim.tbl_map(function(arg)
			return snippet_string.isinstance(arg) and vim.split(arg:str(), "\n")
				or arg
		end, args)
end

---@class LuaSnip.SnippetCursorRestoreData
---This class holds data about the current position of the cursor in a snippet.
---@field key string key of the current node.
---@field store_id number uniquely identifies the data associated with this
---store-restore cycle.
---This is necessary because eg. the snippetStrings may contain cursor-positions
---of more than one restore data, and the correct ones can be identified via
---store_id.
---@field node LuaSnip.Node The node the cursor will be stored relative to.
---@field cursor_start_relative LuaSnip.BytecolBufferPosition The position of
---the cursor, or beginning of selected area, relative to the beginning of
---`node`.
---@field selection_end_start_relative LuaSnip.BytecolBufferPosition The
---position of the cursor, or end of selected area, relative to the beginning of
---`node`. The column is one beyond the byte where the selection ends.
---@field mode string The first character (see `vim.fn.mode()`) of the mode at
---the time of `store`.

---@alias LuaSnip.CursorRestoreData table<number, LuaSnip.SnippetCursorRestoreData>
---Represents the position of the cursor relative to all snippets the cursor was
---inside.
---Maps a `store_id` to the data needed to restore the cursor relative to the
---stored node of that snippet.
---We need the data relative to all parent-snippets of some node because the
---first 1,2,... snippets may disappear when a choice is changed.

---@class LuaSnip.StoreCursorNodeRelativeOpts
---@field place_cursor_mark boolean? Whether to, if possible, place a mark in
---snippetText.

local store_id = 0
---@param node LuaSnip.Node The node to store the cursor relative to.
---@param opts LuaSnip.StoreCursorNodeRelativeOpts
local function store_cursor_node_relative(node, opts)
	local data = {}

	local snippet_current_node = node

	local cursor_state = feedkeys.last_state()
	local store_ids = {}

	-- store for each snippet!
	-- the innermost snippet may be destroyed, and we would have to restore the
	-- cursor in a snippet above that.
	while snippet_current_node do
		local snip = snippet_current_node:get_snippet()

		local snip_data = {}

		snip_data.key = snippet_current_node.key
		snippet_current_node.store_id = store_id
		snip.node_store_id = store_id

		snip_data.store_id = store_id

		snip_data.node = snippet_current_node

		-- from low to high
		table.insert(store_ids, store_id)

		snip_data.cursor_start_relative = util.pos_offset(
			snippet_current_node.mark:get_endpoint(-1),
			cursor_state.pos
		)

		snip_data.mode = cursor_state.mode

		if cursor_state.pos_v then
			snip_data.selection_end_start_relative = util.pos_offset(
				snippet_current_node.mark:get_endpoint(-1),
				cursor_state.pos_v
			)
		end

		if
			snippet_current_node.type == types.insertNode
			and opts.place_cursor_mark
		then
			-- if the snippet_current_node is not an insertNode, the cursor
			-- should always be exactly at the beginning if the node is entered
			-- (which, btw, can only happen if a text or functionNode is
			-- immediately nested inside a choiceNode), which means that
			-- storing the cursor-position relative to the beginning of the
			-- node is sufficient for restoring it in all usecases (now, a user
			-- may have triggered the update while not in this position, but I
			-- think it's fine to not restore the cursor 100% correctly in that
			-- case.
			--
			-- When the node is an insertNode, the cursor may be somewhere
			-- inside the node, and while it will be restored correctly if the
			-- text does not change, if the update inserts some characters or a
			-- line at the beginning of the node (but still reaches
			-- equilibrium), the cursor will have moved relative to the
			-- immediately surrounding text.
			--
			-- A solution to this is to simply place some kind if extmark (but
			-- for regular strings, not for nvim-buffers) in the node (in the
			-- text that is passed to some dynamicNode, to be precise), which
			-- we then recover in the restore-function, and use to set the
			-- cursor correctly :)
			snippet_current_node:store()

			if
				snip_data.cursor_start_relative[1] >= 0
				and snip_data.cursor_start_relative[2] >= 0
			then
				-- we also have this in static_text, but recomputing the text
				-- exactly is rather expensive -> text is still in buffer, yank
				-- it.
				local str = snippet_current_node:get_text() --[=[@as string[] ]=]
				local pos_byte_offset = str_util.multiline_to_byte_offset(
					str,
					snip_data.cursor_start_relative
				)
				if pos_byte_offset then
					snippet_current_node.static_text:add_mark(
						store_id .. "pos",
						pos_byte_offset,
						false
					)
					if
						snip_data.selection_end_start_relative
						and snip_data.selection_end_start_relative[1] >= 0
						and snip_data.selection_end_start_relative[2] >= 0
					then
						local pos_v_byte_offset =
							str_util.multiline_to_byte_offset(
								str,
								snip_data.selection_end_start_relative
							)
						if pos_v_byte_offset then
							-- set rgrav of endpoint of selection true.
							-- This means if the selection is replaced, it would still
							-- be selected, which seems like a nice property.
							snippet_current_node.static_text:add_mark(
								store_id .. "pos_v",
								pos_v_byte_offset,
								true
							)
						end
					end
				end
			end
		end

		data[snip] = snippet_current_node
		data[store_id] = snip_data

		snippet_current_node = snip.parent_node

		store_id = store_id + 1
	end
	data.store_ids = store_ids

	return data
end

local function restore_cursor_pos_relative(node, data)
	local cursor_pos = data.cursor_start_relative
	local cursor_pos_v = data.selection_end_start_relative
	if node.type == types.insertNode then
		local mark_pos = node.static_text:get_mark_pos(data.store_id .. "pos")
		if mark_pos then
			local str = node:get_text()
			local mark_pos_offset =
				str_util.byte_to_multiline_offset(str, mark_pos)
			cursor_pos = mark_pos_offset and mark_pos_offset or cursor_pos

			local mark_pos_v =
				node.static_text:get_mark_pos(data.store_id .. "pos_v")
			if mark_pos_v then
				local mark_pos_v_offset =
					str_util.byte_to_multiline_offset(str, mark_pos_v)
				cursor_pos_v = mark_pos_v_offset
			end
		end
	end

	if data.mode == "i" then
		feedkeys.insert_at(
			util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos)
		)
	elseif data.mode == "s" then
		-- is a selection => restore it.
		local selection_from =
			util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos)
		local selection_to =
			util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos_v)
		feedkeys.select_range(selection_from, selection_to)
	else
		feedkeys.move_to_normal(
			util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos)
		)
	end
end

return {
	subsnip_init_children = subsnip_init_children,
	init_child_positions_func = init_child_positions_func,
	make_args_absolute = make_args_absolute,
	wrap_args = wrap_args,
	wrap_context = wrap_context,
	get_nodes_between = get_nodes_between,
	leave_nodes_between = leave_nodes_between,
	enter_nodes_between = enter_nodes_between,
	select_node = select_node,
	print_dict = print_dict,
	init_node_opts = init_node_opts,
	snippet_extend_context = snippet_extend_context,
	linkable_node = linkable_node,
	binarysearch_pos = binarysearch_pos,
	binarysearch_preference = binarysearch_preference,
	refocus = refocus,
	generic_extmarks_valid = generic_extmarks_valid,
	snippettree_find_undamaged_node = snippettree_find_undamaged_node,
	interactive_node = interactive_node,
	root_path = root_path,
	nodelist_adjust_rgravs = nodelist_adjust_rgravs,
	find_node_dependents = find_node_dependents,
	collect_dependents = collect_dependents,
	node_subtree_do = node_subtree_do,
	str_args = str_args,
	store_cursor_node_relative = store_cursor_node_relative,
	restore_cursor_pos_relative = restore_cursor_pos_relative,
}
