src/zipper.erl

%%% @doc Generic Zipper Implementation.
%%%      Zippers let you traverse immutable data structures with ease and
%%%      flexibility.
-module(zipper).

-compile({no_auto_import, [node/1]}).

%% Creation
-export([new/4]).
%% Traverse
-export([up/1, down/1, left/1, leftmost/1, right/1, rightmost/1, next/1, is_end/1, prev/1,
         root/1, traverse/2]).
%% Editing
-export([insert_left/2, insert_right/2, replace/2, edit/3, insert_child/2, append_child/2,
         remove/1]).
%% Iteration
-export([map/2, fmap/3, filter/2, fold/3, size/1]).
%% Info
-export([node/1, children/1, is_branch/1]).

-type is_branch_fun(T) :: fun((T) -> boolean()).
-type make_node_fun(T) :: fun((T, [T]) -> T).
-type children_fun(T) :: fun((T) -> [T]).
-type info(T) ::
    #{lefts => [T],
      rights => [T],
      parent_node => undefined | T,
      parent_info => undefined | info(T),
      is_modified => boolean()}.

-opaque zipper(T) ::
    #{spec =>
          #{is_branch => is_branch_fun(T),
            make_node => make_node_fun(T),
            children => children_fun(T)},
      node => T,
      info => 'end' | info(T)}.

-type operation() ::
    next | prev | up | down | left | right | root | node | rightmost | leftmost.

-export_type([zipper/1]).
-export_type([info/1, is_branch_fun/1, make_node_fun/1, children_fun/1]).
-export_type([operation/0]).

-ifdef(TEST).

-export([as_list_of_maps/1]).

-endif.

-elvis([{elvis_style, dont_repeat_yourself, disable}]).
-elvis([{elvis_style, god_modules, disable}]).
-elvis([{elvis_style, no_throw, disable}]).

-ifdef(TEST).

-spec as_list_of_maps([zipper(any())]) -> [map()].
as_list_of_maps(Children) ->
    Children.

-endif.

%% @doc Builds a new zipper with nodes of type T.
-spec new(is_branch_fun(T), children_fun(T), make_node_fun(T), T) -> zipper(T).
new(IsBranch, Children, MakeNode, Root) ->
    #{spec =>
          #{is_branch => IsBranch,
            children => Children,
            make_node => MakeNode},
      node => Root,
      info =>
          #{lefts => [],
            rights => [],
            parent_node => undefined,
            parent_info => undefined}}.

%% @doc Returns the zipper in the parent node, if possible.
-spec up(zipper(T)) -> zipper(T) | undefined.
up(#{info := #{parent_node := undefined}}) ->
    undefined;
up(#{spec := #{make_node := MakeNode},
     node := Node,
     info :=
         #{lefts := Lefts,
           rights := Rights,
           parent_node := ParentNode,
           parent_info := ParentInfo,
           is_modified := true}} =
       Zipper) ->
    Children = lists:reverse(Lefts) ++ [Node | Rights],
    NewParentNode = MakeNode(ParentNode, Children),
    Zipper#{node => NewParentNode, info => ParentInfo};
up(#{info := #{parent_node := Parent, parent_info := ParentInfo}} = Zipper) ->
    Zipper#{node => Parent, info => ParentInfo}.

%% @doc Returns the zipper in the first child, if any.
-spec down(zipper(T)) -> zipper(T) | undefined.
down(#{node := Node,
       info := Info,
       spec := #{children := Children}} =
         Zipper) ->
    case is_branch(Zipper) of
        true ->
            [NewNode | Rights] = Children(Node),
            Zipper#{node => NewNode,
                    info =>
                        #{lefts => [],
                          rights => Rights,
                          parent_node => Node,
                          parent_info => Info}};
        false ->
            undefined
    end.

%% @doc Returns the zipper on the left, if any.
-spec left(zipper(T)) -> zipper(T) | undefined.
left(#{info := #{lefts := []}}) ->
    undefined;
left(#{info := Info = #{lefts := [NewNode | Lefts], rights := Rights}, node := Node} =
         Zipper) ->
    Zipper#{info => Info#{lefts => Lefts, rights => [Node | Rights]}, node => NewNode}.

%% @doc Returns the leftmost zipper in the current zipper.
-spec leftmost(zipper(T)) -> zipper(T).
leftmost(#{info := #{lefts := []}} = Zipper) ->
    Zipper;
leftmost(#{info := Info = #{lefts := Lefts, rights := Rights}, node := Node} = Zipper) ->
    Fun = fun(Item, Acc) -> [Item | Acc] end,
    [NewNode | NewRights] = lists:foldl(Fun, [Node | Rights], Lefts),
    Zipper#{info => Info#{lefts => [], rights => NewRights}, node => NewNode}.

%% @doc Returns the zipper on the right, if any.
-spec right(zipper(T)) -> zipper(T) | undefined.
right(#{info := #{rights := []}}) ->
    undefined;
right(#{info := Info = #{rights := [NewNode | Rights], lefts := Lefts}, node := Node} =
          Zipper) ->
    Zipper#{info => Info#{rights := Rights, lefts := [Node | Lefts]}, node := NewNode}.

%% @doc Returns the rightmost zipper in the current zipper.
-spec rightmost(zipper(T)) -> zipper(T).
rightmost(#{info := Info = #{rights := Rights, lefts := Lefts}, node := Node} = Zipper) ->
    Fun = fun(Item, Acc) -> [Item | Acc] end,
    [NewNode | NewLefts] = lists:foldl(Fun, [Node | Lefts], Rights),
    Zipper#{info => Info#{lefts => NewLefts, rights => []}, node => NewNode}.

%% @doc Returns the next zipper.
-spec next(zipper(T)) -> zipper(T).
next(#{info := 'end'} = Zipper) ->
    Zipper;
next(Zipper) ->
    case {is_branch(Zipper), right(Zipper)} of
        {true, _} ->
            down(Zipper);
        {false, undefined} ->
            next_recur(Zipper);
        {false, Right} ->
            Right
    end.

-spec next_recur(zipper(T)) -> zipper(T).
next_recur(Zipper) ->
    case up(Zipper) of
        undefined ->
            Zipper#{info => 'end'};
        UpZipper ->
            case right(UpZipper) of
                undefined ->
                    next_recur(UpZipper);
                Next ->
                    Next
            end
    end.

%% @doc Is it the end of the zipper traversal.
-spec is_end(zipper(_)) -> boolean().
is_end(#{info := 'end'}) ->
    true;
is_end(_Zipper) ->
    false.

%% @doc Returns the previous zipper.
-spec prev(zipper(T)) -> zipper(T) | undefined.
prev(#{info := #{lefts := []}} = Zipper) ->
    up(Zipper);
prev(Zipper) ->
    prev_recur(left(Zipper)).

-spec prev_recur(zipper(T)) -> zipper(T).
prev_recur(Zipper) ->
    case down(Zipper) of
        undefined ->
            Zipper;
        DownZipper ->
            RightMost = rightmost(DownZipper),
            prev_recur(RightMost)
    end.

%% @doc Returns the node on the root of the zipper.
-spec root(zipper(T)) -> T.
root(Zipper) ->
    case up(Zipper) of
        undefined ->
            node(Zipper);
        Parent ->
            root(Parent)
    end.

%% @doc Traverses the zipper following the given list of operations.
%%      If, at some point, an operation is invalid, it will crash.
-spec traverse([operation()], zipper(T)) -> zipper(T) | T | undefined.
traverse([], Zipper) ->
    Zipper;
traverse([Op | Rest], Zipper) ->
    traverse(Rest, zipper:Op(Zipper)).

%% @doc Inserts a node to the left of the current one.
-spec insert_left(T, zipper(T)) -> zipper(T).
insert_left(_, #{info := #{parent_node := undefined}}) ->
    throw(insert_at_top);
insert_left(Node, #{info := Info = #{lefts := Lefts}} = Zipper) ->
    NewInfo = Info#{lefts => [Node | Lefts], is_modified => true},
    Zipper#{info => NewInfo}.

%% @doc Inserts a node to the right of the current one.
-spec insert_right(T, zipper(T)) -> zipper(T).
insert_right(_, #{info := #{parent_node := undefined}}) ->
    throw(insert_at_top);
insert_right(Node, #{info := Info = #{rights := Rights}} = Zipper) ->
    NewInfo = Info#{rights => [Node | Rights], is_modified => true},
    Zipper#{info => NewInfo}.

%% @doc Replaces the current node.
-spec replace(T, zipper(T)) -> zipper(T).
replace(Node, #{info := Info} = Zipper) ->
    Zipper#{node => Node, info => Info#{is_modified => true}}.

%% @doc Edits the current node by applying the given function.
%%      The parameters of said function will be [Node | Args].
-spec edit(fun((...) -> T), [term()], zipper(T)) -> zipper(T).
edit(Fun, Args, #{node := Node, info := Info} = Zipper) ->
    NewNode = erlang:apply(Fun, [Node | Args]),
    Zipper#{node => NewNode, info => Info#{is_modified => true}}.

%% @doc Adds a node as the leftmost child of the current one.
-spec insert_child(T, zipper(T)) -> zipper(T).
insert_child(Child, #{spec := #{make_node := MakeNode}} = Zipper) ->
    Node = node(Zipper),
    Children = children(Zipper),
    NewNode = MakeNode(Node, [Child | Children]),
    replace(NewNode, Zipper).

%% @doc Adds a node as the rightmost child of the current one.
-spec append_child(T, zipper(T)) -> zipper(T).
append_child(Child, #{spec := #{make_node := MakeNode}} = Zipper) ->
    Node = node(Zipper),
    Children = children(Zipper),
    NewNode = MakeNode(Node, Children ++ [Child]),
    replace(NewNode, Zipper).

%% @doc Removes current node from zipper.
%%      Moves down, if possible. If not, it moves to the rightmost node.
-spec remove(zipper(T)) -> zipper(T).
remove(#{info := #{parent_node := undefined}}) ->
    throw(remove_at_top);
remove(#{spec := Spec = #{make_node := MakeNode},
         info :=
             #{lefts := [],
               rights := Rights,
               parent_node := ParentNode,
               parent_info := ParentInfo}}) ->
    NewParentNode = MakeNode(ParentNode, Rights),
    #{spec => Spec,
      node => NewParentNode,
      info => ParentInfo#{is_modified => true}};
remove(#{spec := Spec, info := Info = #{lefts := [Node | Lefts]}}) ->
    NewZipper =
        #{spec => Spec,
          node => Node,
          info => Info#{lefts => Lefts, is_modified => true}},
    prev_removed(NewZipper).

prev_removed(Zipper) ->
    case down(Zipper) of
        undefined ->
            Zipper;
        Child ->
            prev_removed(rightmost(Child))
    end.

%% Iteration

%% @doc Applies a function to all nodes of the zipper.
%%      Returns a list with the results.
-spec map(fun((T) -> U), zipper(T)) -> [U].
map(Fun, Zipper) ->
    ApplyAddFun = fun(X, Acc) -> [Fun(X) | Acc] end,
    Result = fold(ApplyAddFun, [], Zipper),
    lists:reverse(Result).

%% @doc Returns the root of the tree, where the value of each node
%%      (after the current location of Zipper) is replaced with the
%%      result from applying Fun to the node as the first argument
%%      and Args as additional arguments.
-spec fmap(fun((...) -> T), [term()], zipper(T)) -> T.
fmap(Fun, Args, Zipper) ->
    NewZipper = edit(Fun, Args, Zipper),
    NextZipper = next(NewZipper),
    case is_end(NextZipper) of
        true ->
            root(NewZipper);
        false ->
            fmap(Fun, Args, NextZipper)
    end.

%% @doc Applies Fun recursively on the zipper.
%%      The arguments of Fun will be (Node, Acc) where Acc is the result of the
%%      previous call or the initial value provided.
-spec fold(fun((T, A) -> A), A, zipper(T)) -> A.
fold(Fun, Acc, Zipper) ->
    case is_end(Zipper) of
        true ->
            Acc;
        false ->
            Node = node(Zipper),
            NewAcc = Fun(Node, Acc),
            fold(Fun, NewAcc, next(Zipper))
    end.

%% @doc Returns a list of all the nodes in the zipper that match a predicate.
-spec filter(fun((T) -> boolean()), zipper(T)) -> [T].
filter(Pred, Zipper) ->
    FilterFun =
        fun(X, Acc) ->
           case Pred(X) of
               true ->
                   [X | Acc];
               false ->
                   Acc
           end
        end,
    fold(FilterFun, [], Zipper).

%% @doc Returns the size of the zipper.
-spec size(zipper(_)) -> non_neg_integer().
size(Zipper) ->
    IncFun = fun(_, Acc) -> Acc + 1 end,
    fold(IncFun, 0, Zipper).

%% Info

%% @doc Returns the value of the current node in the zipper.
-spec node(zipper(T)) -> T.
node(#{node := Node}) ->
    Node.

%% @doc Returns the list of children zippers.
-spec children(zipper(T)) -> [zipper(T)].
children(#{spec := #{children := Children}, node := Node} = Zipper) ->
    case is_branch(Zipper) of
        true ->
            Children(Node);
        false ->
            throw(children_on_leaf)
    end.

%% @doc Is this node a branch?
-spec is_branch(zipper(_)) -> boolean().
is_branch(#{spec := #{is_branch := IsBranch}, node := Node}) ->
    IsBranch(Node).