-module(graffeo_map).
-moduledoc """
Tier-1 value backend: an immutable, maps-backed graph with dual
adjacency. Every operation returns a new graph; the original is
untouched. Labels on vertices, weight/metadata on edges.
""".
-behaviour(graffeo_backend).
-behaviour(graffeo_builder).
-export([
new/0,
add_vertex/2, add_vertex/3,
add_edge/3, add_edge/4,
edge_meta/3,
vertex_label/2
]).
%% graffeo_builder callbacks (envelope-based)
-export([
empty_like/1,
build_add_vertex/2, build_add_vertex/3,
build_add_edge/4
]).
-export([
vertices/1,
out_neighbours/2,
in_neighbours/2,
in_degree/2,
out_degree/2,
no_edges/1,
no_vertices/1
]).
-record(gmap, {
vs = #{} :: #{graffeo:vertex() => graffeo:label()},
out = #{} :: #{graffeo:vertex() => #{graffeo:vertex() => graffeo:edge_meta()}},
in = #{} :: #{graffeo:vertex() => #{graffeo:vertex() => graffeo:edge_meta()}}
}).
-doc "Opaque reference to the map-backed graph internals.".
-opaque ref() :: #gmap{}.
-export_type([ref/0]).
%%% --- graffeo_builder callbacks ---
-doc "Create a new, empty map-backed graph.".
-spec new() -> ref().
new() ->
#gmap{}.
-doc "Add a vertex with the default label (`undefined`).".
-spec add_vertex(ref(), graffeo:vertex()) -> ref().
add_vertex(G, V) ->
add_vertex(G, V, undefined).
-doc "Add a vertex with a label.".
-spec add_vertex(ref(), graffeo:vertex(), graffeo:label()) -> ref().
add_vertex(#gmap{vs = Vs, out = Out, in = In} = G, V, Label) ->
G#gmap{
vs = Vs#{V => Label},
out = maps:merge(#{V => #{}}, Out),
in = maps:merge(#{V => #{}}, In)
}.
-doc "Add an edge with default metadata (`#{weight => 1}`).".
-spec add_edge(ref(), graffeo:vertex(), graffeo:vertex()) -> ref().
add_edge(G, From, To) ->
add_edge(G, From, To, #{weight => 1}).
-doc "Add an edge with metadata (weight, label).".
-spec add_edge(ref(), graffeo:vertex(), graffeo:vertex(), graffeo:edge_meta()) -> ref().
add_edge(G0, From, To, Meta) ->
G1 = ensure_vertex(G0, From),
G2 = ensure_vertex(G1, To),
#gmap{out = Out, in = In} = G2,
OutFrom = maps:get(From, Out),
InTo = maps:get(To, In),
G2#gmap{
out = Out#{From := OutFrom#{To => Meta}},
in = In#{To := InTo#{From => Meta}}
}.
%%% --- graffeo_backend callbacks ---
-doc "All vertices in the graph.".
-spec vertices(ref()) -> [graffeo:vertex()].
vertices(#gmap{vs = Vs}) ->
maps:keys(Vs).
-doc "Vertices reachable from `V` via outgoing edges.".
-spec out_neighbours(ref(), graffeo:vertex()) -> [graffeo:vertex()].
out_neighbours(#gmap{out = Out}, V) ->
maps:keys(maps:get(V, Out, #{})).
-doc "Vertices that reach `V` via incoming edges.".
-spec in_neighbours(ref(), graffeo:vertex()) -> [graffeo:vertex()].
in_neighbours(#gmap{in = In}, V) ->
maps:keys(maps:get(V, In, #{})).
-doc "Number of incoming edges to `V`.".
-spec in_degree(ref(), graffeo:vertex()) -> non_neg_integer().
in_degree(#gmap{in = In}, V) ->
map_size(maps:get(V, In, #{})).
-doc "Number of outgoing edges from `V`.".
-spec out_degree(ref(), graffeo:vertex()) -> non_neg_integer().
out_degree(#gmap{out = Out}, V) ->
map_size(maps:get(V, Out, #{})).
-doc "Total number of edges in the graph.".
-spec no_edges(ref()) -> non_neg_integer().
no_edges(#gmap{out = Out}) ->
maps:fold(fun(_V, Adj, Acc) -> Acc + map_size(Adj) end, 0, Out).
-doc "Total number of vertices in the graph.".
-spec no_vertices(ref()) -> non_neg_integer().
no_vertices(#gmap{vs = Vs}) ->
map_size(Vs).
%%% --- Extra accessors ---
-doc "Get edge metadata between two vertices.".
-spec edge_meta(ref(), graffeo:vertex(), graffeo:vertex()) ->
{ok, graffeo:edge_meta()} | error.
edge_meta(#gmap{out = Out}, From, To) ->
case maps:find(From, Out) of
{ok, Adj} -> maps:find(To, Adj);
error -> error
end.
-doc "Get the label of a vertex.".
-spec vertex_label(ref(), graffeo:vertex()) ->
{ok, graffeo:label()} | error.
vertex_label(#gmap{vs = Vs}, V) ->
maps:find(V, Vs).
%%% --- graffeo_builder callbacks (envelope-based) ---
-doc false.
-spec empty_like(graffeo:graph()) -> graffeo:graph().
empty_like(_G) ->
graffeo:wrap_ref(graffeo_map, new()).
-doc false.
-spec build_add_vertex(graffeo:graph(), graffeo:vertex()) -> graffeo:graph().
build_add_vertex(G, V) ->
build_add_vertex(G, V, []).
-doc false.
-spec build_add_vertex(graffeo:graph(), graffeo:vertex(), graffeo:label()) -> graffeo:graph().
build_add_vertex(G, V, Label) ->
graffeo:add_vertex(G, V, Label).
-doc false.
-spec build_add_edge(graffeo:graph(), graffeo:vertex(), graffeo:vertex(), graffeo:edge_meta()) ->
graffeo:graph().
build_add_edge(G, From, To, Meta) ->
graffeo:add_edge(G, From, To, Meta).
%%% --- Internal ---
-spec ensure_vertex(ref(), graffeo:vertex()) -> ref().
ensure_vertex(#gmap{vs = Vs} = G, V) ->
case maps:is_key(V, Vs) of
true -> G;
false -> add_vertex(G, V)
end.