%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2008-2023. 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%
%%

%%

-module(ssh_client_channel).

-include("ssh.hrl").
-include("ssh_connect.hrl").

-callback init(Args :: term()) ->
    {ok, State :: term()} | {ok, State :: term(), timeout() | hibernate} |
    {stop, Reason :: term()} | ignore.
-callback handle_call(Request :: term(), From :: {pid(), Tag :: term()},
                      State :: term()) ->
    {reply, Reply :: term(), NewState :: term()} |
    {reply, Reply :: term(), NewState :: term(), timeout() | hibernate} |
    {noreply, NewState :: term()} |
    {noreply, NewState :: term(), timeout() | hibernate} |
    {stop, Reason :: term(), Reply :: term(), NewState :: term()} |
    {stop, Reason :: term(), NewState :: term()}.
-callback handle_cast(Request :: term(), State :: term()) ->
    {noreply, NewState :: term()} |
    {noreply, NewState :: term(), timeout() | hibernate} |
    {stop, Reason :: term(), NewState :: term()}.

-callback terminate(Reason :: (normal | shutdown | {shutdown, term()} |
                               term()),
                    State :: term()) ->
    term().
-callback code_change(OldVsn :: (term() | {down, term()}), State :: term(),
                      Extra :: term()) ->
    {ok, NewState :: term()} | {error, Reason :: term()}.

-callback handle_msg(Msg ::term(), State :: term()) ->
    {ok, State::term()} | {stop, ChannelId::ssh:channel_id(), State::term()}. 

-callback handle_ssh_msg(ssh_connection:event(),
 			 State::term()) -> {ok, State::term()} | 
 					   {stop, ChannelId::ssh:channel_id(), 
 					    State::term()}.
-behaviour(gen_server).

%%% API
-export([start/4, start/5, start_link/4, start_link/5, call/2, call/3,
	 cast/2, reply/2, enter_loop/1]).

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

%% Internal application API
-export([cache_create/0, cache_lookup/2, cache_update/2, 
	 cache_delete/1, cache_delete/2,  cache_foldl/3,
	 cache_info/2,  cache_find/2,
	 get_print_info/1, get_print_info/2
        ]).

-behaviour(ssh_dbg).
-export([ssh_dbg_trace_points/0, ssh_dbg_flags/1, ssh_dbg_on/1, ssh_dbg_off/1, ssh_dbg_format/2]).

-record(state, {
	  cm,
	  channel_cb,
	  channel_state,
	  channel_id,
	  close_sent = false
	 }).

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

call(ChannelPid, Msg) ->
    call(ChannelPid, Msg, infinity).

call(ChannelPid, Msg, TimeOute) ->
    try gen_server:call(ChannelPid, Msg, TimeOute) of
	Result ->
	    Result
    catch 
 	exit:{noproc, _} ->
 	    {error, closed};
	exit:{normal, _} ->
	    {error, closed};
	exit:{shutdown, _} ->
	    {error, closed};
	exit:{{shutdown, _}, _} ->
	    {error, closed};
 	exit:{timeout, _} ->
 	    {error, timeout}
    end.

cast(ChannelPid, Msg) ->
    gen_server:cast(ChannelPid, Msg).


reply(From, Msg) ->
    gen_server:reply(From, Msg).

%%====================================================================
%% Internal application API
%%====================================================================

%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start(ConnectionManager, ChannelId, CallBack, CbInitArgs) ->
    start(ConnectionManager, ChannelId, CallBack, CbInitArgs, undefined).

start(ConnectionManager, ChannelId, CallBack, CbInitArgs, Exec) ->
    Options = [{channel_cb, CallBack},
	       {channel_id, ChannelId},
	       {init_args, CbInitArgs},
	       {cm, ConnectionManager},
	       {exec, Exec}],	  
    gen_server:start(?MODULE, [Options], []).

start_link(ConnectionManager, ChannelId, CallBack, CbInitArgs) ->
    start_link(ConnectionManager, ChannelId, CallBack, CbInitArgs, undefined).

start_link(ConnectionManager, ChannelId, CallBack, CbInitArgs, Exec) ->
    Options = [{channel_cb, CallBack},
	       {channel_id, ChannelId},
	       {init_args, CbInitArgs},
	       {cm, ConnectionManager},
	       {exec, Exec}],	  
    gen_server:start_link(?MODULE, [Options], []).

enter_loop(State) ->
    gen_server:enter_loop(?MODULE, [], State).

%%====================================================================
%% gen_server callbacks
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%%                         {ok, State, Timeout} |
%%                         ignore               |
%%                         {stop, Reason}
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([Options]) ->    
    Cb = proplists:get_value(channel_cb, Options),
    ConnectionManager =  proplists:get_value(cm, Options),
    ChannelId = proplists:get_value(channel_id, Options),
    process_flag(trap_exit, true),
    try Cb:init(channel_cb_init_args(Options)) of
	{ok, ChannelState} ->
	    State = #state{cm = ConnectionManager, 
			   channel_cb = Cb,
			   channel_id = ChannelId,
			   channel_state = ChannelState},
	    self() ! {ssh_channel_up, ChannelId, ConnectionManager}, 
	    {ok, State};
	{ok, ChannelState, Timeout} ->
	    State = #state{cm = ConnectionManager, 
			   channel_cb = Cb,
			   channel_id = ChannelId,
			   channel_state = ChannelState},
	    self() ! {ssh_channel_up, ChannelId, ConnectionManager}, 
	    {ok, State, Timeout};
	{stop, Why} ->
	    {stop, Why}
    catch 
        _:undef ->
            {stop, {bad_channel_callback_module,Cb}};
	_:Reason ->
	    {stop, Reason}
    end.

channel_cb_init_args(Options) ->
    case proplists:get_value(exec, Options) of
        undefined ->
            proplists:get_value(init_args, Options);
        Exec ->
            proplists:get_value(init_args, Options) ++ [Exec]
    end.

%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%%                                      {reply, Reply, State, Timeout} |
%%                                      {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, Reply, State} |
%%                                      {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call(get_print_info, _From, State) ->
    Reply =
	{{State#state.cm,
	  State#state.channel_id},
	 io_lib:format('CB=~p',[State#state.channel_cb])
	},
    {reply, Reply, State};

handle_call({get_print_info,channel_cb}, _From, State) ->
    {reply, State#state.channel_cb, State};

handle_call(Request, From, #state{channel_cb = Module, 
				  channel_state = ChannelState} = State) ->
   try Module:handle_call(Request, From, ChannelState) of
       Result ->
	   handle_cb_result(Result, State)
   catch
       error:{undef, _} -> 
	   {noreply, State}
   end.


%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast(Msg, #state{channel_cb = Module, 
			channel_state = ChannelState} = State) ->
    
    try Module:handle_cast(Msg, ChannelState) of
	Result ->
	    handle_cb_result(Result, State)
    catch
       error:{undef, _} -> 
	    {noreply, State}
   end.

%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%%                                       {noreply, State, Timeout} |
%%                                       {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info({ssh_cm, ConnectionManager, {closed, _ChannelId}},  
	    #state{cm = ConnectionManager,
		   close_sent = true} = State) ->
    {stop, normal, State};
handle_info({ssh_cm, ConnectionManager, {closed, ChannelId}},  
	    #state{cm = ConnectionManager, 
		   close_sent = false} = State) ->
    %% To be on the safe side, i.e. the manager has already been terminated.
    (catch ssh_connection:close(ConnectionManager, ChannelId)),
    {stop, normal, State#state{close_sent = true}};

handle_info({ssh_cm, _, _} = Msg, #state{channel_cb = Module, 
                                         channel_state = ChannelState0} = State) ->
    try Module:handle_ssh_msg(Msg, ChannelState0) of
	{ok, ChannelState} ->
	    adjust_window(Msg),
	    {noreply, State#state{channel_state = ChannelState}};
	{ok, ChannelState, Timeout} ->
	    adjust_window(Msg),
	    {noreply, State#state{channel_state = ChannelState}, Timeout};
	{stop, ChannelId, ChannelState} ->
            do_the_close(Msg, ChannelId, State#state{channel_state = ChannelState})
    catch
        error:_ ->
            do_the_close(Msg, State#state.channel_id, State)
    end;

handle_info(Msg, #state{channel_cb = Module, 
			channel_state = ChannelState0} = State) -> 
    try Module:handle_msg(Msg, ChannelState0)
    of 
	{ok, ChannelState} ->
	    {noreply, State#state{channel_state = ChannelState}};
	{ok, ChannelState, Timeout} ->
	    {noreply, State#state{channel_state = ChannelState}, Timeout};
	{stop, ChannelId, ChannelState} ->
            do_the_close(Msg, ChannelId, State#state{channel_state = ChannelState})
    catch
        error:function_clause when tuple_size(Msg) == 3,
                                   element(1,Msg) == 'EXIT' ->
            do_the_close(Msg, State#state.channel_id, State)
    end.



do_the_close(Msg, ChannelId, State = #state{close_sent = false,
                                            cm = ConnectionManager}) ->
    catch ssh_connection:close(ConnectionManager, ChannelId),
    do_the_close(Msg, ChannelId, State#state{close_sent=true});
do_the_close({'EXIT', _Pid, shutdown=Reason},     _, State) -> {stop, Reason, State};
do_the_close({'EXIT', _Pid, {shutdown,_}=Reason}, _, State) -> {stop, Reason, State};
do_the_close(_Msg,                                _, State) -> {stop, normal, State}.


%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(Reason, #state{cm = ConnectionManager, 
 			 channel_id = ChannelId,
 			 close_sent = false} = State) ->
    catch ssh_connection:close(ConnectionManager, ChannelId),
    terminate(Reason, State#state{close_sent = true});
terminate(Reason, #state{channel_cb = Cb, channel_state = ChannelState}) ->
    catch Cb:terminate(Reason, ChannelState),
    ok.

%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(OldVsn, #state{channel_cb = Module, 
			   channel_state = ChannelState0} = State, Extra) ->
    {ok, ChannelState} = Module:code_change(OldVsn, ChannelState0, Extra),
    {ok, State#state{channel_state = ChannelState}}.

%%====================================================================
%% Internal application API
%%====================================================================
cache_create() ->
    ets:new(cm_tab, [set,{keypos, #channel.local_id}]).

cache_lookup(Cache, Key) ->
    case ets:lookup(Cache, Key) of
	[Channel] ->
	    Channel;
	[] ->
	    undefined
    end.

cache_update(Cache, #channel{local_id = Id} = Entry) when Id =/= undefined ->
    ets:insert(Cache, Entry).

cache_delete(Cache, Key) ->
    ets:delete(Cache, Key).

cache_delete(Cache) ->
    ets:delete(Cache).

cache_foldl(Fun, Acc, Cache) ->
    ets:foldl(Fun, Acc, Cache).
    
cache_info(num_entries, Cache) ->
    proplists:get_value(size, ets:info(Cache)).

cache_find(ChannelPid, Cache) ->
   case ets:match_object(Cache, #channel{user = ChannelPid}) of
       [] ->
	   undefined;
       [Channel] ->
	   Channel
   end.

get_print_info(Pid) ->
    call(Pid, get_print_info, 1000).

get_print_info(Pid, Arg) ->
    call(Pid, {get_print_info,Arg}, 1000).

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
handle_cb_result({reply, Reply, ChannelState}, State) ->
    {reply, Reply, State#state{channel_state = ChannelState}};
handle_cb_result({reply, Reply, ChannelState, Timeout}, State) ->
    {reply, Reply,State#state{channel_state = ChannelState}, Timeout};
handle_cb_result({noreply, ChannelState}, State) ->
    {noreply, State#state{channel_state = ChannelState}};
handle_cb_result({noreply, ChannelState, Timeout}, State) ->
    {noreply, State#state{channel_state = ChannelState}, Timeout};
handle_cb_result({stop, Reason, Reply, ChannelState}, State) ->
    {stop, Reason, Reply,  State#state{channel_state = ChannelState}};
handle_cb_result({stop, Reason, ChannelState}, State) ->
    {stop, Reason, State#state{channel_state = ChannelState}}.

adjust_window({ssh_cm, ConnectionManager,
	       {data, ChannelId, _, Data}}) ->
    ssh_connection:adjust_window(ConnectionManager, ChannelId, byte_size(Data));
adjust_window(_) ->
    ok.
    

%%%################################################################
%%%#
%%%# Tracing
%%%#

ssh_dbg_trace_points() -> [terminate, channels, channel_events].

ssh_dbg_flags(channels) -> [c];
ssh_dbg_flags(terminate) -> [c];
ssh_dbg_flags(channel_events) -> [c].

ssh_dbg_on(terminate) -> dbg:tp(?MODULE,  terminate, 2, x);
ssh_dbg_on(channels) -> dbg:tp(?MODULE,  init, 1, x),
                        ssh_dbg_on(terminate);
ssh_dbg_on(channel_events) -> dbg:tp(?MODULE,  handle_call, 3, x),
                              dbg:tp(?MODULE,  handle_cast, 2, x),
                              dbg:tp(?MODULE,  handle_info, 2, x).

ssh_dbg_off(terminate) -> dbg:ctpg(?MODULE, terminate, 2);
ssh_dbg_off(channels) -> dbg:ctpg(?MODULE, init, 1),
                         ssh_dbg_off(terminate);
ssh_dbg_off(channel_events) -> dbg:ctpg(?MODULE,  handle_call, 3),
                               dbg:ctpg(?MODULE,  handle_cast, 2),
                               dbg:ctpg(?MODULE,  handle_info, 2).

ssh_dbg_format(channels, {call, {?MODULE,init, [[KVs]]}}) ->
    ["Server Channel Starting:\n",
     io_lib:format("Connection: ~p, ChannelId: ~p, CallBack: ~p\nCallBack init args = ~p", 
                   [proplists:get_value(K,KVs) || K <- [cm, channel_id, channel_cb]]
                   ++ [channel_cb_init_args(KVs)])
    ];
ssh_dbg_format(channels, {return_from, {?MODULE,init,1}, {stop,Reason}}) ->
    ["Server Channel Start FAILED!\n",
     io_lib:format("Reason = ~p", [Reason])
    ];

ssh_dbg_format(channels, F) ->
    ssh_dbg_format(terminate, F);

ssh_dbg_format(terminate, {call, {?MODULE,terminate, [Reason, State]}}) ->
    ["Server Channel Terminating:\n",
     io_lib:format("Reason: ~p,~nState:~n~s", [Reason, wr_record(State)])
    ];
ssh_dbg_format(terminate, {return_from, {?MODULE,terminate,2}, _Ret}) ->
    skip;

ssh_dbg_format(channel_events, {call, {?MODULE,handle_call, [Call,From,State]}}) ->
    [hdr("is called", State),
     io_lib:format("From: ~p~nCall: ~p~n", [From, Call])
    ];
ssh_dbg_format(channel_events, {return_from, {?MODULE,handle_call,3}, Ret}) ->
    ["Server Channel call returned:\n",
     io_lib:format("~p~n", [ssh_dbg:reduce_state(Ret,#state{})])
    ];

ssh_dbg_format(channel_events, {call, {?MODULE,handle_cast, [Cast,State]}}) ->
    [hdr("got cast", State),
     io_lib:format("Cast: ~p~n", [Cast])
    ];
ssh_dbg_format(channel_events, {return_from, {?MODULE,handle_cast,2}, Ret}) ->
    ["Server Channel cast returned:\n",
     io_lib:format("~p~n", [ssh_dbg:reduce_state(Ret,#state{})])
    ];

ssh_dbg_format(channel_events, {call, {?MODULE,handle_info, [Info,State]}}) ->
    [hdr("got info", State),
     io_lib:format("Info: ~p~n", [Info])
    ];
ssh_dbg_format(channel_events, {return_from, {?MODULE,handle_info,2}, Ret}) ->
    ["Server Channel info returned:\n",
     io_lib:format("~p~n", [ssh_dbg:reduce_state(Ret,#state{})])
    ].

hdr(Title, S) ->
    io_lib:format("Server Channel (Id=~p, CB=~p) ~s:\n", [S#state.channel_id, S#state.channel_cb, Title]).

?wr_record(state).


