%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright © 2021-2026 Broadcom. All Rights Reserved. The term "Broadcom"
%% refers to Broadcom Inc. and/or its subsidiaries.
%%

%% @doc
%% Khepri tree manipulation API.
%%
%% @hidden

-module(khepri_tree).

-include_lib("stdlib/include/assert.hrl").

-include("include/khepri.hrl").
-include("src/khepri_error.hrl").
-include("src/khepri_node.hrl").

-export([new/0,
         get_root/1,
         get_keep_while_conds/1,
         get_keep_while_conds_revidx/1,
         assert_equal/2,

         are_keep_while_conditions_met/2,

         collect_node_props_cb/3,
         count_node_cb/3,

         find_matching_nodes/3,
         fold/5,
         delete_matching_nodes/4,
         insert_or_update_node/5,
         does_path_match/3,
         walk_down_the_tree/5,

         convert_tree/3]).

-ifdef(TEST).
-export([unopacify/1]).
-endif.

-record(tree, {root = #node{} :: khepri_tree:tree_node(),
               keep_while_conds = #{} :: khepri_tree:keep_while_conds_map(),
               keep_while_conds_revidx = #{}}).

-type tree_node() :: #node{}.
%% A node in the tree structure.

-type tree(KeepWhileCondsRevIdxType) :: #tree{keep_while_conds_revidx ::
                                              KeepWhileCondsRevIdxType}.

-opaque tree_v0() :: tree(khepri_tree:keep_while_conds_revidx_v0()).
-opaque tree_v1() :: tree(khepri_tree:keep_while_conds_revidx_v1()).

-type tree() :: tree_v0() | tree_v1().

-type keep_while_conds_map() :: #{khepri_path:native_path() =>
                                  khepri_condition:native_keep_while()}.
%% Per-node `keep_while' conditions.

-opaque keep_while_conds_revidx_v0() :: #{khepri_path:native_path() =>
                                          #{khepri_path:native_path() => ok}}.

-opaque keep_while_conds_revidx_v1() :: khepri_prefix_tree:tree(
                                          #{khepri_path:native_path() => ok}).

-type keep_while_conds_revidx() :: keep_while_conds_revidx_v0() |
                                   keep_while_conds_revidx_v1().
%% Internal reverse index of the keep_while conditions.
%%
%% If node A depends on a condition on node B, then this reverse index will
%% have a "node B => node A" association. The version 0 of this type used a map
%% and folded over the entries in the map using `lists:prefix/2' to find
%% matching conditions. In version 1 this type was replaced with a prefix tree
%% which improves lookup time when the reverse index contains many entries.

-type applied_changes() :: #{khepri_path:native_path() =>
                             {create, khepri:node_props()} |
                             {update, khepri:node_props()} |
                             delete}.
%% Internal index of the per-node changes which happened during a traversal.
%% This is used when the tree is walked back up to determine the list of tree
%% nodes to remove after some keep_while condition evaluates to false.

-type walk_down_the_tree_fun() ::
    fun((khepri_path:native_path(),
         tree_node() | {interrupted, any(), map()},
         Acc :: any()) ->
        ok(tree_node() | keep | delete, any()) |
        khepri:error()).
%% Function called to handle a node found (or an error) and used in {@link
%% walk_down_the_tree/6}.

-type ok(Type1, Type2) :: {ok, Type1, Type2}.
-type ok(Type1, Type2, Type3) :: {ok, Type1, Type2, Type3}.

-export_type([tree_node/0,
              tree_v0/0,
              tree_v1/0,
              tree/0,
              keep_while_conds_map/0,
              keep_while_conds_revidx_v0/0,
              keep_while_conds_revidx_v1/0,
              keep_while_conds_revidx/0,
              applied_changes/0]).

%% -------------------------------------------------------------------
%% Tree node functions.
%% -------------------------------------------------------------------

-spec new() -> tree().

new() ->
    #tree{}.

-spec get_root(Tree) -> Root when
      Tree :: khepri_tree:tree(),
      Root :: khepri_tree:tree_node().

get_root(#tree{root = Root}) ->
    Root.

-spec get_keep_while_conds(Tree) -> KeepWhileConds when
      Tree :: khepri_tree:tree(),
      KeepWhileConds :: khepri_tree:keep_while_conds_map().

get_keep_while_conds(#tree{keep_while_conds = KeepWhileConds}) ->
    KeepWhileConds.

-spec get_keep_while_conds_revidx(Tree) -> KeepWhileCondsRevIdx when
      Tree :: khepri_tree:tree(),
      KeepWhileCondsRevIdx :: khepri_tree:keep_while_conds_revidx().

get_keep_while_conds_revidx(
  #tree{keep_while_conds_revidx = KeepWhileCondsRevIdx}) ->
    KeepWhileCondsRevIdx.

-spec assert_equal(Tree1, Tree2) -> ok when
      Tree1 :: khepri_tree:tree(),
      Tree2 :: khepri_tree:tree().

assert_equal(#tree{} = Tree1, #tree{} = Tree2) ->
    ?assertEqual(Tree1, Tree2),
    ok.

-spec create_node_record(Payload) -> Node when
      Payload :: khepri_payload:payload(),
      Node :: tree_node().
%% @private

create_node_record(Payload) ->
    #node{props = ?INIT_NODE_PROPS,
          payload = Payload}.

-spec set_node_payload(Node, Payload) -> Node when
      Node :: tree_node(),
      Payload :: khepri_payload:payload().
%% @private

set_node_payload(#node{payload = Payload} = Node, Payload) ->
    Node;
set_node_payload(#node{props = #{payload_version := DVersion} = Stat} = Node,
                 Payload) ->
    Stat1 = Stat#{payload_version => DVersion + 1},
    Node#node{props = Stat1, payload = Payload}.

-spec remove_node_payload(Node) -> Node when
      Node :: tree_node().
%% @private

remove_node_payload(
  #node{payload = ?NO_PAYLOAD} = Node) ->
    Node;
remove_node_payload(
  #node{props = #{payload_version := DVersion} = Stat} = Node) ->
    Stat1 = Stat#{payload_version => DVersion + 1},
    Node#node{props = Stat1, payload = khepri_payload:none()}.

-spec add_node_child(Node, ChildName, Child) -> Node when
      Node :: tree_node(),
      Child :: tree_node(),
      ChildName :: khepri_path:component().

add_node_child(#node{props = #{child_list_version := CVersion} = Stat,
                     child_nodes = Children} = Node,
               ChildName, Child) ->
    Children1 = Children#{ChildName => Child},
    Stat1 = Stat#{child_list_version => CVersion + 1},
    Node#node{props = Stat1, child_nodes = Children1}.

-spec update_node_child(Node, ChildName, Child) -> Node when
      Node :: tree_node(),
      Child :: tree_node(),
      ChildName :: khepri_path:component().

update_node_child(#node{child_nodes = Children} = Node, ChildName, Child) ->
    Children1 = Children#{ChildName => Child},
    Node#node{child_nodes = Children1}.

-spec remove_node_child(Node, ChildName) -> Node when
      Node :: tree_node(),
      ChildName :: khepri_path:component().

remove_node_child(#node{props = #{child_list_version := CVersion} = Stat,
                        child_nodes = Children} = Node,
                  ChildName) ->
    ?assert(maps:is_key(ChildName, Children)),
    Stat1 = Stat#{child_list_version => CVersion + 1},
    Children1 = maps:remove(ChildName, Children),
    Node#node{props = Stat1, child_nodes = Children1}.

-spec remove_node_child_nodes(Node) -> Node when
      Node :: tree_node().

remove_node_child_nodes(
  #node{child_nodes = Children} = Node) when Children =:= #{} ->
    Node;
remove_node_child_nodes(
  #node{props = #{child_list_version := CVersion} = Stat} = Node) ->
    Stat1 = Stat#{child_list_version => CVersion + 1},
    Node#node{props = Stat1, child_nodes = #{}}.

-spec gather_node_props(Node, TreeOptions) -> NodeProps when
      Node :: tree_node(),
      TreeOptions :: khepri:tree_options(),
      NodeProps :: khepri:node_props().

gather_node_props(#node{props = #{payload_version := PVersion,
                                  child_list_version := CVersion},
                        payload = Payload,
                        child_nodes = Children},
                  #{props_to_return := WantedProps}) ->
    lists:foldl(
      fun
          (payload_version, Acc) ->
              Acc#{payload_version => PVersion};
          (child_list_version, Acc) ->
              Acc#{child_list_version => CVersion};
          (child_list_length, Acc) ->
              Acc#{child_list_length => maps:size(Children)};
          (child_names, Acc) ->
              Acc#{child_names => maps:keys(Children)};
          (payload, Acc) ->
              case Payload of
                  #p_data{data = Data}  -> Acc#{data => Data};
                  #p_sproc{sproc = Fun} -> Acc#{sproc => Fun};
                  _                     -> Acc
              end;
          (has_payload, Acc) ->
              case Payload of
                  #p_data{data = _}   -> Acc#{has_data => true};
                  #p_sproc{sproc = _} -> Acc#{is_sproc => true};
                  _                   -> Acc
              end;
          (raw_payload, Acc) ->
              Acc#{raw_payload => Payload};
          (_Unknown, Acc) ->
              %% We ignore props we don't know about. It might be a new one in
              %% a future version of the machine.
              Acc
      end, #{}, WantedProps);
gather_node_props(#node{}, _Options) ->
    #{}.

gather_node_props_for_error(Node) ->
    gather_node_props(Node, #{props_to_return => ?DEFAULT_PROPS_TO_RETURN}).

-spec reset_versions(Node) -> Node when
      Node :: tree_node().
%% @private

reset_versions(#node{props = Stat} = CurrentNode) ->
    Stat1 = Stat#{payload_version => ?INIT_DATA_VERSION,
                  child_list_version => ?INIT_CHILD_LIST_VERSION},
    CurrentNode#node{props = Stat1}.

-spec squash_version_bumps(OldNode, NewNode) -> Node when
      OldNode :: tree_node(),
      NewNode :: tree_node(),
      Node :: tree_node().
%% @private

squash_version_bumps(
  #node{props = #{payload_version := DVersion,
                  child_list_version := CVersion}},
  #node{props = #{payload_version := DVersion,
                  child_list_version := CVersion}} = CurrentNode) ->
    CurrentNode;
squash_version_bumps(
  #node{props = #{payload_version := OldDVersion,
                  child_list_version := OldCVersion}},
  #node{props = #{payload_version := NewDVersion,
                  child_list_version := NewCVersion} = Stat} = CurrentNode) ->
    DVersion = case NewDVersion > OldDVersion of
                   true  -> OldDVersion + 1;
                   false -> OldDVersion
               end,
    CVersion = case NewCVersion > OldCVersion of
                   true  -> OldCVersion + 1;
                   false -> OldCVersion
               end,
    Stat1 = Stat#{payload_version => DVersion,
                  child_list_version => CVersion},
    CurrentNode#node{props = Stat1}.

-spec squash_version_bumps_after_keep_while(
        Tree, AppliedChangesBefore, AppliedChangesAfter) ->
    NewTree when
      Tree :: tree(),
      AppliedChangesBefore :: applied_changes(),
      AppliedChangesAfter :: applied_changes(),
      NewTree :: tree().
%% @doc Squashes `child_list_version' bumps triggered by `keep_while'
%% conditions if a tree node's version was already bumped after the command
%% was applied.
%%
%% @private

squash_version_bumps_after_keep_while(
  #tree{root = Root} = Tree, AppliedChangesBefore, AppliedChangesAfter) ->
    %% We record all tree nodes that had their `child_list_version' bumped by
    %% the previous update of the tree.
    BumpsBefore = maps:fold(
                    fun
                        (Path, Change, Acc)
                          when Change =:= delete orelse
                               element(1, Change) =:= create ->
                            ParentPath = lists:droplast(Path),
                            Acc#{ParentPath => true};
                        (_Path, _Change, Acc) ->
                            Acc
                    end, #{}, AppliedChangesBefore),
    %% We use the previous map to determine which `keep_while'-induced changes
    %% should trigger a new bump of `child_list_version'.
    BumpsAfter = maps:fold(
                   fun(Path, _Change, Acc) ->
                           ParentPath = lists:droplast(Path),
                           case BumpsBefore of
                               %% `ParentPath' had its `child_list_version'
                               %% bumped by the walk/update. The application
                               %% of `keep_while' bumped it again and we don't
                               %% want that. Mark this path to indicate we
                               %% want to revert that second bump.
                               #{ParentPath := _} -> Acc#{ParentPath => -1};
                               _                  -> Acc
                           end
                   end, #{}, AppliedChangesAfter),
    %% Go through all bump reverts we need and "unbump" the corresponding
    %% `child_list_version'.
    Root1 = maps:fold(
              fun(Path, Bump, Root1) ->
                      squash_version_bumps_after_keep_while1(Root1, Path, Bump)
              end, Root, BumpsAfter),
    Tree1 = Tree#tree{root = Root1},
    Tree1.

squash_version_bumps_after_keep_while1(
  #node{child_nodes = Children} = Node,
  [ChildName | Rest],
  Bump) ->
    case Children of
        #{ChildName := ChildNode} ->
            ChildNode1 = squash_version_bumps_after_keep_while1(
                           ChildNode, Rest, Bump),
            Children1 = Children#{ChildName => ChildNode1},
            Node1 = Node#node{child_nodes = Children1},
            Node1;
        _ ->
            Node
    end;
squash_version_bumps_after_keep_while1(
  #node{props = #{child_list_version := CVersion} = Props} = Node,
  [],
  Bump) ->
    CVersion1 = CVersion + Bump,
    Props1 = Props#{child_list_version => CVersion1},
    Node1 = Node#node{props = Props1},
    Node1.

%% -------------------------------------------------------------------
%% Keep-while functions.
%% -------------------------------------------------------------------

-spec to_absolute_keep_while(BasePath, KeepWhile) -> AbsKeepWhile when
      BasePath :: khepri_path:native_path(),
      KeepWhile :: khepri_condition:native_keep_while(),
      AbsKeepWhile :: khepri_condition:native_keep_while().
%% @private

to_absolute_keep_while(BasePath, KeepWhile) ->
    maps:fold(
      fun(Path, Cond, Acc) ->
              AbsPath = khepri_path:abspath(Path, BasePath),
              Acc#{AbsPath => Cond}
      end, #{}, KeepWhile).

-spec are_keep_while_conditions_met(Tree, KeepWhile) -> Ret when
      Tree :: tree(),
      KeepWhile :: khepri_condition:native_keep_while(),
      Ret :: true | {false, any()}.
%% @private

are_keep_while_conditions_met(_, KeepWhile)
  when KeepWhile =:= #{} ->
    true;
are_keep_while_conditions_met(Tree, KeepWhile) ->
    TreeOptions = #{props_to_return => [payload,
                                        payload_version,
                                        child_list_version,
                                        child_list_length]},
    maps:fold(
      fun
          (Path, Condition, true) ->
              case find_matching_nodes(Tree, Path, TreeOptions) of
                  {ok, Result} when Result =/= #{} ->
                      are_keep_while_conditions_met1(Result, Condition);
                  {ok, Result}
                    when Result =:= #{} andalso
                         Condition =:= #if_node_exists{exists = false} ->
                      %% The path pattern of the `keep_while' condition
                      %% matched no tree nodes. The condition indicates the
                      %% target node/pattern should not exist, therefore the
                      %% condition is met.
                      true;
                  {ok, _} ->
                      {false, {pattern_matches_no_nodes, Path}};
                  {error, Reason} ->
                      {false, Reason}
              end;
          (_, _, False) ->
              False
      end, true, KeepWhile).

are_keep_while_conditions_met1(Result, Condition) ->
    maps:fold(
      fun
          (Path, NodeProps, true) ->
              khepri_condition:is_met(Condition, Path, NodeProps);
          (_, _, False) ->
              False
      end, true, Result).

is_keep_while_condition_met_on_self(
  #tree{keep_while_conds = KeepWhileConds}, Path, Node) ->
    case KeepWhileConds of
        #{Path := #{Path := Condition}} ->
            khepri_condition:is_met(Condition, Path, Node);
        _ ->
            true
    end.

-spec update_keep_while_conds(Tree, Watcher, KeepWhile) -> NewTree when
      Tree :: khepri_tree:tree(),
      Watcher :: khepri_path:native_path(),
      KeepWhile :: khepri_condition:native_keep_while(),
      NewTree :: khepri_tree:tree().

update_keep_while_conds(Tree, Watcher, KeepWhile) ->
    AbsKeepWhile = to_absolute_keep_while(Watcher, KeepWhile),
    Tree1 = update_keep_while_conds_revidx(Tree, Watcher, AbsKeepWhile),
    KeepWhileConds = get_keep_while_conds(Tree1),
    KeepWhileConds1 = KeepWhileConds#{Watcher => AbsKeepWhile},
    Tree1#tree{keep_while_conds = KeepWhileConds1}.

-spec update_keep_while_conds_revidx(Tree, Watcher, KeepWhile) -> NewTree when
      Tree :: tree(),
      Watcher :: khepri_path:native_path(),
      KeepWhile :: khepri_condition:native_keep_while(),
      NewTree :: tree().

update_keep_while_conds_revidx(
  #tree{keep_while_conds_revidx = KeepWhileCondsRevIdx} = Tree,
  Watcher, KeepWhile) ->
    case is_v1_keep_while_conds_revidx(KeepWhileCondsRevIdx) of
        true ->
            update_keep_while_conds_revidx_v1(Tree, Watcher, KeepWhile);
        false ->
            update_keep_while_conds_revidx_v0(Tree, Watcher, KeepWhile)
    end.

is_v1_keep_while_conds_revidx(KeepWhileCondsRevIdx) ->
    khepri_prefix_tree:is_prefix_tree(KeepWhileCondsRevIdx).

update_keep_while_conds_revidx_v0(
  #tree{keep_while_conds = KeepWhileConds,
        keep_while_conds_revidx = KeepWhileCondsRevIdx} = Tree,
  Watcher, KeepWhile) ->
    %% First, clean up reversed index where a watched path isn't watched
    %% anymore in the new keep_while.
    OldWatcheds = maps:get(Watcher, KeepWhileConds, #{}),
    KeepWhileCondsRevIdx1 = maps:fold(
                          fun(Watched, _, KWRevIdx) ->
                                  Watchers = maps:get(Watched, KWRevIdx),
                                  Watchers1 = maps:remove(Watcher, Watchers),
                                  case maps:size(Watchers1) of
                                      0 -> maps:remove(Watched, KWRevIdx);
                                      _ -> KWRevIdx#{Watched => Watchers1}
                                  end
                          end, KeepWhileCondsRevIdx, OldWatcheds),
    %% Then, record the watched paths.
    KeepWhileCondsRevIdx2 = maps:fold(
                          fun(Watched, _, KWRevIdx) ->
                                  Watchers = maps:get(Watched, KWRevIdx, #{}),
                                  Watchers1 = Watchers#{Watcher => ok},
                                  KWRevIdx#{Watched => Watchers1}
                          end, KeepWhileCondsRevIdx1, KeepWhile),
    Tree#tree{keep_while_conds_revidx = KeepWhileCondsRevIdx2}.

update_keep_while_conds_revidx_v1(
  #tree{keep_while_conds = KeepWhileConds,
        keep_while_conds_revidx = KeepWhileCondsRevIdx} = Tree,
  Watcher, KeepWhile) ->
    %% First, clean up reversed index where a watched path isn't watched
    %% anymore in the new keep_while.
    OldWatcheds = maps:get(Watcher, KeepWhileConds, #{}),
    KeepWhileCondsRevIdx1 = maps:fold(
                          fun(Watched, _, KWRevIdx) ->
                                  khepri_prefix_tree:update(
                                    fun(Watchers) ->
                                            Watchers1 = maps:remove(
                                                          Watcher, Watchers),
                                            case maps:size(Watchers1) of
                                                0 -> ?NO_PAYLOAD;
                                                _ -> Watchers1
                                            end
                                    end, Watched, KWRevIdx)
                          end, KeepWhileCondsRevIdx, OldWatcheds),
    %% Then, record the watched paths.
    KeepWhileCondsRevIdx2 = maps:fold(
                          fun(Watched, _, KWRevIdx) ->
                                  khepri_prefix_tree:update(
                                    fun (?NO_PAYLOAD) ->
                                            #{Watcher => ok};
                                        (Watchers) ->
                                            Watchers#{Watcher => ok}
                                    end, Watched, KWRevIdx)
                          end, KeepWhileCondsRevIdx1, KeepWhile),
    Tree#tree{keep_while_conds_revidx = KeepWhileCondsRevIdx2}.

%% -------------------------------------------------------------------
%% Find matching nodes.
%% -------------------------------------------------------------------

-spec find_matching_nodes(Tree, PathPattern, TreeOptions) ->
    Ret when
      Tree :: tree(),
      PathPattern :: khepri_path:native_pattern(),
      TreeOptions :: khepri:tree_options(),
      Ret :: khepri_machine:write_ret().
%% @private

find_matching_nodes(Tree, PathPattern, TreeOptions) ->
    fold(
      Tree, PathPattern,
      fun collect_node_props_cb/3, #{},
      TreeOptions).

-spec collect_node_props_cb(Path, NodeProps, Map) ->
    Ret when
      Path :: khepri_path:native_path(),
      NodeProps :: khepri:node_props(),
      Map :: khepri:node_props_map(),
      Ret :: Map.
%% @private

collect_node_props_cb(Path, NodeProps, Map) when is_map(Map) ->
    Map#{Path => NodeProps}.

-spec count_node_cb(Path, NodeProps, Count) ->
    Ret when
      Path :: khepri_path:native_path(),
      NodeProps :: khepri:node_props(),
      Count :: non_neg_integer(),
      Ret :: Count.
%% @private

count_node_cb(_Path, _NodeProps, Count) when is_integer(Count) ->
    Count + 1.

-spec fold(Tree, PathPattern, Fun, Acc, TreeOptions) ->
    Ret when
      Tree :: tree(),
      PathPattern :: khepri_path:native_pattern(),
      Fun :: khepri:fold_fun(),
      Acc :: khepri:fold_acc(),
      TreeOptions :: khepri:tree_options(),
      Ret :: khepri:ok(Acc) | khepri:error().
%% @doc Folds the given `Fun' over each tree node which matches the path
%% pattern.
%%
%% @private

fold(Tree, PathPattern, Fun, Acc, TreeOptions) ->
    WalkFun = fun(Path, Node, Acc1) ->
                      find_matching_nodes_cb(
                        Path, Node, Fun, Acc1, TreeOptions)
              end,
    Ret = walk_down_the_tree(
            Tree, PathPattern, TreeOptions, WalkFun, Acc),
    case Ret of
        {ok, NewTree, _AppliedChanges, Acc2} ->
            ?assertEqual(Tree, NewTree),
            {ok, Acc2};
        Error ->
            Error
    end.

find_matching_nodes_cb(Path, #node{} = Node, Fun, Acc, TreeOptions) ->
    NodeProps = gather_node_props(Node, TreeOptions),
    Acc1 = Fun(Path, NodeProps, Acc),
    {ok, keep, Acc1};
find_matching_nodes_cb(
  _,
  {interrupted, node_not_found = Reason, Info},
  _Fun, _Acc,
  #{expect_specific_node := true}) ->
    %% If we are collecting node properties (the result is a map) and the path
    %% targets a specific node which is not found, we return an error.
    %%
    %% If we are counting nodes, that's fine and the next function clause will
    %% run. The walk won't be interrupted.
    Reason1 = ?khepri_error(Reason, Info),
    {error, Reason1};
find_matching_nodes_cb(_, {interrupted, _, _}, _, Acc, _) ->
    {ok, keep, Acc}.

%% -------------------------------------------------------------------
%% Delete matching nodes.
%% -------------------------------------------------------------------

-spec delete_matching_nodes(Tree, PathPattern, AppliedChanges, TreeOptions) ->
    Ret when
      Tree :: khepri_tree:tree(),
      PathPattern :: khepri_path:pattern(),
      AppliedChanges :: applied_changes(),
      TreeOptions :: khepri:tree_options(),
      Ret :: {ok, NewTree, NewAppliedChanges, Result} | khepri:error(),
      NewTree :: khepri_tree:tree(),
      NewAppliedChanges :: applied_changes(),
      Result :: any().

delete_matching_nodes(Tree, PathPattern, AppliedChanges, TreeOptions) ->
    Fun = fun(Path, Node, Result) ->
                  delete_matching_nodes_cb(
                    Path, Node, TreeOptions, explicit, Result)
          end,
    walk_down_the_tree(
      Tree, PathPattern, TreeOptions, AppliedChanges, Fun, #{}).

delete_matching_nodes_cb(
  [] = Path, #node{} = Node, TreeOptions, DeleteReason, Result) ->
    Result1 = add_deleted_node_to_result(
                Path, Node, TreeOptions, DeleteReason, Result),
    Node1 = remove_node_payload(Node),
    Node2 = remove_node_child_nodes(Node1),
    {ok, Node2, Result1};
delete_matching_nodes_cb(
  Path, #node{} = Node, TreeOptions, DeleteReason, Result) ->
    Result1 = add_deleted_node_to_result(
                Path, Node, TreeOptions, DeleteReason, Result),
    {ok, delete, Result1};
delete_matching_nodes_cb(
  _, {interrupted, _, _}, _Options, _DeleteReason, Result) ->
    {ok, keep, Result}.

add_deleted_node_to_result(
  Path, Node, TreeOptions, explicit = DeleteReason, Result) ->
    add_deleted_node_to_result1(
      Path, Node, TreeOptions, DeleteReason, Result);
add_deleted_node_to_result(
  _Path, _Node, #{return_indirect_deletes := false}, _DeleteReason,
  Result) ->
    Result;
add_deleted_node_to_result(
  Path, Node, TreeOptions, DeleteReason, Result) ->
    add_deleted_node_to_result1(
      Path, Node, TreeOptions, DeleteReason, Result).

add_deleted_node_to_result1(
  Path, #node{child_nodes = Children} = Node, TreeOptions, DeleteReason,
  Result) ->
    NodeProps1 = gather_node_props(Node, TreeOptions),
    NodeProps2 = maybe_add_delete_reason_prop(
                   NodeProps1, TreeOptions, DeleteReason),
    Result1 = Result#{Path => NodeProps2},

    Result3 = maps:fold(
                fun(ChildName, ChildNode, Result2) ->
                        ChildPath = Path ++ [ChildName],
                        case Result2 of
                            #{ChildPath := _} ->
                                Result2;
                            _ ->
                                add_deleted_node_to_result1(
                                  ChildPath, ChildNode, TreeOptions,
                                  DeleteReason, Result2)
                        end
                end, Result1, Children),
    Result3.

maybe_add_delete_reason_prop(
  NodeProps, #{props_to_return := WantedProps}, DeleteReason) ->
    case lists:member(delete_reason, WantedProps) of
        true ->
            NodeProps#{delete_reason => DeleteReason};
        false ->
            NodeProps
    end;
maybe_add_delete_reason_prop(NodeProps, _TreeOptions, _DeleteReason) ->
    NodeProps.

%% -------------------------------------------------------------------
%% Insert or update a tree node.
%% -------------------------------------------------------------------

-spec insert_or_update_node(
    Tree, PathPattern, Payload, PutOptions, TreeOptions) -> Ret when
      Tree :: khepri_tree:tree(),
      PathPattern :: khepri_path:native_pattern(),
      Payload :: khepri_payload:payload(),
      PutOptions :: khepri:put_options(),
      TreeOptions :: khepri:tree_options(),
      NodeProps :: khepri:node_props_map(),
      AppliedChanges :: applied_changes(),
      Ret :: ok(Tree, AppliedChanges, NodeProps) | khepri:error().

insert_or_update_node(
  Tree, PathPattern, Payload, #{keep_while := KeepWhile}, TreeOptions) ->
    Fun = fun(Path, Node, {_, _, Result}) ->
                  Ret = insert_or_update_node_cb(
                          Path, Node, Payload, TreeOptions, Result),
                  case Ret of
                      {ok, Node1, Result1} when Result1 =/= #{} ->
                          AbsKeepWhile0 = to_absolute_keep_while(
                                            Path, KeepWhile),
                          AbsKeepWhile1 = (
                            filter_out_irrelevant_keep_while_conds(
                              Path, Node, AbsKeepWhile0)),
                          KWMet = are_keep_while_conditions_met(
                                    Tree, AbsKeepWhile1),
                          case KWMet of
                              true ->
                                  {ok, Node1, {updated, Path, Result1}};
                              {false, Reason} ->
                                  %% The keep_while condition is not met. We
                                  %% can't insert the node and return an
                                  %% error instead.
                                  NodeName = case Path of
                                                 [] -> ?KHEPRI_ROOT_NODE;
                                                 _  -> lists:last(Path)
                                             end,
                                  Reason1 = ?khepri_error(
                                               keep_while_conditions_not_met,
                                               #{node_name => NodeName,
                                                 node_path => Path,
                                                 keep_while_reason => Reason}),
                                  {error, Reason1}
                          end;
                      {ok, Node1, Result1} ->
                          {ok, Node1, {updated, Path, Result1}};
                      Error ->
                          Error
                  end
          end,
    Ret1 = walk_down_the_tree(
             Tree, PathPattern, TreeOptions, Fun, {undefined, [], #{}}),
    case Ret1 of
        {ok, Tree1, AppliedChanges, {updated, ResolvedPath, Ret2}} ->
            Tree2 = update_keep_while_conds(
                      Tree1, ResolvedPath, KeepWhile),
            {ok, Tree2, AppliedChanges, Ret2};
        Error ->
            ?assertMatch({error, _}, Error),
            Error
    end;
insert_or_update_node(
  Tree, PathPattern, Payload, _PutOptions, TreeOptions) ->
    Fun = fun(Path, Node, Result) ->
                  insert_or_update_node_cb(
                    Path, Node, Payload, TreeOptions, Result)
          end,
    walk_down_the_tree(Tree, PathPattern, TreeOptions, Fun, #{}).

insert_or_update_node_cb(
  Path, #node{} = Node, Payload, TreeOptions, Result) ->
    case maps:is_key(Path, Result) of
        false ->
            %% After a node is modified, we collect properties from the updated
            %% `#node{}', except the payload which is from the old one.
            Node1 = set_node_payload(Node, Payload),
            NodeProps = gather_node_props_from_old_and_new_nodes(
                          Node, Node1, TreeOptions),
            {ok, Node1, Result#{Path => NodeProps}};
        true ->
            {ok, Node, Result}
    end;
insert_or_update_node_cb(
  Path, {interrupted, node_not_found = Reason, Info}, Payload, TreeOptions,
  Result) ->
    %% We store the payload when we reached the target node only, not in the
    %% parent nodes we have to create in between.
    IsTarget = maps:get(node_is_target, Info),
    case can_continue_update_after_node_not_found(Info) of
        true when IsTarget ->
            Node = create_node_record(Payload),
            NodeProps = gather_node_props_from_old_and_new_nodes(
                          undefined, Node, TreeOptions),
            {ok, Node, Result#{Path => NodeProps}};
        true ->
            Node = create_node_record(khepri_payload:none()),
            {ok, Node, Result};
        false ->
            Reason1 = ?khepri_error(Reason, Info),
            {error, Reason1}
    end;
insert_or_update_node_cb(_, {interrupted, Reason, Info}, _, _, _) ->
    Reason1 = ?khepri_error(Reason, Info),
    {error, Reason1}.

filter_out_irrelevant_keep_while_conds(
  Path, {interrupted, node_not_found, #{node_is_target := true}}, KeepWhile) ->
    %% A `keep_while' condition on self is irrelevant if the node does not
    %% exist yet.
    KeepWhile1 = maps:remove(Path, KeepWhile),
    %% Likewise for `keep_while' conditions that apply to children of the
    %% node.
    KeepWhile2 = maps:filter(
                   fun(KWPath, _) ->
                           not lists:prefix(Path, KWPath)
                   end, KeepWhile1),
    KeepWhile2;
filter_out_irrelevant_keep_while_conds(
  _Path, _Node, KeepWhile) ->
    KeepWhile.

gather_node_props_from_old_and_new_nodes(OldNode, NewNode, TreeOptions) ->
    OldNodeProps = case OldNode of
                       undefined ->
                           #{};
                       _ ->
                           gather_node_props(OldNode, TreeOptions)
                   end,
    NewNodeProps0 = gather_node_props(NewNode, TreeOptions),
    NewNodeProps1 = maps:remove(data, NewNodeProps0),
    NewNodeProps2 = maps:remove(sproc, NewNodeProps1),
    maps:merge(OldNodeProps, NewNodeProps2).

can_continue_update_after_node_not_found(#{condition := Condition}) ->
    can_continue_update_after_node_not_found1(Condition);
can_continue_update_after_node_not_found(#{node_name := NodeName}) ->
    can_continue_update_after_node_not_found1(NodeName).

can_continue_update_after_node_not_found1(ChildName)
  when ?IS_KHEPRI_PATH_COMPONENT(ChildName) ->
    true;
can_continue_update_after_node_not_found1(#if_node_exists{exists = false}) ->
    true;
can_continue_update_after_node_not_found1(#if_all{conditions = Conds}) ->
    lists:all(fun can_continue_update_after_node_not_found1/1, Conds);
can_continue_update_after_node_not_found1(#if_any{conditions = Conds}) ->
    lists:any(fun can_continue_update_after_node_not_found1/1, Conds);
can_continue_update_after_node_not_found1(_) ->
    false.

%% -------------------------------------------------------------------
%% Does path match.
%% -------------------------------------------------------------------

does_path_match(Path, PathPattern, Tree) ->
    PathPattern1 = khepri_path:compile(PathPattern),
    does_path_match(Path, PathPattern1, [], Tree).

does_path_match(PathRest, PathRest, _ReversedPath, _Tree) ->
    true;
does_path_match([], _PathPatternRest, _ReversedPath, _Tree) ->
    false;
does_path_match(_PathRest, [], _ReversedPath, _Tree) ->
    false;
does_path_match(
  [Component | Path], [Component | PathPattern], ReversedPath, Tree)
  when ?IS_KHEPRI_PATH_COMPONENT(Component) ->
    does_path_match(Path, PathPattern, [Component | ReversedPath], Tree);
does_path_match(
  [Component | _Path], [Condition | _PathPattern], _ReversedPath, _Tree)
  when ?IS_KHEPRI_PATH_COMPONENT(Component) andalso
       ?IS_KHEPRI_PATH_COMPONENT(Condition) ->
    false;
does_path_match(
  [Component | Path], [Condition | PathPattern], ReversedPath, Tree) ->
    %% Query the tree node, required to evaluate the condition.
    ReversedPath1 = [Component | ReversedPath],
    CurrentPath = lists:reverse(ReversedPath1),
    TreeOptions = #{expect_specific_node => true,
                    props_to_return => [payload,
                                        payload_version,
                                        child_list_version,
                                        child_list_length]},
    {ok, #{CurrentPath := Node}} = find_matching_nodes(
                                     Tree,
                                     lists:reverse([Component | ReversedPath]),
                                     TreeOptions),
    case khepri_condition:is_met(Condition, Component, Node) of
        true ->
            ConditionMatchesGrandchildren =
            case khepri_condition:applies_to_grandchildren(Condition) of
                true ->
                    does_path_match(
                      Path, [Condition | PathPattern], ReversedPath1, Tree);
                false ->
                    false
            end,
            ConditionMatchesGrandchildren orelse
              does_path_match(Path, PathPattern, ReversedPath1, Tree);
        {false, _} ->
            false
    end.

%% -------------------------------------------------------------------
%% Tree traversal functions.
%% -------------------------------------------------------------------

-record(walk,
        {tree :: #tree{},
         node :: #node{} | delete,
         path_pattern :: khepri_path:native_pattern(),
         tree_options :: khepri:tree_options(),
         %% Used to remember the path of the node the walk is currently on.
         reversed_path = [] :: khepri_path:native_path(),
         %% Used to update parents up in the tree in a tail-recursive function.
         reversed_parent_tree = [] :: [#node{} | {#node{}, child_created}],
         'fun' :: walk_down_the_tree_fun(),
         fun_acc :: any(),
         applied_changes :: applied_changes()}).

-spec walk_down_the_tree(
        Tree, PathPattern, TreeOptions, Fun, FunAcc) -> Ret when
      Tree :: tree(),
      PathPattern :: khepri_path:native_pattern(),
      TreeOptions :: khepri:tree_options(),
      Fun :: walk_down_the_tree_fun(),
      FunAcc :: any(),
      AppliedChanges :: applied_changes(),
      Ret :: ok(Tree, AppliedChanges, FunAcc) | khepri:error().
%% @private

walk_down_the_tree(Tree, PathPattern, TreeOptions, Fun, FunAcc) ->
    walk_down_the_tree(Tree, PathPattern, TreeOptions, #{}, Fun, FunAcc).

-spec walk_down_the_tree(
        Tree, PathPattern, TreeOptions,
        AppliedChanges, Fun, FunAcc) -> Ret when
      Tree :: tree(),
      PathPattern :: khepri_path:native_pattern(),
      TreeOptions :: khepri:tree_options(),
      AppliedChanges :: applied_changes(),
      Fun :: walk_down_the_tree_fun(),
      FunAcc :: any(),
      Ret :: ok(Tree, AppliedChanges, FunAcc) | khepri:error().
%% @private

walk_down_the_tree(
  Tree, PathPattern, TreeOptions, AppliedChanges, Fun, FunAcc) ->
    CompiledPathPattern = khepri_path:compile(PathPattern),
    TreeOptions1 = case TreeOptions of
                       #{expect_specific_node := true} ->
                           TreeOptions;
                       _ ->
                           TreeOptions#{expect_specific_node => false}
                   end,
    Walk = #walk{tree = Tree,
                 node = Tree#tree.root,
                 path_pattern = CompiledPathPattern,
                 tree_options = TreeOptions1,
                 'fun' = Fun,
                 fun_acc = FunAcc,
                 applied_changes = AppliedChanges},
    case walk_down_the_tree1(Walk) of
        {ok, #walk{tree = Tree1,
                   node = Root1,
                   applied_changes = AppliedChanges1,
                   fun_acc = FunAcc1}} ->
            Tree2 = Tree1#tree{root = Root1},
            {ok, Tree2, AppliedChanges1, FunAcc1};
        Error ->
            Error
    end.

-spec walk_down_the_tree1(Walk) -> Ret when
      Walk :: #walk{},
      Ret :: khepri:ok(Walk) | khepri:error().
%% @private

walk_down_the_tree1(
  #walk{path_pattern = [?KHEPRI_ROOT_NODE | PathPattern],
        reversed_path = ReversedPath,
        reversed_parent_tree = ReversedParentTree} = Walk) ->
    ?assertEqual([], ReversedPath),
    ?assertEqual([], ReversedParentTree),
    Walk1 = Walk#walk{path_pattern = PathPattern},
    walk_down_the_tree1(Walk1);
walk_down_the_tree1(
  #walk{path_pattern = [?THIS_KHEPRI_NODE | PathPattern]} = Walk) ->
    Walk1 = Walk#walk{path_pattern = PathPattern},
    walk_down_the_tree1(Walk1);
walk_down_the_tree1(
  #walk{path_pattern = [?PARENT_KHEPRI_NODE | PathPattern],
        reversed_path = [_CurrentName | ReversedPath],
        reversed_parent_tree = [ParentNode0 | ReversedParentTree]} = Walk) ->
    ParentNode = case ParentNode0 of
                     {PN, child_created} -> PN;
                     _                   -> ParentNode0
                 end,
    Walk1 = Walk#walk{node = ParentNode,
                      path_pattern = PathPattern,
                      reversed_path = ReversedPath,
                      reversed_parent_tree = ReversedParentTree},
    walk_down_the_tree1(Walk1);
walk_down_the_tree1(
  #walk{path_pattern = [?PARENT_KHEPRI_NODE | PathPattern],
        reversed_path = [],
        reversed_parent_tree = []} = Walk) ->
    %% The path tries to go above the root node, like "cd /..". In this case,
    %% we stay on the root node.
    Walk1 = Walk#walk{path_pattern = PathPattern},
    walk_down_the_tree1(Walk1);
walk_down_the_tree1(
  #walk{node = #node{child_nodes = Children} = CurrentNode,
        path_pattern = [ChildName | PathPattern],
        reversed_path = ReversedPath,
        reversed_parent_tree = ReversedParentTree} = Walk)
  when ?IS_KHEPRI_NODE_ID(ChildName) ->
    Walk1 = Walk#walk{path_pattern = PathPattern,
                      reversed_path = [ChildName | ReversedPath],
                      reversed_parent_tree =
                      [CurrentNode | ReversedParentTree]},
    case Children of
        #{ChildName := Child} ->
            Walk2 = Walk1#walk{node = Child},
            walk_down_the_tree1(Walk2);
        _ ->
            interrupted_walk_down(
              Walk1, node_not_found,
              #{node_name => ChildName,
                node_path => lists:reverse([ChildName | ReversedPath])})
    end;
walk_down_the_tree1(
  #walk{node = #node{child_nodes = Children} = CurrentNode,
        path_pattern = [Condition | PathPattern],
        tree_options = #{expect_specific_node := true},
        reversed_path = ReversedPath,
        reversed_parent_tree = ReversedParentTree} = Walk)
  when ?IS_KHEPRI_CONDITION(Condition) ->
    %% We distinguish the case where the condition must be verified against the
    %% current node (i.e. the node name is ?KHEPRI_ROOT_NODE or
    %% ?THIS_KHEPRI_NODE in the condition) instead of its child nodes.
    SpecificNode = khepri_path:component_targets_specific_node(Condition),
    case SpecificNode of
        {true, NodeName}
          when NodeName =:= ?KHEPRI_ROOT_NODE orelse
               NodeName =:= ?THIS_KHEPRI_NODE ->
            CurrentName = special_component_to_node_name(
                            NodeName, ReversedPath),
            CondMet = khepri_condition:is_met(
                        Condition, CurrentName, CurrentNode),
            case CondMet of
                true ->
                    Walk1 = Walk#walk{path_pattern = PathPattern},
                    walk_down_the_tree1(Walk1);
                {false, Cond} ->
                    Walk1 = Walk#walk{path_pattern = PathPattern},
                    interrupted_walk_down(
                      Walk1, mismatching_node,
                      #{node_name => CurrentName,
                        node_path => lists:reverse(ReversedPath),
                        node_props => gather_node_props_for_error(CurrentNode),
                        condition => Cond})
            end;
        {true, ChildName} when ChildName =/= ?PARENT_KHEPRI_NODE ->
            Walk1 = Walk#walk{path_pattern = PathPattern,
                              reversed_path = [ChildName | ReversedPath],
                              reversed_parent_tree =
                              [CurrentNode | ReversedParentTree]},
            case Children of
                #{ChildName := Child} ->
                    Walk2 = Walk1#walk{node = Child},
                    CondMet = khepri_condition:is_met(
                                Condition, ChildName, Child),
                    case CondMet of
                        true ->
                            walk_down_the_tree1(Walk2);
                        {false, Cond} ->
                            interrupted_walk_down(
                              Walk2, mismatching_node,
                              #{node_name => ChildName,
                                node_path => lists:reverse(
                                               [ChildName | ReversedPath]),
                                node_props => gather_node_props_for_error(
                                                Child),
                                condition => Cond})
                    end;
                _ ->
                    interrupted_walk_down(
                      Walk1, node_not_found,
                      #{node_name => ChildName,
                        node_path => lists:reverse([ChildName | ReversedPath]),
                        condition => Condition})
            end;
        {true, ?PARENT_KHEPRI_NODE} ->
            %% TODO: Support calling Fun() with parent node based on
            %% conditions on child nodes.
            BadPathPattern =
            lists:reverse(ReversedPath, [Condition | PathPattern]),
            Exception = ?khepri_exception(
                           condition_targets_parent_node,
                           #{path => BadPathPattern,
                             condition => Condition}),
            {error, Exception};
        false ->
            %% The caller expects that the path matches a single specific node
            %% (no matter if it exists or not), but the condition could match
            %% several nodes.
            BadPathPattern =
            lists:reverse(ReversedPath, [Condition | PathPattern]),
            Exception = ?khepri_exception(
                           possibly_matching_many_nodes_denied,
                           #{path => BadPathPattern}),
            {error, Exception}
    end;
walk_down_the_tree1(
  #walk{node = #node{child_nodes = Children} = CurrentNode,
        path_pattern = [Condition | PathPattern] = WholePathPattern,
        tree_options = #{expect_specific_node := false} = TreeOptions,
        reversed_path = ReversedPath,
        reversed_parent_tree = ReversedParentTree,
        applied_changes = AppliedChanges} = Walk)
  when ?IS_KHEPRI_CONDITION(Condition) ->
    %% Like with "expect_specific_node =:= true" function clause above, We
    %% distinguish the case where the condition must be verified against the
    %% current node (i.e. the node name is ?KHEPRI_ROOT_NODE or
    %% ?THIS_KHEPRI_NODE in the condition) instead of its child nodes.
    SpecificNode = khepri_path:component_targets_specific_node(Condition),
    case SpecificNode of
        {true, NodeName}
          when NodeName =:= ?KHEPRI_ROOT_NODE orelse
               NodeName =:= ?THIS_KHEPRI_NODE ->
            CurrentName = special_component_to_node_name(
                            NodeName, ReversedPath),
            CondMet = khepri_condition:is_met(
                        Condition, CurrentName, CurrentNode),
            case CondMet of
                true ->
                    Walk1 = Walk#walk{path_pattern = PathPattern},
                    walk_down_the_tree1(Walk1);
                {false, _} ->
                    StartingNode = starting_node_in_rev_parent_tree(
                                     ReversedParentTree, CurrentNode),
                    Walk1 = Walk#walk{node = StartingNode},
                    {ok, Walk1}
            end;
        {true, ?PARENT_KHEPRI_NODE} ->
            %% TODO: Support calling Fun() with parent node based on
            %% conditions on child nodes.
            BadPathPattern =
            lists:reverse(ReversedPath, [Condition | PathPattern]),
            Exception = ?khepri_exception(
                           condition_targets_parent_node,
                           #{path => BadPathPattern,
                             condition => Condition}),
            {error, Exception};
        _ ->
            %% There is a special case if the current node is the root node.
            %% The caller can request that the root node's properties are
            %% included. This is true by default if the path is `[]'. This
            %% allows to get its props and payload atomically in a single
            %% query.
            IsRoot = ReversedPath =:= [],
            IncludeRootProps = maps:get(
                                 include_root_props, TreeOptions, false),
            Ret0 = case IsRoot andalso IncludeRootProps of
                       true ->
                           Walk1 = Walk#walk{path_pattern = [],
                                             reversed_path = [],
                                             reversed_parent_tree = []},
                           walk_down_the_tree1(Walk1);
                       _ ->
                           {ok, Walk}
                   end,

            %% The result of the first part (the special case for the root
            %% node if relevant) is used as a starting point for handling all
            %% child nodes.
            Ret1 = maps:fold(
                     fun
                         (ChildName, Child,
                          {ok, Walk1}) ->
                             Walk2 = Walk1#walk{path_pattern =
                                                WholePathPattern,
                                                reversed_path = ReversedPath},
                             handle_branch(Walk2, ChildName, Child);
                         (_, _, Error) ->
                             Error
                     end, Ret0, Children),
            case Ret1 of
                {ok,
                 #walk{node = CurrentNode,
                       applied_changes = AppliedChanges} = Walk2} ->
                    %% The current node didn't change, no need to update the
                    %% tree and evaluate keep_while conditions.
                    StartingNode = starting_node_in_rev_parent_tree(
                                     ReversedParentTree, CurrentNode),
                    Walk3 = Walk2#walk{node = StartingNode},
                    {ok, Walk3};
                {ok, #walk{node = CurrentNode1} = Walk2} ->
                    CurrentNode2 = case CurrentNode1 of
                                       CurrentNode ->
                                           CurrentNode;
                                       delete ->
                                           CurrentNode1;
                                       _ ->
                                           %% Because of the loop, payload &
                                           %% child list versions may have
                                           %% been increased multiple times.
                                           %% We want them to increase once
                                           %% for the whole (atomic)
                                           %% operation.
                                           squash_version_bumps(
                                             CurrentNode, CurrentNode1)
                                   end,
                    Walk3 = Walk2#walk{node = CurrentNode2,
                                       reversed_path = ReversedPath,
                                       reversed_parent_tree =
                                       ReversedParentTree},
                    walk_back_up_the_tree(Walk3);
                Error ->
                    Error
            end
    end;
walk_down_the_tree1(
  #walk{node = #node{} = CurrentNode,
        path_pattern = [],
        'fun' = Fun,
        fun_acc = FunAcc,
        reversed_path = ReversedPath,
        reversed_parent_tree = ReversedParentTree} = Walk) ->
    CurrentPath = lists:reverse(ReversedPath),
    case Fun(CurrentPath, CurrentNode, FunAcc) of
        {ok, keep, FunAcc1} ->
            StartingNode = starting_node_in_rev_parent_tree(
                             ReversedParentTree, CurrentNode),
            Walk1 = Walk#walk{node = StartingNode, fun_acc = FunAcc1},
            {ok, Walk1};
        {ok, delete, FunAcc1} ->
            Walk1 = Walk#walk{node = delete, fun_acc = FunAcc1},
            walk_back_up_the_tree(Walk1);
        {ok, #node{} = CurrentNode1, FunAcc1} ->
            Walk1 = Walk#walk{node = CurrentNode1, fun_acc = FunAcc1},
            walk_back_up_the_tree(Walk1);
        Error ->
            Error
    end.

-spec special_component_to_node_name(SpecialComponent, ReversedPath) ->
    NodeName when
      SpecialComponent :: ?KHEPRI_ROOT_NODE | ?THIS_KHEPRI_NODE,
      ReversedPath :: khepri_path:native_path(),
      NodeName :: khepri_path:component().

special_component_to_node_name(?KHEPRI_ROOT_NODE = NodeName, []) ->
    NodeName;
special_component_to_node_name(?THIS_KHEPRI_NODE, [NodeName | _]) ->
    NodeName;
special_component_to_node_name(?THIS_KHEPRI_NODE, []) ->
    ?KHEPRI_ROOT_NODE.

-spec starting_node_in_rev_parent_tree(ReversedParentTree) -> Node when
      Node :: tree_node(),
      ReversedParentTree :: [Node].
%% @private

starting_node_in_rev_parent_tree(ReversedParentTree) ->
    hd(lists:reverse(ReversedParentTree)).

-spec starting_node_in_rev_parent_tree(ReversedParentTree, Node) -> Node when
      Node :: tree_node(),
      ReversedParentTree :: [Node].
%% @private

starting_node_in_rev_parent_tree([], CurrentNode) ->
    CurrentNode;
starting_node_in_rev_parent_tree(ReversedParentTree, _) ->
    starting_node_in_rev_parent_tree(ReversedParentTree).

-spec handle_branch(Walk, ChildName, Child) -> Ret when
      Walk :: #walk{},
      ChildName :: khepri_path:component(),
      Child :: tree_node(),
      Ret :: khepri:ok(Walk) | khepri:error().
%% @private

handle_branch(
  #walk{node = CurrentNode,
        path_pattern = [Condition | PathPattern] = WholePathPattern,
        reversed_path = ReversedPath} = Walk,
  ChildName, Child) ->
    CondMet = khepri_condition:is_met(Condition, ChildName, Child),
    Ret = case CondMet of
              true ->
                  Walk1 = Walk#walk{node = Child,
                                    path_pattern = PathPattern,
                                    reversed_path = [ChildName | ReversedPath],
                                    reversed_parent_tree = [CurrentNode]},
                  walk_down_the_tree1(Walk1);
              {false, _} ->
                  {ok, Walk}
          end,
    case Ret of
        {ok,
         #walk{node = #node{child_nodes = Children} = CurrentNode1} = Walk2}
         when is_map_key(ChildName, Children) ->
            case khepri_condition:applies_to_grandchildren(Condition) of
                false ->
                    Ret;
                true ->
                    Walk3 = Walk2#walk{node = Child,
                                       path_pattern = WholePathPattern,
                                       reversed_path =
                                       [ChildName | ReversedPath],
                                       reversed_parent_tree = [CurrentNode1]},
                    walk_down_the_tree1(Walk3)
            end;
        {ok, _Walk} ->
            %% The child node is gone, no need to test if the condition
            %% applies to it or recurse.
            Ret;
        Error ->
            Error
    end.

-spec interrupted_walk_down(Walk, Reason, Info) -> Ret when
      Walk :: #walk{},
      Reason :: mismatching_node | node_not_found,
      Info :: map(),
      Ret :: khepri:ok(Walk) | khepri:error().
%% @private

interrupted_walk_down(
  #walk{tree = Tree,
        path_pattern = PathPattern,
        reversed_path = ReversedPath,
        reversed_parent_tree = ReversedParentTree,
        'fun' = Fun,
        fun_acc = FunAcc} = Walk,
  Reason, Info) ->
    NodePath = lists:reverse(ReversedPath),
    IsTarget = khepri_path:realpath(PathPattern) =:= [],
    Info1 = Info#{node_is_target => IsTarget},
    ErrorTuple = {interrupted, Reason, Info1},
    case Fun(NodePath, ErrorTuple, FunAcc) of
        {ok, ToDo, FunAcc1}
          when ToDo =:= keep orelse ToDo =:= delete ->
            ?assertNotEqual([], ReversedParentTree),
            StartingNode = starting_node_in_rev_parent_tree(
                             ReversedParentTree),
            Walk1 = Walk#walk{node = StartingNode,
                              fun_acc = FunAcc1},
            {ok, Walk1};
        {ok, #node{} = NewNode, FunAcc1} ->
            ReversedParentTree1 =
            case Reason of
                node_not_found ->
                    %% We record the fact the child is a new node. This is used
                    %% to reset the child's stats if it got new payload or
                    %% child nodes at the same time.
                    [{hd(ReversedParentTree), child_created}
                     | tl(ReversedParentTree)];
                _ ->
                    ReversedParentTree
            end,
            case PathPattern of
                [] ->
                    %% We reached the target node. We could call
                    %% walk_down_the_tree1() again, but it would call Fun() a
                    %% second time.
                    Walk1 = Walk#walk{node = NewNode,
                                      reversed_parent_tree =
                                      ReversedParentTree1,
                                      fun_acc = FunAcc1},
                    walk_back_up_the_tree(Walk1);
                _ ->
                    %% We created a tree node automatically on our way to the
                    %% target. We want to add a `keep_while' condition for it
                    %% so it is automatically reclaimed when it becomes
                    %% useless (i.e., no payload and no child nodes).
                    Cond = #if_any{conditions =
                                   [#if_child_list_length{count = {gt, 0}},
                                    #if_has_payload{has_payload = true}]},
                    KeepWhile = #{NodePath => Cond},
                    Tree1 = update_keep_while_conds(Tree, NodePath, KeepWhile),

                    Walk1 = Walk#walk{tree = Tree1,
                                      node = NewNode,
                                      reversed_parent_tree =
                                      ReversedParentTree1,
                                      fun_acc = FunAcc1},
                    walk_down_the_tree1(Walk1)
            end;
        Error ->
            Error
    end.

-spec walk_back_up_the_tree(Walk) -> Ret when
      Walk :: #walk{},
      Ret :: khepri:ok(Walk).
%% @private

walk_back_up_the_tree(Walk) ->
    walk_back_up_the_tree(Walk, #{}).

-spec walk_back_up_the_tree(Walk, AppliedChangesAcc) -> Ret when
      Walk :: #walk{},
      AppliedChangesAcc :: applied_changes(),
      Ret :: khepri:ok(Walk).
%% @private

walk_back_up_the_tree(
  #walk{node = delete,
        reversed_path = [ChildName | ReversedPath] = WholeReversedPath,
        reversed_parent_tree = [ParentNode | ReversedParentTree]} = Walk,
  AppliedChangesAcc) ->
    %% Evaluate keep_while of nodes which depended on ChildName (it is
    %% removed) at the end of walk_back_up_the_tree().
    Path = lists:reverse(WholeReversedPath),
    AppliedChangesAcc1 = AppliedChangesAcc#{Path => delete},

    %% All children of `Path' are also added recursively to the
    %% `AppliedChangesAcc1' map.
    #node{child_nodes = Children} = ParentNode,
    ChildNode = maps:get(ChildName, Children),
    AppliedChangesAcc2 = list_deleted_nodes_recursively_from(
                           Path, ChildNode, AppliedChangesAcc1),

    %% Evaluate keep_while of parent node on itself right now (its child_count
    %% has changed).
    ParentNode1 = remove_node_child(ParentNode, ChildName),

    Walk1 = Walk#walk{node = ParentNode1,
                      reversed_path = ReversedPath,
                      reversed_parent_tree = ReversedParentTree},
    handle_keep_while_for_parent_update(Walk1, AppliedChangesAcc2);
walk_back_up_the_tree(
  #walk{node = Child,
        reversed_path = [ChildName | ReversedPath] = WholeReversedPath,
        reversed_parent_tree =
        [{ParentNode, child_created} | ReversedParentTree]} = Walk,
  AppliedChangesAcc) ->
    Child1 = reset_versions(Child),

    %% Evaluate keep_while of nodes which depend on ChildName (it is
    %% created) at the end of walk_back_up_the_tree().
    Path = lists:reverse(WholeReversedPath),
    TreeOptions = #{props_to_return => [payload,
                                        payload_version,
                                        child_list_version,
                                        child_list_length]},
    NodeProps = gather_node_props(Child1, TreeOptions),
    AppliedChangesAcc1 = AppliedChangesAcc#{Path => {create, NodeProps}},

    %% Evaluate keep_while of parent node on itself right now (its child_count
    %% has changed).
    ParentNode1 = add_node_child(ParentNode, ChildName, Child1),
    Walk1 = Walk#walk{node = ParentNode1,
                      reversed_path = ReversedPath,
                      reversed_parent_tree = ReversedParentTree},
    handle_keep_while_for_parent_update(Walk1, AppliedChangesAcc1);
walk_back_up_the_tree(
  #walk{node = Child,
        reversed_path = [ChildName | ReversedPath] = WholeReversedPath,
        reversed_parent_tree = [ParentNode | ReversedParentTree]} = Walk,
  AppliedChangesAcc) ->
    %% Evaluate keep_while of nodes which depend on ChildName (it is
    %% modified) at the end of walk_back_up_the_tree().
    Path = lists:reverse(WholeReversedPath),
    TreeOptions = #{props_to_return => [payload,
                                        payload_version,
                                        child_list_version,
                                        child_list_length]},
    InitialNodeProps = gather_node_props(
                         maps:get(ChildName, ParentNode#node.child_nodes),
                         TreeOptions),
    NodeProps = gather_node_props(Child, TreeOptions),
    AppliedChangesAcc1 = case NodeProps of
                             InitialNodeProps ->
                                 AppliedChangesAcc;
                             _ ->
                                 AppliedChangesAcc#{
                                   Path => {update, NodeProps}}
                         end,

    %% No need to evaluate keep_while of ParentNode, its child_count is
    %% unchanged.
    ParentNode1 = update_node_child(ParentNode, ChildName, Child),
    Walk1 = Walk#walk{node = ParentNode1,
                      reversed_path = ReversedPath,
                      reversed_parent_tree = ReversedParentTree},
    walk_back_up_the_tree(Walk1, AppliedChangesAcc1);
walk_back_up_the_tree(
  #walk{reversed_path = [], %% <-- We reached the root (i.e. not a branch,
                            %% see handle_branch())
        reversed_parent_tree = [],
        applied_changes = AppliedChanges} = Walk,
  AppliedChangesAcc) ->
    AppliedChanges1 = merge_applied_changes(AppliedChanges, AppliedChangesAcc),
    Walk1 = Walk#walk{applied_changes = AppliedChanges1},
    handle_applied_changes(Walk1);
walk_back_up_the_tree(
  #walk{reversed_parent_tree = [],
        applied_changes = AppliedChanges} = Walk,
  AppliedChangesAcc) ->
    AppliedChanges1 = merge_applied_changes(AppliedChanges, AppliedChangesAcc),
    Walk1 = Walk#walk{applied_changes = AppliedChanges1},
    {ok, Walk1}.

handle_keep_while_for_parent_update(
  #walk{reversed_parent_tree = [{_GrandParentNode, child_created} | _]} = Walk,
  AppliedChangesAcc) ->
    %% This is a freshly created node, we don't want to get rid of it right
    %% away.
    walk_back_up_the_tree(Walk, AppliedChangesAcc);
handle_keep_while_for_parent_update(
  #walk{tree = Tree,
        node = ParentNode,
        reversed_path = ReversedPath,
        tree_options = TreeOptions,
        fun_acc = Acc} = Walk,
  AppliedChangesAcc) ->
    ParentPath = lists:reverse(ReversedPath),
    IsMet = is_keep_while_condition_met_on_self(
              Tree, ParentPath, ParentNode),
    case IsMet of
        true ->
            %% We continue with the update.
            walk_back_up_the_tree(Walk, AppliedChangesAcc);
        {false, _Reason} ->
            %% This parent node must be removed because it doesn't meet its
            %% own keep_while condition. keep_while conditions for nodes
            %% depending on this one will be evaluated with the recursion.
            {ok, delete, Acc1} = delete_matching_nodes_cb(
                                   ParentPath, ParentNode,
                                   TreeOptions, keep_while, Acc),
            Walk1 = Walk#walk{node = delete,
                              fun_acc = Acc1},
            walk_back_up_the_tree(Walk1, AppliedChangesAcc)
    end.

-spec list_deleted_nodes_recursively_from(Path, Node, AppliedChangesAcc) ->
    NewAppliedChangesAcc when
      Path :: khepri_path:native_path(),
      Node :: tree_node(),
      AppliedChangesAcc :: applied_changes(),
      NewAppliedChangesAcc :: applied_changes().
%% @doc Augment the `AppliedChangesAcc' map with all the tree nodes that were
%% deleted as a consequence of the deletion of a parent.
%%
%% @private

list_deleted_nodes_recursively_from(
  Path, #node{child_nodes = Children}, AppliedChangesAcc) ->
    maps:fold(
      fun(ChildName, ChildNode, AppliedChangesAcc1) ->
              ChildPath = Path ++ [ChildName],
              AppliedChangesAcc2 = AppliedChangesAcc1#{ChildPath => delete},
              list_deleted_nodes_recursively_from(
                ChildPath, ChildNode, AppliedChangesAcc2)
      end, AppliedChangesAcc, Children).

merge_applied_changes(AppliedChanges1, AppliedChanges2) ->
    maps:fold(
      fun
          (Path, delete, KWA1) ->
              KWA1#{Path => delete};
          (Path, {_, _} = TypeAndNodeProps, KWA1) ->
              case KWA1 of
                  #{Path := delete} -> KWA1;
                  _                 -> KWA1#{Path => TypeAndNodeProps}
              end
      end, AppliedChanges1, AppliedChanges2).

handle_applied_changes(
  #walk{applied_changes = AppliedChanges} = Walk)
  when AppliedChanges =:= #{} ->
    {ok, Walk};
handle_applied_changes(
  #walk{tree = Tree,
        node = Root,
        applied_changes = AppliedChanges} = Walk) ->
    Tree1 = Tree#tree{root = Root},
    ToDelete = eval_keep_while_conditions(Tree1, AppliedChanges),

    Tree2 = maps:fold(
              fun
                  (RemovedPath, delete, T) ->
                      KW1 = maps:remove(
                              RemovedPath, T#tree.keep_while_conds),
                      T1 = update_keep_while_conds_revidx(T, RemovedPath, #{}),
                      T1#tree{keep_while_conds = KW1};
                  (_, {_, _}, T) ->
                      T
              end, Tree1, AppliedChanges),

    ToDelete1 = filter_and_sort_paths_to_delete(ToDelete, AppliedChanges),
    Walk1 = Walk#walk{tree = Tree2},
    remove_expired_nodes(ToDelete1, Walk1).

eval_keep_while_conditions(
  #tree{keep_while_conds_revidx = KeepWhileCondsRevIdx} = Tree,
  AppliedChanges) ->
    %% AppliedChanges lists all nodes which were modified or removed. We
    %% want to transform that into a list of nodes to remove.
    %%
    %% Those marked as `delete' in AppliedChanges are already gone. We
    %% need to find the nodes which depended on them, i.e. their keep_while
    %% condition is not met anymore. Note that removed nodes' child nodes are
    %% gone as well and must be handled (they are not specified in
    %% AppliedChanges).
    %%
    %% Those modified in AppliedChanges must be evaluated again to decide
    %% if they should be removed.
    case is_v1_keep_while_conds_revidx(KeepWhileCondsRevIdx) of
        true ->
            eval_keep_while_conditions_v1(Tree, AppliedChanges);
        false ->
            eval_keep_while_conditions_v0(Tree, AppliedChanges)
    end.

eval_keep_while_conditions_v0(
  #tree{keep_while_conds_revidx = KeepWhileCondsRevIdx} = Tree,
  AppliedChanges) ->
    maps:fold(
      fun
          (RemovedPath, delete, ToDelete) ->
              maps:fold(
                fun(Path, Watchers, ToDelete1) ->
                        case lists:prefix(RemovedPath, Path) of
                            true ->
                                eval_keep_while_conditions_after_removal(
                                  Tree, Watchers, ToDelete1);
                            false ->
                                ToDelete1
                        end
                end, ToDelete, KeepWhileCondsRevIdx);
          (UpdatedPath, {_Type, NodeProps}, ToDelete) ->
              case KeepWhileCondsRevIdx of
                  #{UpdatedPath := Watchers} ->
                      eval_keep_while_conditions_after_update(
                        Tree, UpdatedPath, NodeProps, Watchers, ToDelete);
                  _ ->
                      ToDelete
              end
      end, #{}, AppliedChanges).

eval_keep_while_conditions_v1(
  #tree{keep_while_conds_revidx = KeepWhileCondsRevIdx} = Tree,
  AppliedChanges) ->
    maps:fold(
      fun
          (RemovedPath, delete, ToDelete) ->
              khepri_prefix_tree:fold_prefixes_of(
                fun(Watchers, ToDelete1) ->
                        eval_keep_while_conditions_after_removal(
                          Tree, Watchers, ToDelete1)
                end, ToDelete, RemovedPath, KeepWhileCondsRevIdx);
          (UpdatedPath, {_Type, NodeProps}, ToDelete) ->
              Result = khepri_prefix_tree:find_path(
                         UpdatedPath, KeepWhileCondsRevIdx),
              case Result of
                  {ok, Watchers} ->
                      eval_keep_while_conditions_after_update(
                        Tree, UpdatedPath, NodeProps, Watchers, ToDelete);
                  error ->
                      ToDelete
              end
      end, #{}, AppliedChanges).

eval_keep_while_conditions_after_update(
  #tree{keep_while_conds = KeepWhileConds} = Tree,
  UpdatedPath, NodeProps, Watchers, ToDelete) ->
    maps:fold(
      fun(Watcher, ok, ToDelete1) ->
              KeepWhile = maps:get(Watcher, KeepWhileConds),
              CondOnUpdated = maps:get(UpdatedPath, KeepWhile),
              IsMet = khepri_condition:is_met(
                        CondOnUpdated, UpdatedPath, NodeProps),
              case IsMet of
                  true ->
                      ToDelete1;
                  {false, _} ->
                      case are_keep_while_conditions_met(Tree, KeepWhile) of
                          true       -> ToDelete1;
                          {false, _} -> ToDelete1#{Watcher => delete}
                      end
              end
      end, ToDelete, Watchers).

eval_keep_while_conditions_after_removal(
  #tree{keep_while_conds = KeepWhileConds} = Tree,
  Watchers, ToDelete) ->
    maps:fold(
      fun(Watcher, ok, ToDelete1) ->
              KeepWhile = maps:get(Watcher, KeepWhileConds),
              case are_keep_while_conditions_met(Tree, KeepWhile) of
                  true       -> ToDelete1;
                  {false, _} -> ToDelete1#{Watcher => delete}
              end
      end, ToDelete, Watchers).

filter_and_sort_paths_to_delete(ToDelete, AppliedChanges) ->
    Paths1 = lists:sort(
               fun
                   (A, B) when length(A) =:= length(B) ->
                       A < B;
                   (A, B) ->
                       length(A) < length(B)
               end,
               maps:keys(ToDelete)),
    Paths2 = lists:foldl(
               fun(Path, Map) ->
                       case AppliedChanges of
                           #{Path := delete} ->
                               Map;
                           _ ->
                               case is_parent_being_removed(Path, Map) of
                                   false -> Map#{Path => delete};
                                   true  -> Map
                               end
                       end
               end, #{}, Paths1),
    maps:keys(Paths2).

is_parent_being_removed([], _) ->
    false;
is_parent_being_removed(Path, Map) ->
    is_parent_being_removed1(lists:reverse(Path), Map).

is_parent_being_removed1([_ | Parent], Map) ->
    case maps:is_key(lists:reverse(Parent), Map) of
        true  -> true;
        false -> is_parent_being_removed1(Parent, Map)
    end;
is_parent_being_removed1([], _) ->
    false.

remove_expired_nodes([], Walk) ->
    {ok, Walk};
remove_expired_nodes(PathsToDelete, Walk) ->
    %% Sometimes after a `keep_while' condition "expires", we end up deleting
    %% many siblings. Or we want to delete many children of a tree node that
    %% is also deleted.
    %%
    %% Here, we convert this list of paths to a list of path patterns, trying
    %% to gather several siblings together in a single pattern, or entirely
    %% eliminate paths to children of a deleted tree node.
    PatternsToDelete = paths_to_patterns(PathsToDelete),
    remove_expired_nodes1(PatternsToDelete, Walk).

-spec paths_to_patterns(PathsToDelete) -> PatternsToDelete when
      PathsToDelete :: [khepri_path:native_path()],
      PatternsToDelete :: [khepri_path:native_pattern()].
%% @private

paths_to_patterns(PathsToDelete) ->
    %% This sort here is important: shorter paths, and thus parent paths will
    %% be considered first before their children in the computation below.
    %% This allows to skip childrend when a (grand-)parent is already
    %% scheduled for deletion.
    PathsToDelete1 = lists:sort(PathsToDelete),
    paths_to_patterns(PathsToDelete1, #{}).

paths_to_patterns([PathToDelete | Rest], PatternsToDelete) ->
    PatternsToDelete1 = path_to_pattern(PathToDelete, PatternsToDelete),
    paths_to_patterns(Rest, PatternsToDelete1);
paths_to_patterns([], PatternsToDelete) ->
    PatternsToDelete1 = maps:fold(
                          fun
                              (ParentPath, [Component], Acc) ->
                                  %% Only one child is deleted. Let's convert
                                  %% it back to a regular path.
                                  Path = ParentPath ++ [Component],
                                  [Path | Acc];
                              (ParentPath, Siblings, Acc) ->
                                  %% Many siblings are deleted. We use a
                                  %% single pattern that matches any siblings.
                                  Pattern0 = #if_any{conditions = Siblings},
                                  Pattern1 = ParentPath ++ [Pattern0],
                                  [Pattern1 | Acc]
                          end, [], PatternsToDelete),
    PatternsToDelete1.

path_to_pattern(PathToDelete, PatternsToDelete) ->
    ParentPath = [],
    path_to_pattern(PathToDelete, ParentPath, PatternsToDelete).

path_to_pattern([Component | Rest], ParentPath, PatternsToDelete) ->
    Siblings = maps:get(ParentPath, PatternsToDelete, []),
    case Rest of
        [_ | _] ->
            case lists:member(Component, Siblings) of
                true ->
                    %% A parent is already scheduled for deletion. We can skip
                    %% this path because it's a child that will go away with
                    %% the parent.
                    PatternsToDelete;
                false ->
                    %% We are in the middle of the path to delete and this
                    %% parent is not deleted. Let's continue with the next
                    %% component.
                    ThisPath = ParentPath ++ [Component],
                    path_to_pattern(Rest, ThisPath, PatternsToDelete)
            end;
        [] ->
            %% We reached the last component of a path.
            %%
            %% We add it to a list of siblings under the parent's path. Later,
            %% we can convert this to an actual pattern.
            Siblings1 = [Component | Siblings],
            PatternsToDelete1 = PatternsToDelete#{ParentPath => Siblings1},
            PatternsToDelete1
    end.

remove_expired_nodes1(
  [PathToDelete | Rest],
  #walk{tree = Tree,
        applied_changes = AppliedChanges,
        tree_options = TreeOptions,
        fun_acc = Acc} = Walk) ->
    %% See `delete_matching_nodes/4'. This is the same except that the
    %% accumulator is passed through and the `DeleteReason' is provided as
    %% `keep_while'.
    Fun = fun(Path, Node, Result) ->
                  delete_matching_nodes_cb(
                    Path, Node, TreeOptions, keep_while, Result)
          end,
    TreeOptions1 = TreeOptions#{expect_specific_node => false},
    Result = walk_down_the_tree(
               Tree, PathToDelete, TreeOptions1, #{}, Fun, Acc),
    case Result of
        {ok, Tree1, AppliedChanges1, Acc1} ->
            %% When we are handling deletes as part of a `keep_while', it is
            %% possible that nodes' child list versions were bumped if a child
            %% was added in the first pass. In this case, we don't want to
            %% bump that version twice (add + delete), but just once.
            Tree2 = squash_version_bumps_after_keep_while(
                      Tree1, AppliedChanges, AppliedChanges1),

            AppliedChanges2 = merge_applied_changes(
                                AppliedChanges, AppliedChanges1),
            Walk1 = Walk#walk{tree = Tree2,
                              node = Tree2#tree.root,
                              applied_changes = AppliedChanges2,
                              fun_acc = Acc1},
            remove_expired_nodes1(Rest, Walk1)
    end;
remove_expired_nodes1([], Walk) ->
    {ok, Walk}.

%% -------------------------------------------------------------------
%% Conversion between tree versions.
%% -------------------------------------------------------------------

convert_tree(Tree, MacVer, MacVer) ->
    Tree;
convert_tree(Tree, 0, 1) ->
    Tree;
convert_tree(Tree, 1, 2) ->
    %% In version 2 the reverse index for keep while conditions was converted
    %% into a prefix tree. See the `keep_while_conds_revidx_v0()' and
    %% `keep_while_conds_revidx_v1()' types.
    #tree{keep_while_conds_revidx = KeepWhileCondsRevIdxV0} = Tree,
    KeepWhileCondsRevIdxV1 = khepri_prefix_tree:from_map(
                               KeepWhileCondsRevIdxV0),
    Tree#tree{keep_while_conds_revidx = KeepWhileCondsRevIdxV1}.

-ifdef(TEST).
-spec unopacify(Tree) -> Term when
      Tree :: khepri_tree:keep_while_conds_revidx(),
      Term :: any().

unopacify(Tree) ->
    Tree.
-endif.
