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

%%---------------------------------------------------------------
%% Basic negotiated options with Telnet (RFC 854)
%%
%% Side A request: I WILL set option Opt.
%% Side B answer:  DO go ahead, or no, DON'T set it.
%% 
%% Side A request: Please DO set this option. 
%% Side B answer:  Ok I WILL, or no, I WON'T set it.
%%
%% "Enable option" requests may be rejected. 
%% "Disable option" requests must not.
%%---------------------------------------------------------------

-module(ct_telnet_client).
-moduledoc false.

%%-define(debug, true).

-export([open/2, open/3, open/4, open/5, open/6, close/1]).
-export([send_data/2, send_data/3, get_data/1]).

-define(TELNET_PORT, 23).
-define(OPEN_TIMEOUT,10000).
-define(IDLE_TIMEOUT,8000).

%% telnet control characters
-define(SE,	240).
-define(NOP,	241).
-define(DM,	242).
-define(BRK,	243).
-define(IP,	244).
-define(AO,	245).
-define(AYT,	246).
-define(EC,	247).
-define(EL,	248).
-define(GA,	249).
-define(SB,	250).
-define(WILL,	251).
-define(WONT,	252).
-define(DO,	253).
-define(DONT,	254).
-define(IAC,	255).

%% telnet options
-define(BINARY,            0).
-define(ECHO,	           1).
-define(SUPPRESS_GO_AHEAD, 3).
-define(TERMINAL_TYPE,     24).  
-define(WINDOW_SIZE,       31).

-record(state,{conn_name, get_data, keep_alive=true, log_pos=1}).

open(Server, ConnName) ->
    open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT, true, false, ConnName).

open(Server, Port, ConnName) ->
    open(Server, Port, ?OPEN_TIMEOUT, true, false, ConnName).

open(Server, Port, Timeout, ConnName) ->
    open(Server, Port, Timeout, true, false, ConnName).

open(Server, Port, Timeout, KeepAlive, ConnName) ->
    open(Server, Port, Timeout, KeepAlive, false, ConnName).

open(Server, Port, Timeout, KeepAlive, NoDelay, ConnName) ->
    Self = self(),
    {Pid, MRef} = spawn_monitor(fun() ->
                                        init(Self, Server, Port, Timeout,
                                             KeepAlive, NoDelay, ConnName)
                                end),
    receive
        {Result, Pid} ->
            demonitor(MRef, [flush]),
            if Result == open -> {ok, Pid}; true -> Result end;
        {'DOWN', MRef, process, _, _} = T ->
            {error, T}
    end.

close(Pid) ->
    Pid ! {close,self()},
    receive closed -> ok 
    after 5000 -> ok
    end.	    

send_data(Pid, Data) ->
    send_data(Pid, Data, "\n").
send_data(Pid, Data, true) ->
    send_data(Pid, Data, "\n");
send_data(Pid, Data, Newline) when is_list(Newline) ->
    send_data(Pid, Data++Newline, false);
send_data(Pid, Data, false) ->
    Pid ! {send_data, Data},
    ok.

get_data(Pid) ->
    MRef = monitor(process, Pid),
    Pid ! {get_data, self()},
    receive
        {data, Data} ->
            demonitor(MRef, [flush]),
            {ok, Data};
        {'DOWN', MRef, process, _, _} = T ->
            {error, T}
    end.

%%%-----------------------------------------------------------------
%%% Internal functions
init(Parent, Server, Port, Timeout, KeepAlive, NoDelay, ConnName) ->
    ct_util:mark_process(),
    case gen_tcp:connect(Server, Port, [list,{packet,0},{nodelay,NoDelay}], Timeout) of
	{ok,Sock} ->
	    dbg("~tp connected to: ~tp (port: ~w, keep_alive: ~w)\n",
		[ConnName,Server,Port,KeepAlive]),
	    send([?IAC,?DO,?SUPPRESS_GO_AHEAD], Sock, ConnName),	      
	    Parent ! {open,self()},
	    loop(#state{conn_name=ConnName, get_data=10, keep_alive=KeepAlive},
		 Sock, []),
	    gen_tcp:close(Sock);
        Error ->
	    Parent ! {Error,self()}
    end.

loop(State, Sock, Acc) ->
    receive
	{tcp_closed,_} ->
	    dbg("Connection closed\n", []),
	    Data = lists:reverse(lists:append(Acc)),
	    dbg("Printing queued messages: ~tp",[Data]),
	    ct_telnet:log(State#state.conn_name,
			  general_io, "~ts",
			  [lists:sublist(Data,
					 State#state.log_pos,
					 length(Data))]),
	    receive
		{get_data,Pid} ->
		    Pid ! closed
	    after 100 ->
		    ok
	    end;
	{tcp,_,Msg0} ->
	    dbg("rcv tcp msg: ~tp~n",[Msg0]),
	    Msg = check_msg(Sock,Msg0,[]),
	    loop(State, Sock, [Msg | Acc]);
	{send_data,Data} ->
	    send(Data, Sock, State#state.conn_name),
	    loop(State, Sock, Acc);
	{get_data,Pid} ->
	    NewState = 
		case Acc of
		    [] ->
			dbg("get_data nodata\n",[]),
			erlang:send_after(100,self(),{get_data_delayed,Pid}),
			if State#state.keep_alive == true ->
				State#state{get_data=State#state.get_data - 1};
			   State#state.keep_alive == false ->
				State
			end;
		    _ ->
			Data = lists:reverse(lists:append(Acc)),
			Len = length(Data),
			dbg("get_data ~tp\n",[Data]),
			ct_telnet:log(State#state.conn_name,
				      general_io, "~ts",
				      [lists:sublist(Data,
						     State#state.log_pos,
						     Len)]),
			Pid ! {data,Data},
			State#state{log_pos = 1}
		end,
	    loop(NewState, Sock, []);
	{get_data_delayed,Pid} ->
	    NewState =
		case State of
		    #state{keep_alive = true, get_data = 0} ->
			dbg("sending NOP\n",[]),
			if Acc == [] -> send([?IAC,?NOP], Sock, 
					     State#state.conn_name);
			   true -> ok
			end,
			State#state{get_data=10};
		    _ ->
			State
		end,
	    {NewAcc,Pos} = 
		case erlang:is_process_alive(Pid) of
		    true when Acc /= [] ->
			Data = lists:reverse(lists:append(Acc)),
			Len = length(Data),
			dbg("get_data_delayed ~tp\n",[Data]),
			ct_telnet:log(State#state.conn_name,
				      general_io, "~ts",
				      [lists:sublist(Data,
						     State#state.log_pos,
						     Len)]),
			Pid ! {data,Data},
			{[],1};
		    true when Acc == [] ->
			dbg("get_data_delayed nodata\n",[]),
			Pid ! {data,[]},
			{[],1};
		    false ->
			{Acc,NewState#state.log_pos}
		end,
	    loop(NewState#state{log_pos=Pos}, Sock, NewAcc);			       
	{close,Pid} ->
	    dbg("Closing connection\n", []),
	    if Acc == [] ->
		    ok;
	       true ->
		    Data = lists:reverse(lists:append(Acc)),
		    dbg("Printing queued messages: ~tp",[Data]),
		    ct_telnet:log(State#state.conn_name,
				  general_io, "~ts",
				  [lists:sublist(Data,
						 State#state.log_pos,
						 length(Data))])
	    end,
	    gen_tcp:close(Sock),
	    Pid ! closed
    after wait(State#state.keep_alive,?IDLE_TIMEOUT) ->
	    dbg("idle timeout\n",[]),
	    Data = lists:reverse(lists:append(Acc)),
	    case Data of
		[] ->
		    dbg("sending NOP\n",[]),
		    send([?IAC,?NOP], Sock, State#state.conn_name),
		    loop(State, Sock, Acc);
		_ when State#state.log_pos == length(Data)+1 ->
		    loop(State, Sock, Acc);
		_ ->
		    dbg("idle timeout, printing ~tp\n",[Data]),
		    Len = length(Data),
		    ct_telnet:log(State#state.conn_name,
				  general_io, "~ts",
				  [lists:sublist(Data,
						 State#state.log_pos,
						 Len)]),
		    loop(State#state{log_pos = Len+1}, Sock, Acc)
	    end
    end.

wait(true, Time) -> Time;
wait(false, _) -> infinity.   

send(Data, Sock, ConnName) ->
    case Data of
	[?IAC|_] = Cmd ->
	    cmd_dbg("Sending",Cmd),
	    try io_lib:format("[~w] ~w", [?MODULE,Data]) of
		Str ->
		    ct_telnet:log(ConnName, general_io, Str, [])
	    catch
		_:_ -> ok
	    end;
	_ ->
	    dbg("Sending: ~tp\n", [Data]),
	    try io_lib:format("[~w] ~ts", [?MODULE,Data]) of
		Str ->
		    ct_telnet:log(ConnName, general_io, Str, [])
	    catch
		_:_ -> ok
	    end
    end,
    ok = gen_tcp:send(Sock, Data),
    ok.

%% [IAC,IAC] = buffer data value 255
check_msg(Sock, [?IAC,?IAC | T], Acc) ->
    check_msg(Sock, T, [?IAC|Acc]);

%% respond to a command
check_msg(Sock, [?IAC | Cs], Acc) ->
    case get_cmd(Cs) of
	{Cmd,Cs1} ->
	    cmd_dbg("Got",Cmd),
	    ok = respond_cmd(Cmd, Sock),
	    check_msg(Sock, Cs1, Acc); 
	error ->
	    Acc
    end;

%% buffer a data value
check_msg(Sock, [H|T], Acc) ->
    check_msg(Sock, T, [H|Acc]);

check_msg(_Sock, [], Acc) ->
    Acc.


%% Positive responses (WILL and DO).

respond_cmd([?WILL,?ECHO], Sock) ->
    R = [?IAC,?DO,?ECHO],
    cmd_dbg("Responding",R),
    gen_tcp:send(Sock, R);

respond_cmd([?DO,?ECHO], Sock) ->
    R = [?IAC,?WILL,?ECHO],
    cmd_dbg("Responding",R),
    gen_tcp:send(Sock, R);

%% Answers from server

respond_cmd([?WILL,?SUPPRESS_GO_AHEAD], _Sock) ->
    dbg("Server will suppress-go-ahead\n", []);

respond_cmd([?WONT,?SUPPRESS_GO_AHEAD], _Sock) ->
    dbg("Warning! Server won't suppress-go-ahead\n", []);

respond_cmd([?DONT | _Opt], _Sock) ->		% server ack?
    ok;						
respond_cmd([?WONT | _Opt], _Sock) ->		% server ack?
    ok;						

%% Negative responses (WON'T and DON'T). These are default!

respond_cmd([?WILL,Opt], Sock) ->
    R = [?IAC,?DONT,Opt],
    cmd_dbg("Responding",R),
    gen_tcp:send(Sock, R);

respond_cmd([?DO | Opt], Sock) ->
    R = [?IAC,?WONT | Opt],
    cmd_dbg("Responding",R),
    gen_tcp:send(Sock, R);

%% Commands without options (which we ignore)

respond_cmd(?NOP, _Sock) ->
    ok;

%% Unexpected messages.

respond_cmd([Cmd | Opt], _Sock) when Cmd >= 240, Cmd =< 255 ->
    dbg("Received cmd: ~w. Ignored!\n", [[Cmd | Opt]]);

respond_cmd([Cmd | Opt], _Sock)  ->
    dbg("WARNING: Received unknown cmd: ~w. Ignored!\n", [[Cmd | Opt]]).


get_cmd([Cmd | Rest]) when Cmd == ?SB ->
    get_subcmd(Rest, []);

get_cmd([Cmd | Rest]) when Cmd >= 240, Cmd =< 249 ->
    {?NOP, Rest};

get_cmd([Cmd,Opt | Rest]) when Cmd >= 251, Cmd =< 254 ->
    {[Cmd,Opt], Rest};

get_cmd(_Other) ->
    error.

get_subcmd([?SE | Rest], Acc) ->
    {[?SE | lists:reverse(Acc)], Rest};

get_subcmd([Opt | Rest], Acc) ->
    get_subcmd(Rest, [Opt | Acc]).

-ifdef(debug).
dbg(Str,Args) ->
    TS = timestamp(),
    io:format("[~p ct_telnet_client, ~s]\n" ++ Str,[self(),TS|Args]).

cmd_dbg(Prefix,Cmd) ->
    case Cmd of
	[?IAC|Cmd1] ->
	    cmd_dbg(Prefix,Cmd1);
	[Ctrl|Opts] ->
	    CtrlStr =
		case Ctrl of
		    ?DO ->   "DO";
		    ?DONT -> "DONT";
		    ?WILL -> "WILL";
		    ?WONT -> "WONT";
		    ?NOP ->  "NOP";
		    _ ->     "CMD"
		end,
	    Opts1 =
		case Opts of
		    [Opt] -> Opt;
		    _ -> Opts
		end,
	    dbg("~ts: ~ts(~w): ~w\n", [Prefix,CtrlStr,Ctrl,Opts1]);
	Any  ->
	    dbg("Unexpected in cmd_dbg:~n~w~n",[Any])
    end.

timestamp() ->
    {MS,S,US} = os:timestamp(),
    {{Year,Month,Day}, {Hour,Min,Sec}} =
        calendar:now_to_local_time({MS,S,US}),
    MilliSec = trunc(US/1000),
    lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B "
                                "~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0B",
                                [Year,Month,Day,Hour,Min,Sec,MilliSec])).
-else.
dbg(_Str,_Args) ->
    ok.

cmd_dbg(_Prefix,_Cmd) ->
    ok.
-endif.
