%%% 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 Generic server based CFG server.
%%%
%%% @author Istvan Bozo <bozo_i@inf.elte.hu>

-module(refsc_cfg_server).

-vsn("$Rev: 17551 $ ").

-behaviour(gen_server).

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

%% interface functions for server services (asynchronous)
-export([build_cfgs/1, rebuild_cfgs/1, delete_cfgs/1]).

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

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

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

-include("slicer.hrl").

%% Record to store the server state
-record(state,
        {graph_tab   :: ets:tab(),  % Stored cfg edges
         cardidx_tab :: ets:tab(),  % Index of currently running building procs
         toreply_tab :: ets:tab()}).% List of not replied calls/ delayed answers

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

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

-spec stop() -> ok.
%% @doc Stops the CFG server.
stop() ->
    gen_server:cast(?MODULE, {stop, []}).

-spec reset() -> ok.
%% @doc Synchronous call to reset the server, it delets the cashed
%% CFGs.
reset() ->
    gen_server:call(?MODULE, {reset, []}, infinity).

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

%% synchronous operations

-spec get_status(Form :: refsc_cfg_utils:cfg_node()) ->
                        Ret  :: ready | in_progress | unavailable.
%% @doc Synchronous call for querying the status of the function
%% form. The possible returnvalues are:
%% `ready' - the CFG is ready
%% `in_progress' - the building of the CFG has been initialised
%% `unavailable' - the function is not loaded in the SPG of
%%                 RefactorErl, or the building has failed
get_status(Form) ->
    gen_server:call(?MODULE, {get_status, Form}, infinity).

-spec get_cfg(Form :: refsc_cfg_utils:cfg_node()) ->
                     {Form     :: refsc_cfg_utils:cfg_node(),
                      Edges    :: [refsc_cfg_utils:cfg_edge()],
                      AppNodes :: [refsc_cfg_utils:cfg_node()]}.
%% @doc Returns the edges of the CFG for the given function or an atom
%% `unavailable' if the CFG is not available.
get_cfg(Form) ->
    gen_server:call(?MODULE, {get_cfg, Form}, infinity).


%% asynchronous operations

-spec build_cfgs(Forms :: [refsc_cfg_utils:cfg_node()]) -> ok.
%% @doc The function initiates the processes for building the CFG for
%% the given function forms, if there is no CFG stored for the given
%% function. It starts the building process only for the functions
%% that are not cashed by the server.
build_cfgs(Forms) when is_list(Forms) ->
    gen_server:cast(?MODULE, {build_cfgs, Forms}).

-spec rebuild_cfgs(Forms :: [refsc_cfg_utils:cfg_node()]) -> ok.
%% @doc The function initiates the processes for rebuilding the CFG
%% for the given functions. If there are stored CFGs for the given
%% functions, these are deleted and new building processes are
%% initiated. The function will be used for rebuilding the CFG for
%% modified functions.
rebuild_cfgs(Forms) when is_list(Forms) ->
    gen_server:cast(?MODULE, {rebuild_cfgs, Forms}).

-spec delete_cfgs(Forms :: [refsc_cfg_utils:cfg_node()]) -> ok.
%% @doc The function initiates the process for deleting the CFGs for
%% the listed functions.
delete_cfgs(Forms) when is_list(Forms) ->
    gen_server:cast(?MODULE, {delete_cfgs, Forms}).

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

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

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

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

%% @private
handle_call({get_status, Form}, _FROM, State) ->
    Reply = handle_get_status(Form, State),
    {reply, Reply, State};
handle_call({get_cfg, Form}, FROM, #state{toreply_tab = ToReply} = State) ->
    case handle_get_status(Form, State) of
        {Form, ready} ->
            Reply = handle_get_cfg(Form, State),
            {reply, Reply, State};
        {Form, unavailable} ->
            ets:insert(ToReply, {Form, FROM}),
            handle_build_cfgs([Form], State),
            {noreply, State};
        {Form, in_progress} ->
            ets:insert(ToReply, {Form, FROM}),
            {noreply, State}
    end;
handle_call({reset, _}, _FROM, State) ->
    NewState = reinit_state(State),
    {reply, ok, NewState}.

%% @private
handle_cast({build_cfgs, Forms}, State) ->
    handle_build_cfgs(Forms, State),
    {noreply, State};
handle_cast({rebuild_cfgs, Forms}, State) ->
    handle_rebuild_cfgs(Forms, State),
    {noreply, State};
handle_cast({stop, _}, State) ->
    {stop, normal, State}.

%% @private
handle_info({P, cfg_ready, _} = Msg, State) when is_pid(P) ->
    handle_add_edges(Msg, State),
    {noreply, State};
handle_info({'EXIT', P, normal}, State) ->
    error_logger:info_report({terminated, P}),
    {noreply, State};
handle_info({'EXIT', P, _}, State) ->
    [Form] =
        ets:select(State#state.cardidx_tab, [{{'$1', {'$2', '$3'}},
                                              [{'==', '$3', P}], ['$1']}]),
    ToReply =
        ets:lookup(State#state.toreply_tab, Form),
    [gen_server:reply(ToR, {Form, unavailable}) || {_, ToR} <- ToReply],
    ets:match_delete(State#state.cardidx_tab, {'_', {'_', P}}),
    {noreply, State}.


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

-define(InProg, in_progress).
-define(Ready, ready).
-define(UnAvailable, unavailable).

init_state() ->
    Graph   = ets:new(graph_tab,   [protected]),
    CardIdx = ets:new(cardidx_tab, [private]),
    ToReply = ets:new(toreply_tab, [bag, private]),
    #state{graph_tab = Graph, cardidx_tab = CardIdx, toreply_tab = ToReply}.

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

del_tables(#state{} = State) ->
    ets:delete(State#state.cardidx_tab),
    ets:delete(State#state.graph_tab),
    ets:delete(State#state.toreply_tab),
    ok.


handle_get_status(Form, #state{cardidx_tab = CardIdx}) ->
    case ets:lookup(CardIdx, Form) of
        [] ->
            {Form, ?UnAvailable};
        [{Form, ?Ready}] ->
            {Form, ?Ready};
        [{Form, {?InProg, _}}] ->
            {Form, ?InProg}
    end.

handle_rebuild_cfgs(Forms, State) ->
    DelFun =
        fun(F) ->
                ets:match_delete(State#state.graph_tab,  {F, '_'}),
                ets:match_delete(State#state.cardidx_tab, {F, '_'})
        end,
    [ DelFun(Form) || Form <- Forms],
    handle_build_cfgs(Forms, State),
    ok.

handle_build_cfgs(Forms, State) ->
    [build_cfg(Form, State) || Form <- Forms],
    ok.

build_cfg(Form, #state{cardidx_tab = CardIdx}) ->
    case ets:lookup(CardIdx, Form) of
        [] ->
            ParentPid  = self(),
            ProcessPid =
                proc_lib:spawn_link(
                  fun() ->
                          {Edges, Apps} = refsc_cfg_utils:build_cfg(Form),
                          ParentPid ! {self(), cfg_ready, {Form, Edges, Apps}}
                  end),
            ets:insert(CardIdx, {Form, {?InProg, ProcessPid}});
        _  ->
            ok
    end.

%% The function is called if the computation of the graph edges is finished!
handle_get_cfg(Form, #state{graph_tab = Graph}) ->
    case ets:lookup(Graph, Form) of
        [Elem = {Form, _Edges, _Apps}] -> Elem;
        _ ->
            error_logger:error_report([{cfg_is_not_available, Form}])
    end.

handle_add_edges({Pid, cfg_ready, Data = {Form, Edges, Apps}},
                 #state{graph_tab = Graph, cardidx_tab = CardIdx,
                        toreply_tab = ToReply}) ->
    case ets:lookup(CardIdx, Form) of
        [{Form, {?InProg, Pid}}] ->
            ets:insert(Graph, {Form, Edges, Apps}),
            ets:insert(CardIdx, {Form, ?Ready}),
            Tuples = ets:lookup(ToReply, Form),
            [gen_server:reply(CallerPid, Data) || {_Form, CallerPid} <- Tuples],
            ets:delete(ToReply, Form);
        _ ->
            %% This case occurs if a new rebuild request arrives and
            %% the old one becomes deprecated or a message arrives
            %% from outside the server.
            ok
    end.
