%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2009-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%
%%

%%

-module(ssh_shell).
-moduledoc false.

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

%%% As this is an user interactive client it behaves like a daemon
%%% channel in spite of it being a client. 
-behaviour(ssh_server_channel).

%% ssh_server_channel callbacks
-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]).

%% Spawn export
-export([input_loop/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, 
	{
	 io,      %% Io process
	 channel, %% Id of the ssh channel
	 cm       %% Ssh connection manager
	 }
       ).

%%====================================================================
%% ssh_server_channel callbacks
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} 
%%                        
%% Description: Initiates the CLI
%%--------------------------------------------------------------------
init([ConnectionManager, ChannelId] = Args) ->
    
    %% Make sure that we are proclib compatible as
    %% this client should be runnable from the
    %% erlang shell.
    case get('$initial_call') of
	undefined ->
	    Me = get_my_name(),
	    Ancestors = get_ancestors(),
	    put('$ancestors', [Me | Ancestors]),
	    put('$initial_call', {?MODULE, init, Args});
	_ ->
	    ok
    end,

    case ssh_connection:shell(ConnectionManager, ChannelId) of
	ok ->
	    {group_leader, GIO} = 
		process_info(self(), group_leader),
	    IoPid = spawn_link(?MODULE, input_loop,
			       [GIO, self()]),
	    {ok, #state{io = IoPid, 
			channel = ChannelId, 
			cm = ConnectionManager}};
	Error ->
	    {stop, Error}
    end.

%%--------------------------------------------------------------------
%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State}
%%                        
%% Description: Handles channel messages received on the ssh-connection.
%%--------------------------------------------------------------------
handle_ssh_msg({ssh_cm, _, {data, _ChannelId, 0, Data}}, State) ->
    %% TODO: When unicode support is ready
    %% should we call this function or perhaps a new
    %% function.
    io:format("~ts", [Data]),
    {ok, State};

handle_ssh_msg({ssh_cm, _, 
		{data, _ChannelId, ?SSH_EXTENDED_DATA_STDERR, Data}},
	       State) ->
    %% TODO: When unicode support is ready
    %% should we call this function or perhaps a new
    %% function.
    io:format("~ts", [Data]),
    {ok, State};

handle_ssh_msg({ssh_cm, _, {eof, _ChannelId}}, State) ->
    {ok, State};

handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) ->
    %% Ignore signals according to RFC 4254 section 6.9.
    {ok, State};

handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, State) ->
    io:put_chars("Connection closed by peer"),
    %% TODO: When unicode support is ready
    %% should we call this function or perhaps a new
    %% function. The error is encoded as UTF-8!
    io:put_chars(Error),
    {stop, ChannelId,  State};

handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, 0}}, State) ->
    io:put_chars("logout"),
    io:put_chars("Connection closed"),
    {stop, ChannelId, State};

handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State) ->
    io:put_chars("Connection closed by peer"),
    io:put_chars("Status: " ++ integer_to_list(Status)),
    {stop, ChannelId, State}.

%%--------------------------------------------------------------------
%% Function: handle_msg(Args) -> {ok, State} | {stop, ChannelId, State}
%%                        
%% Description: Handles other channel messages
%%--------------------------------------------------------------------
handle_msg({ssh_channel_up, ChannelId, ConnectionManager},
	   #state{channel = ChannelId,
		  cm = ConnectionManager} = State) ->
    {ok,  State};

handle_msg({input, IoPid, eof}, #state{io = IoPid, channel = ChannelId, 
				       cm = ConnectionManager} = State) ->
    ssh_connection:send_eof(ConnectionManager, ChannelId),
    {ok, State};

handle_msg({input, IoPid, Line0}, #state{io = IoPid,
					channel = ChannelId,
					cm = ConnectionManager} = State) ->
    %% Not nice, but it make it work somehow
    Line = case encoding(Line0) of
               utf8 ->
                   Line0;
               unicode ->
                   unicode:characters_to_binary(Line0);
               latin1 ->
                   unicode:characters_to_binary(Line0,latin1,utf8)
           end,
    ssh_connection:send(ConnectionManager, ChannelId, Line),
    {ok, State}.
    
%%--------------------------------------------------------------------
%% Function: terminate(Reasons, State) -> _
%%                        
%% Description: Cleanup when shell channel is terminated
%%--------------------------------------------------------------------
terminate(_Reason, #state{io = IoPid}) ->
    exit(IoPid, kill).
    
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
encoding(Bin) ->
    case unicode:characters_to_binary(Bin,utf8,utf8) of
	Bin ->
	    utf8;
        Bin2 when is_binary(Bin2) ->
            unicode;
	_ ->
	    latin1
    end.

%%--------------------------------------------------------------------
input_loop(Fd, Pid) ->
    case io:get_line(Fd, '') of
	eof ->
	    Pid ! {input, self(), eof},
	    ok; 
	Line ->
	    Pid ! {input, self(), Line},
	    input_loop (Fd, Pid)
    end.
    
get_my_name() ->
    case process_info(self(),registered_name) of
	{registered_name,Name} -> Name;
	_                      -> self()
    end.

get_ancestors() ->
    case get('$ancestors') of
	A when is_list(A) -> A;
	_              -> []
    end.

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

ssh_dbg_trace_points() -> [terminate, shell].

ssh_dbg_flags(shell) -> [c];
ssh_dbg_flags(terminate) -> [c].

ssh_dbg_on(shell) -> dbg:tp(?MODULE,handle_ssh_msg,2,x);
ssh_dbg_on(terminate) -> dbg:tp(?MODULE,  terminate, 2, x).

ssh_dbg_off(shell) -> dbg:ctpg(?MODULE,handle_ssh_msg,2);
ssh_dbg_off(terminate) -> dbg:ctpg(?MODULE, terminate, 2).


ssh_dbg_format(shell, {call,{?MODULE,handle_ssh_msg,
                           [{ssh_cm, _ConnectionHandler, Request},
                            #state{channel=Ch}]}}) when is_tuple(Request) ->
    [io_lib:format("SHELL conn ~p chan ~p, req ~p", 
                   [self(),Ch,element(1,Request)]),
     case Request of
         {window_change, ChannelId, Width, Height, PixWidth, PixHeight} ->
             fmt_kv([{channel_id,ChannelId}, 
                     {width,Width}, {height,Height},
                     {pix_width,PixWidth}, {pixel_hight,PixHeight}]);

         {env, ChannelId, WantReply, Var, Value} ->
             fmt_kv([{channel_id,ChannelId}, {want_reply,WantReply}, {Var,Value}]);

         {exec, ChannelId, WantReply, Cmd} ->
             fmt_kv([{channel_id,ChannelId}, {want_reply,WantReply}, {command,Cmd}]);

         {pty, ChannelId, WantReply,
          {TermName, Width, Height, PixWidth, PixHeight, Modes}} ->
             fmt_kv([{channel_id,ChannelId}, {want_reply,WantReply}, 
                     {term,TermName},
                     {width,Width}, {height,Height}, {pix_width,PixWidth}, {pixel_hight,PixHeight},
                     {pty_opts, Modes}]);

         {data, ChannelId, Type, Data} -> 
             fmt_kv([{channel_id,ChannelId},
                     {type, case Type of
                                0 -> "0 (normal data)";
                                1 -> "1 (extended data, i.e. errors)";
                                _ -> Type
                            end},
                     {data, ssh_dbg:shrink_bin(Data)},
                     {hex, h, Data}
                    ]);
         _ ->
             io_lib:format("~nunder construction:~nRequest = ~p",[Request])
     end];
ssh_dbg_format(shell, {call,{?MODULE,handle_ssh_msg,_}}) -> skip;
ssh_dbg_format(shell, {return_from,{?MODULE,handle_ssh_msg,2},_Result}) -> skip;

ssh_dbg_format(terminate, {call, {?MODULE,terminate, [Reason, State]}}) ->
    ["Shell 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.

?wr_record(state).

fmt_kv(KVs) -> lists:map(fun fmt_kv1/1, KVs).

fmt_kv1({K,V})   -> io_lib:format("~n~p: ~p",[K,V]);
fmt_kv1({K,s,V}) -> io_lib:format("~n~p: ~s",[K,V]);
fmt_kv1({K,h,V}) -> io_lib:format("~n~p: ~s",[K, [$\n|ssh_dbg:hex_dump(V)]]).
    
