%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2007-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%

%%
%%----------------------------------------------------------------------
%% Purpose: Sequence generator for the megaco test suite
%%----------------------------------------------------------------------

-module(megaco_test_generator).

-behaviour(gen_server).

-compile({no_auto_import,[error/2]}).

%% ----

-export([
	 start_link/3, 
	 start_link/4, 
	 exec/2, exec/3, 
	 stop/1
	]).

%% Misc utility function for modules implementing this behaviour
-export([
	 sleep/1, 
	 sz/1, 
	 debug/1, debug/2, 
	 error/2, 
	 print/3, print/4
	]).


%% Internal exports
-export([start/4]).
-export([handler_init/5]).

%% Internal gen_server exports
-export([
	 init/1, 
	 handle_call/3, 
	 handle_cast/2, 
	 handle_info/2, 
	 terminate/2, 
	 code_change/3
	]).


-include_lib("megaco/include/megaco.hrl").
-include("megaco_test_lib.hrl").


%%----------------------------------------------------------------------

-define(TIMEOUT, timer:minutes(5)).


%%----------------------------------------------------------------------

-record(state, 
	{
	  parent, 
	  callback_module, 
	  callback_state, 
	  handler = {undefined, undefined}, 
	  timer, 
	  name,
	  id
	 }).


%%%=========================================================================
%%%  API
%%%=========================================================================

-callback init(Args) -> {ok, State} | {error, Reason} when
      Args :: term(),
      State :: term(),
      Reason :: term().

-callback handle_parse(Instruction, State) -> 
    {ok, NewInstruction, NewState} |
    {error, Reason} when
      Instruction    :: term(),
      State          :: term(),
      NewInstruction :: term(),
      NewState       :: term(),
      Reason         :: term().

-callback handle_exec(Instruction, State) -> 
    {ok, NewState} |
    {error, Reason} when
      Instruction :: term(),
      State       :: term(),
      NewState    :: term(),
      Reason      :: term().

-callback terminate(Reason, State) -> 
    megaco:void() when
      Reason :: term(),
      State :: term().


%%----------------------------------------------------------------------

start_link(Mod, Args, Name) 
  when is_atom(Mod) andalso is_list(Name) ->
    start(Mod, Args, Name, self()).

start_link(Mod, Args, Name, Node) 
  when is_atom(Mod) andalso is_list(Name) andalso (Node =/= node()) ->
    case rpc:call(Node, ?MODULE, start, [Mod, Args, Name, self()]) of
	{ok, Pid} ->
	    link(Pid),
	    {ok, Pid};
	Error ->
	    Error
    end;
start_link(Mod, Args, Name, Node) 
  when is_atom(Mod) andalso is_list(Name) andalso (Node =:= node()) ->
    case start(Mod, Args, Name, self()) of
	{ok, Pid} ->
	    link(Pid),
	    {ok, Pid};
	Error ->
	    Error
    end.    

start(Mod, Args, Name, Pid) when is_pid(Pid) ->
    gen_server:start({local, Mod}, ?MODULE, [Mod, Args, Name, Pid], []).


exec(Server, Instructions) ->
    exec(Server, Instructions, infinity).

exec(Server, Instructions, Timeout) 
  when ((Timeout == infinity) orelse 
	(is_integer(Timeout) andalso (Timeout > 0))) ->
    call(Server, {exec, Instructions, Timeout}).


stop(Server) ->
    call(Server, stop).


%%----------------------------------------------------------------------
    
%%--------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, State}          |
%%          {ok, State, Timeout} |
%%          ignore               |
%%          {stop, Reason}
%%--------------------------------------------------------------------

init([Mod, Args, Name, Parent]) ->
    put(name, Name ++ "-CTRL"),
    process_flag(trap_exit, true), 
    put(debug, true),
    d("init -> entry with"
      "~n   Mod:     ~p"
      "~n   Args:    ~p"
      "~n   Name:    ~p"
      "~n   Parent:  ~p", [Mod, Args, Name, Parent]),
    case (catch Mod:init(Args)) of
	{ok, CallbackState} ->
	    d("init -> ~p initiated:"
	      "~n   CallbackState: ~p", [Mod, CallbackState]),
	    State = #state{callback_module = Mod, 
			   callback_state  = CallbackState, 
			   parent          = Parent, 
			   name            = Name},
	    d("init -> initiated"),
	    {ok, State};
	{error, Reason} ->
	    {stop, Reason}
    end.


%%--------------------------------------------------------------------
%% Func: handle_call/3
%% Returns: {reply, Reply, State}          |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
%%--------------------------------------------------------------------
handle_call({exec, Instructions, Timeout}, _From, 
	    #state{callback_module = Mod, 
		   callback_state  = CallbackState,
		   name            = Name} = State) ->
    d("handle_call(exec) -> entry with"
      "~n   Timeout: ~p", [Timeout]),
    case (catch handle_parse(Mod, CallbackState, Instructions)) of
	{ok, NewCallbackState, NewInstructions} ->
	    d("handle_call(exec) -> parsed"
	      "~n   NewCallbackState: ~p", [NewCallbackState]),
	    case handler_start(Name, Mod, NewCallbackState, NewInstructions) of
		{ok, Pid} ->
		    d("handle_call(exec) -> handler started"
		      "~n   Pid: ~p", [Pid]),
		    Timer = maybe_start_timer(Timeout),
		    Id    = {node(), make_ref()},
		    Reply = {ok, Id}, 
		    {reply, Reply, 
		     State#state{callback_state = NewCallbackState,
				 handler        = {running, Pid},
				 timer          = Timer,
				 id             = Id}};
		{error, Reason} ->
		    e("failed starting handler process"
		      "~n   Reason: ~p", [Reason]),		    
		    Reply = {error, {failed_starting_handler, Reason}}, 
		    {stop, Reason, Reply, State}
	    end;
	{error, Reason} ->
	    e("failed parsing instructions"
	      "~n   Reason: ~p", [Reason]),		    
	    Reply = {error, {invalid_instruction, Reason}}, 
	    {stop, Reason, Reply, State}
    end;

handle_call(stop, _From, State) ->
    Reply = ok,
    {stop, normal, Reply, State};

handle_call(Request, From, State) ->
    e("unexpected request"
      "~n   Request: ~p"
      "~n   From:    ~p", [Request, From]),		    
    Reason = {error, {unknown_request, Request, From}},
    Reply  = {error, unknown_request},
    {stop, Reason, Reply, State}.


%%--------------------------------------------------------------------
%% Func: handle_cast/2
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%--------------------------------------------------------------------
handle_cast(Msg, State) ->
    e("unexpected message"
      "~n   Msg: ~p", [Msg]),		    
    Reason = {error, {unknown_message, Msg}},
    {stop, Reason, State}.


%%--------------------------------------------------------------------
%% Func: handle_info/2
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%--------------------------------------------------------------------
handle_info({handler_result, Pid, Result}, 
	    #state{parent  = Parent,
		   handler = {running, Pid},
		   timer   = Timer,
		   id      = Id} = State) ->
    d("handle_info(handler_result) -> entry with"
      "~n   Result: ~p", [Result]),
    maybe_stop_timer(Timer),
    handler_stop(Pid), 
    deliver_exec_result(Parent, Id, Result),
    NewState = State#state{handler = {stopping, Pid}, 
			   timer   = undefined,
			   id      = undefined},
    {noreply, NewState};

handle_info(handler_timeout, #state{handler = {running, Pid}} = State) ->
    d("handle_info(handler_timeout) -> entry with"),
    handler_stop(Pid), 
    {noreply, State#state{handler = {stopping, Pid}}};

handle_info({'EXIT', Pid, {stopped, Result}}, 
	    #state{parent  = Parent,
		   handler = {stopping, Pid},
		   id      = Id} = State) ->
    d("handle_info(handler stopped EXIT) -> entry with"
      "~n   Result: ~p", [Result]),
    deliver_exec_result(Parent, Id, {error, {handler_timeout, Result}}),
    {noreply, State#state{handler = {stopped, undefined}, 
			  timer   = undefined,
			  id      = undefined}};

handle_info({'EXIT', Pid, normal}, 
	    #state{handler = {_, Pid},
		   timer   = Timer} = State) ->
    d("handle_info(handler normal EXIT) -> entry"), 
    maybe_stop_timer(Timer),
    {noreply, State#state{handler = {stopped, undefined}, timer = undefined}};

handle_info({'EXIT', Pid, Reason}, 
	    #state{parent  = Parent,
		   handler = {_, Pid},
		   timer   = Timer,
		   id      = Id} = State) ->
    d("handle_info(handler EXIT) -> entry with"
      "~n   Reason: ~p", [Reason]),
    maybe_stop_timer(Timer),
    deliver_exec_result(Parent, Id, {error, {handler_crashed, Reason}}),
    {noreply, State#state{handler = {crashed, undefined}, 
			  timer   = undefined,
			  id      = undefined}};

handle_info(Info, State) ->
    e("unexpected info"
      "~n   Info:  ~p"
      "~n   State: ~p", [Info, State]),		    
    Reason = {error, {unknown_info, Info}},
    {stop, Reason, State}.


%%--------------------------------------------------------------------
%% Func: terminate/2
%% Purpose: Shutdown the server
%% Returns: any (ignored by gen_server)
%%--------------------------------------------------------------------
terminate(normal, #state{handler = {_HandlerState, Pid}} = _State) ->
    d("terminate(normal) -> entry"),
    handler_stop(Pid),
    ok;

terminate(Reason, #state{handler         = {_HandlerState, Pid},
			 callback_module = Mod, 
			 callback_state  = CallbackState} = _State) ->
    d("terminate -> entry with"
      "~n   Reason: ~p", [Reason]),
    handler_kill(Pid),
    (catch Mod:terminate(Reason, CallbackState)),
    ok.


%%----------------------------------------------------------------------
%% Func: code_change/3
%% Purpose: Convert process state when code is changed
%% Returns: {ok, NewState}
%%----------------------------------------------------------------------

code_change(_Vsn, S, _Extra) ->
    {ok, S}.


%%%-------------------------------------------------------------------
%%% Internal functions
%%%-------------------------------------------------------------------

deliver_exec_result(Parent, Id, {ok, Result}) ->
    Parent ! {exec_complete, Id, ok, Result};
deliver_exec_result(Parent, Id, {error, Reason}) ->
    Parent ! {exec_complete, Id, error, Reason}.


handle_parse(Mod, State, Instructions) ->
    handle_parse(Mod, State, Instructions, []).

handle_parse(_Mod, State, [], Acc) ->
    {ok, State, lists:reverse(Acc)};

handle_parse(Mod, State, [Instruction|Instructions], Acc) ->
    case (catch Mod:handle_parse(Instruction, State)) of
	{ok, NewInstruction, NewState} ->
	    handle_parse(Mod, NewState, Instructions, [NewInstruction|Acc]);
	{error, Reason} ->
	    {error, {invalid_instruction, Instruction, Reason}};
	{'EXIT', Reason} ->
	    {error, {exit, Instruction, Reason}}
    end.


%%%-------------------------------------------------------------------

handler_kill(Pid) when is_pid(Pid) ->
    erlang:exit(Pid, kill);
handler_kill(_) ->
    ok.

handler_stop(Pid) when is_pid(Pid) ->
    Pid ! {stop, self()};
handler_stop(_) ->
    ok.

handler_start(Name, Mod, State, Instructions) ->
    Args = [Name, self(), Mod, State, Instructions], 
    proc_lib:start_link(?MODULE, handler_init, Args).


handler_init(Name, Parent, Mod, State, Instructions) ->
    put(name, Name ++ "-HANDLER"),
    proc_lib:init_ack(Parent, {ok, self()}),
    d("handler_init -> initiated"),
    handler_main(Parent, Mod, State, Instructions).

handler_main(Parent, Mod, State, []) ->
    d("handler_main -> done when"
      "~n   State: ~p", [State]),
    Result = (catch Mod:terminate(normal, State)),
    Parent ! {handler_result, self(), {ok, Result}},
    receive
	{stop, Parent} ->
	    exit(normal);
	{'EXIT', Parent, Reason} ->
	    exit({parent_died, Reason})
    end;
	
handler_main(Parent, Mod, State, [Instruction|Instructions]) ->
    d("handler_main -> entry with"
      "~n      Instruction: ~p", [Instruction]),
    receive
	{stop, Parent} ->
	    d("handler_main -> premature stop requested"),
	    Result = (catch Mod:terminate(stopped, State)),
	    exit({stopped, Result});
	{'EXIT', Parent, Reason} ->
	    d("handler_main -> parent exited"
	      "~n   Reason: ~p", [Reason]),
	    Result = (catch Mod:terminate({parent_died, Reason}, State)),
	    exit({parent_died, Reason, Result})
    after 0 ->
            d("handler_main -> exec: "
              "~n      Instruction: ~p", [Instruction]),
	    case (catch handler_callback_exec(Mod, State, Instruction)) of
		{ok, NewState} ->
		    handler_main(Parent, Mod, NewState, Instructions);
		{error, Reason} ->
		    d("handler_main -> exec failed"
		      "~n   Reason: ~p", [Reason]),
		    case (catch Mod:terminate(normal, State)) of
			{ok, Result} ->
			    Parent ! {handler_result, self(), {error, Result}};
			Error ->
			    Result = {bad_terminate, Error},
			    Parent ! {handler_result, self(), {error, Result}}
		    end,
		    receive
			{stop, Parent} ->
			    exit(normal);
			{'EXIT', Parent, Reason} ->
			    exit({parent_died, Reason})
		    end;
		{'EXIT', Reason} ->
		    d("handler_main -> exec EXIT"
		      "~n   Reason: ~p", [Reason]),
		    exit({callback_exec_exit, Reason})
		    
	    end
    end.

handler_callback_exec(Mod, State, Instruction) ->
    Mod:handle_exec(Instruction, State).

    
%%%-------------------------------------------------------------------

maybe_start_timer(Timeout) when is_integer(Timeout) ->
    erlang:send_after(Timeout, self(), handler_timeout);
maybe_start_timer(_) ->
    undefined.


maybe_stop_timer(undefined) ->
    ok;
maybe_stop_timer(Timer) ->
    (catch erlang:cancel_timer(Timer)).


%%% ----------------------------------------------------------------

call(Server, Request) ->
    call(Server, Request, infinity).

call(Server, Request, Timeout) ->
    case (catch gen_server:call(Server, Request, Timeout)) of
        {'EXIT', _} ->
            {error, not_started};
        Res ->
            Res
    end.

%% cast(Server, Msg) ->
%%     case (catch gen_server:cast(Server, Msg)) of
%%         {'EXIT', _} ->
%%             {error, not_started};
%%         Res ->
%%             Res
%%     end.


%%% ----------------------------------------------------------------

sleep(X) when is_integer(X) andalso (X =< 0) -> ok;
sleep(X) -> receive after X -> ok end.

sz(Bin) when is_binary(Bin) ->
    size(Bin);
sz(L) when is_list(L) ->
    length(L);
sz(_) ->
    -1.


%%% ----------------------------------------------------------------

d(F)    -> debug(F).
d(F, A) -> debug(F, A).

e(F, A) -> error(F, A).

%% p(P,    F, A) -> print(P,    F, A).
%% p(P, N, F, A) -> print(P, N, F, A).


%% -------------------------

debug(F) ->
    debug(F, []).

debug(F, A) ->
    debug(get(debug), F, A).

debug(true, F, A) ->
    print(false, " DBG", F, A);
debug(_, _, _) ->
    ok.


print(Pre, F, A) ->
    print(true, Pre, F, A).


error(F, A) ->
    print(true, " ERROR", F, A).


print(true = _Verbose, Pre, F, A) ->
    FStr = ?F("*** [~s] ~p ~s~s *** " ++ 
                  "~n   " ++ F ++ "~n~n", 
              [?FTS(), self(), string_name(), Pre | A]),
    io:format(user, FStr, []),
    io:format(standard_io, FStr, []);
print(false = _Verbose, Pre, F, A) ->
    FStr = ?F("*** [~s] ~p ~s~s *** " ++ 
                  "~n   " ++ F ++ "~n~n", 
              [?FTS(), self(), string_name(), Pre | A]),
    io:format(FStr, []).

string_name() ->
    case get(name) of
        N when is_list(N) ->
            N;
        undefined ->
            "";
        N when is_atom(N) ->
            atom_to_list(N)
    end.

