%%% This file is part of RefactorErl.
%%%
%%% RefactorErl is free software: you can redistribute it and/or modify
%%% it under the terms of the GNU Lesser General Public License as published
%%% by the Free Software Foundation, either version 3 of the License, or
%%% (at your option) any later version.
%%%
%%% RefactorErl is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%%% GNU Lesser General Public License for more details.
%%%
%%% You should have received a copy of the GNU Lesser General Public License
%%% along with RefactorErl.  If not, see <http://plc.inf.elte.hu/erlang/>.
%%%
%%% The Original Code is RefactorErl.
%%%
%%% The Initial Developer of the Original Code is Eötvös Loránd University.
%%% Portions created  by Eötvös Loránd University and ELTE-Soft Ltd.
%%% are Copyright 2007-2025 Eötvös Loránd University, ELTE-Soft Ltd.
%%% and Ericsson Hungary. All Rights Reserved.


%%% ============================================================================
%%% Module information

%%% @doc Module of the CDG server. The module is also responsible to
%%% keep the CFG server up-to-date.
%%%
%%% @author Istvan Bozo <bozo_i@inf.elte.hu>

-module(refsc_cdg_server).

-behaviour(gen_server).

%% interface functions for managing the server
-export([start_link/0, stop/0, reset/1]).

%% interface functions for server services (asynchronous)
-export([build/1]).

%% interface functions for server services (synchronous)
-export([get/1, get_compound_cdg/1]).


%% callback functions
-export([handle_call/3, handle_cast/2, handle_info/2]).

-export([init/1, terminate/2, code_change/3]).

%% internal handlers
-export([construct/2]).

-include("slicer.hrl").


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Interface functions for managing the server
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


-spec start_link() -> {ok, Pid :: pid()} | {error, Reason :: term()}.
%% @doc The function starts the CDG server.
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

-spec stop() -> ok.
%% @doc The function stops the CDG server.
stop() ->
    gen_server:cast(?MODULE, {stop}).

-spec reset(Mode :: cdg_reset | full_reset) -> ok.
%% @doc The function initiates a synchronous call to reset the stored
%% graphs. It accepts atoms `cdg_reset' and `full_reset'. The former
%% option resets only the CDG server and the second option initiates
%% also the CFG server reset.
reset(cdg_reset) ->
    gen_server:call(?MODULE, {reset, cdg_server});
reset(full_reset) ->
    gen_server:call(?MODULE, {reset, cdg_cfg_server}).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Interface functions for queries and requests
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec get_compound_cdg(Forms :: [refsc_cfg_utils:cfg_node()]) ->
                              Edges :: [refsc_cfg_utils:cfg_edge()].
%% @doc The function returns the compound CDG, where temporal edges
%% are resolved.
get_compound_cdg(Forms) ->
    Funs   = ?CDGUtils:get_involved_funs(Forms),
    refsc_cdg_utils:build_compound_cdg(Funs).


-spec build(Forms :: [refsc_cdg_utils:cdg_node()]) -> ok.

%% @doc Asynchronous interface function, to initialise the building
%% process for the given function forms.
build(Forms) when is_list(Forms) ->
    gen_server:cast(?MODULE, {build, Forms}).

-spec get(Forms :: [refsc_cdg_utils:cdg_node()]) ->
                 [{Form    :: refsc_cdg_utils:cdg_node(),
                   PdInfo  :: {[tuple()], atom()},
                   Edges   :: [refsc_cdg_utils:cdg_edge()],
                   CallSrc :: [refsc_cdg_utils:cdg_node()]}].
%% @doc Synchronous interface function to query the CDGs for the given
%% function forms.
get(Forms) when is_list(Forms) ->
    gen_server:cast(?MODULE, {build, Forms}),
    [gen_server:call(?MODULE, {get, F}) || F <- Forms].

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Data types
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Record for the server state.
-record(state,
        {graph_tab   :: ets:tab(),% Intrafunc. CDG-s
         postdom_tab :: ets:tab(),% Postdom trees
         cardidx_tab :: ets:tab(),% Index of running procs
         toreply_tab :: ets:tab()% Not replied calls
        }).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Callback functions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% @private
init(_) ->
    process_flag(trap_exit, true),
    State = init_state(),
    {ok, State}.

%% @private
terminate(_Reason, #state{} = State) ->
    del_tables(State).

%% @private
%% Synchronous callbacks
handle_call({get, Form}, FROM, #state{} = State) ->
    case check_status(Form, State) of
        {Form, ready} ->
            [{Form, Hash, CDG, CallSrcs}] =
                ets:lookup(State#state.graph_tab, Form),
            [{Form, Hash, PD}]  = ets:lookup(State#state.postdom_tab, Form),
            {reply, {Form, PD, CDG, CallSrcs}, State};
        Other ->
            error_logger:info_report({cdg_other, Other}),
            ets:insert(State#state.toreply_tab, {Form, FROM}),
            {noreply, State}
    end;
handle_call({reset, cdg_server}, _FROM, #state{} = State) ->
    NewState = reinit_state(State),
    {reply, ok, NewState};
handle_call({reset, cdg_cfg_server}, _FROM, #state{} = State) ->
    refsc_cfg_server:reset(),
    NewState = reinit_state(State),
    {reply, ok, NewState}.

%% @private
handle_cast({build, Forms}, #state{} = State) ->
    error_logger:info_report({build_req, Forms}),
    handle_build(Forms, State),
    {noreply, State};
handle_cast({stop, []}, #state{} = State) ->
    {stop, normal, State}.

%% @private
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% @private
handle_info({P, {ready, Form, Hash, PD, CDG, CSrcs}}, State) when is_pid(P) ->
    error_logger:info_report({ready_msg_from_CDG, P, Form}),
    handle_add_edges(P, Form, Hash, PD, CDG, CSrcs, State),
    {noreply, State};
handle_info({'EXIT', P, normal}, State) ->
    error_logger:info_report({terminated, P}),
    {noreply, State};
handle_info(Msg, State) ->
    error_logger:warning_report(Msg),
    {noreply, State}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Implementation of functions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

init_state() ->
    NewCDGGraph = ets:new(graph_tab, [private]),
    NewPDGraph  = ets:new(postdom_tab, [private]),
    NewCardIdx  = ets:new(cardidx_tab, [private]),
    NewToReply  = ets:new(toreply_tab, [bag, private]),
    #state{graph_tab = NewCDGGraph, postdom_tab = NewPDGraph,
           cardidx_tab = NewCardIdx, toreply_tab = NewToReply}.

reinit_state(#state{} = State) ->
    del_tables(State),
    init_state().

del_tables(#state{graph_tab = GT, postdom_tab = PT,
                  cardidx_tab = CT, toreply_tab = TT}) ->
    ets:delete(GT),
    ets:delete(PT),
    ets:delete(CT),
    ets:delete(TT).

handle_add_edges(Pid, Form, Hash, PD, CDG, CallSrcs, #state{} = State) ->
    case ets:lookup(State#state.cardidx_tab, Form) of
        [{Form, {in_progress, Pid}}] ->
            ets:insert(State#state.graph_tab, {Form, Hash, CDG, CallSrcs}),
            ets:insert(State#state.postdom_tab, {Form, Hash, PD}),
            ets:insert(State#state.cardidx_tab, {Form, ready}),
            %reply the previous calls
            [gen_server:reply(CPid, {Form, PD, CDG, CallSrcs}) ||
                {_Form, CPid} <- ets:lookup(State#state.toreply_tab, Form)];
        Other ->
            error_logger:info_report(Other)
    end.

handle_build(Forms, State) ->
    UnAvFun =
        fun(F) ->
                check_status(F, State) =:= {F, unavailable}
        end,
    {ToBld, Other} = lists:partition(UnAvFun, Forms),
    ToReBldFun =
        fun(F) ->
                ActHash = ?Form:hash(F),
                GData  = ets:lookup(State#state.graph_tab, F),
                GState = check_status(F, State),
                case {GData, GState} of
                    {[{F, H, _, _}], _} -> ActHash =/= H;
                    {[], {F, in_progress}} ->
                        false
                end
        end,
    {ToReBld, _Ready}  = lists:partition(ToReBldFun, Other),
    refsc_cfg_server:build_cfgs(ToBld),
    refsc_cfg_server:rebuild_cfgs(ToReBld),
    Result = [refsc_cfg_server:get_cfg(F) || F <- ToBld ++ ToReBld],
    Parent = self(),
    [ begin
          H = ?Form:hash(F),
          P = proc_lib:spawn_link(?MODULE, construct, [Parent, {F,H,E,A}]),
          ets:insert(State#state.cardidx_tab, {F, {in_progress, P}})
      end || {F, E, A} <- Result],
    ok.

%% @private
construct(Parent, {Form, Hash, CFGEdges, _}) when CFGEdges =:= unavailable ->
    Parent ! {self(), {unavailable, Form, Hash}};
construct(Parent, {Form, Hash, CFGEdges, Apps}) ->
    PostdomEdges = refsc_postdom:immediate_postdominators(Form, CFGEdges, Apps),
    DepEdges =
        refsc_cdg_utils:build_cdg({Form, CFGEdges}, PostdomEdges),
    Parent ! {self(), {ready, Form, Hash, PostdomEdges, DepEdges, Apps}}.

%% @spec check_status(node(), #state{}) -> {node(), atom()}
%% @doc The function returns `true' it the CDG is available, otherwise
%% it returns `false'.
check_status(Form, State) ->
    case ets:lookup(State#state.cardidx_tab, Form) of
        [{Form, ready}] ->
            {Form, ready};
        [{Form, {in_progress, _}}] ->
            {Form, in_progress};
        [] ->
            {Form, unavailable}
    end.
