%%
%% %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%
%%

%%% Common Test Framework Utilities.
%%%
%%% This is a support module for the Common Test Framework. It
%%% implements the process ct_util_server which acts like a data
%%% holder for suite, configuration and connection data.
%%%
-module(ct_util).
-moduledoc false.

-export([start/0, start/1, start/3, start/4,
	 stop/1, update_last_run_index/0]).

-export([register_connection/4, unregister_connection/1,
	 does_connection_exist/3, get_key_from_name/1]).

-export([get_connections/1, close_connections/0]).

-export([save_suite_data/3, save_suite_data/2,
	 save_suite_data_async/3, save_suite_data_async/2,
	 read_suite_data/1, 
	 delete_suite_data/0, delete_suite_data/1, match_delete_suite_data/1,
	 delete_testdata/0, delete_testdata/1, match_delete_testdata/1,
	 set_testdata/1, get_testdata/1, get_testdata/2,
	 set_testdata_async/1, update_testdata/2, update_testdata/3,
	 set_verbosity/1, get_verbosity/1]).

-export([override_silence_all_connections/0, override_silence_connections/1, 
	 get_overridden_silenced_connections/0, 
	 delete_overridden_silenced_connections/0, 
	 silence_all_connections/0, silence_connections/1,
	 is_silenced/1, is_silenced/2, reset_silent_connections/0]).

-export([get_mode/0, create_table/3, read_opts/0]).

-export([set_cwd/1, reset_cwd/0, get_start_dir/0]).

-export([parse_table/1]).

-export([listenv/1]).

-export([get_target_name/1, get_connection/2]).

-export([is_test_dir/1, get_testdir/2]).

-export([kill_attached/2, get_attached/1]).

-export([warn_duplicates/1]).

-export([mark_process/0, mark_process/1, is_marked/1, is_marked/2,
         remaining_test_procs/0]).

-export([get_profile_data/0, get_profile_data/1,
	 get_profile_data/2, open_url/3]).

-include("ct.hrl").
-include("ct_event.hrl").
-include("ct_util.hrl").

-define(default_verbosity, [{default,?MAX_VERBOSITY},
			    {'$unspecified',?MAX_VERBOSITY}]).
-define(default_custom_stylesheet, undefined).

-record(suite_data, {key,name,value}).

%%%-----------------------------------------------------------------
start() ->
    start(normal, ".", ?default_verbosity, ?default_custom_stylesheet).
%%% -spec start(Mode) -> Pid | exit(Error)
%%%       Mode = normal | interactive
%%%       Pid = pid()
%%%
%%% Start start the ct_util_server process
%%% (tool-internal use only).
%%%
%%% This function is called from ct_run.erl. It starts and initiates
%%% the ct_util_server
%%% 
%%% Returns the process identity of the
%%% ct_util_server.
%%%
%%% See ct.
start(LogDir) when is_list(LogDir) ->
    start(normal, LogDir, ?default_verbosity, ?default_custom_stylesheet);
start(Mode) ->
    start(Mode, ".", ?default_verbosity, ?default_custom_stylesheet).

start(LogDir, Verbosity, CustomStylesheet) when is_list(LogDir) ->
    start(normal, LogDir, Verbosity, CustomStylesheet).

start(Mode, LogDir, Verbosity, CustomStylesheet) ->
    case whereis(ct_util_server) of
	undefined ->
	    S = self(),
	    Pid = spawn_link(fun() ->
	        do_start(S, Mode, LogDir, Verbosity, CustomStylesheet)
	    end),
	    receive 
		{Pid,started} -> Pid;
		{Pid,Error} -> exit(Error);
		{_Ref,{Pid,Error}} -> exit(Error)
	    end;
	Pid ->
	    case get_mode() of
		interactive when Mode==interactive ->
		    Pid;
		interactive ->
		    {error,interactive_mode};
		_OtherMode ->
		    Pid
	    end
    end.

do_start(Parent, Mode, LogDir, Verbosity, CustomStylesheet) ->
    process_flag(trap_exit,true),
    register(ct_util_server,self()),
    mark_process(),
    create_table(?conn_table,#conn.handle),
    create_table(?board_table,2),
    create_table(?suite_table,#suite_data.key),

    create_table(?verbosity_table,1),
    _ = [ets:insert(?verbosity_table,{Cat,Lvl}) || {Cat,Lvl} <- Verbosity],

    {ok,StartDir} = file:get_cwd(),
    case file:set_cwd(LogDir) of
	ok -> ok;
	E -> exit(E)
    end,
    DoExit = fun(Reason) -> ok = file:set_cwd(StartDir), exit(Reason) end,
    Opts = case read_opts() of
	       {ok,Opts1} ->
		   Opts1;
	       Error ->
		   Parent ! {self(),Error},
		   DoExit(Error)
	   end,

    %% start an event manager (if not already started by master)
    case ct_event:start_link() of
	{error,{already_started,_}} ->
	    ok;
	_ ->
            ct_event:add_handler()
    end,

    %% start ct_config server
    try ct_config:start(Mode) of
	_ -> ok
    catch
	_Class:CfgError ->
	    DoExit(CfgError)
    end,

    %% add user event handlers
    _ = case lists:keysearch(event_handler,1,Opts) of
	{value,{_,Handlers}} ->
	    Add = fun({H,Args}) ->
			  case catch gen_event:add_handler(?CT_EVMGR_REF,H,Args) of
			      ok -> ok;
			      {'EXIT',Why} -> DoExit(Why);
			      Other -> DoExit({event_handler,Other})
			  end
		  end,
	    case catch lists:foreach(Add,Handlers) of
		{'EXIT',Reason} ->
		    Parent ! {self(),Reason};
		_ ->
		    ok
	    end;
	false ->
	    ok
    end,

    case ct_default_gl:start_link(group_leader()) of
        {ok, _} -> ok;
        ignore -> ok
    end,

    {StartTime,TestLogDir} = ct_logs:init(Mode, Verbosity, CustomStylesheet),

    ct_event:notify(#event{name=test_start,
			   node=node(),
			   data={StartTime,
				 lists:flatten(TestLogDir)}}),
    %% Initialize ct_hooks
    _ = try ct_hooks:init(Opts) of
	ok ->
	    Parent ! {self(),started};
	{fail,CTHReason} ->
	    ct_logs:tc_print('Suite Callback',CTHReason,[]),
	    self() ! {{stop,{self(),{user_error,CTHReason}}},
		      {Parent,make_ref()}}
    catch
	_:CTHReason:StackTrace ->
	    ErrorInfo = if is_atom(CTHReason) ->
				io_lib:format("{~tp,~tp}",
					      [CTHReason, StackTrace]);
			   true ->
				CTHReason
			end,
	    ct_logs:tc_print('Suite Callback',ErrorInfo,[]),
	    self() ! {{stop,{self(),{user_error,CTHReason}}},
		      {Parent,make_ref()}}
    end,
    loop(Mode, [], StartDir, CustomStylesheet).

create_table(TableName,KeyPos) ->
    create_table(TableName,set,KeyPos).
create_table(TableName,Type,KeyPos) ->
    catch ets:delete(TableName),
    _ = ets:new(TableName,[Type,named_table,public,{keypos,KeyPos}]),
    ok.

read_opts() ->
    case file:consult(ct_run:variables_file_name("./")) of
	{ok,Opts} ->
	    {ok,Opts};
	{error,enoent} ->
	    {error,not_installed};
	Error ->
	    {error,{bad_installation,Error}}
    end.


save_suite_data(Key, Value) ->
    call({save_suite_data, {Key, undefined, Value}}).

save_suite_data(Key, Name, Value) ->
    call({save_suite_data, {Key, Name, Value}}).

save_suite_data_async(Key, Value) ->
    save_suite_data_async(Key, undefined, Value).

save_suite_data_async(Key, Name, Value) ->
    cast({save_suite_data, {Key, Name, Value}}).

read_suite_data(Key) ->
    call({read_suite_data, Key}).

delete_suite_data() ->
    call({delete_suite_data, all}).

delete_suite_data(Key) ->
    call({delete_suite_data, Key}).

match_delete_suite_data(KeyPat) ->
    call({match_delete_suite_data, KeyPat}).

delete_testdata() ->
    call(delete_testdata).

delete_testdata(Key) ->
    call({delete_testdata, Key}).

match_delete_testdata(KeyPat) ->
    call({match_delete_testdata, KeyPat}).

update_testdata(Key, Fun) ->
    update_testdata(Key, Fun, []).

update_testdata(Key, Fun, Opts) ->
    call({update_testdata, Key, Fun, Opts}).

set_testdata(TestData) ->
    call({set_testdata, TestData}).

set_testdata_async(TestData) ->
    cast({set_testdata, TestData}).

get_testdata(Key) ->
    call({get_testdata, Key}).

get_testdata(Key, Timeout) ->
    call({get_testdata, Key}, Timeout).

set_cwd(Dir) ->
    call({set_cwd,Dir}).

reset_cwd() ->
    call(reset_cwd).

get_start_dir() ->
    call(get_start_dir).

%% handle verbosity outside ct_util_server (let the client read
%% the verbosity table) to avoid possible deadlock situations
set_verbosity(Elem = {_Category,_Level}) ->
    try ets:insert(?verbosity_table, Elem) of
	_ ->
	    ok
    catch
	_:Reason ->
	    {error,Reason}
    end.
	
get_verbosity(Category) ->
    try ets:lookup(?verbosity_table, Category) of
	[{Category,Level}] -> 
	    Level;
	_ ->
	    undefined
    catch
	_:Reason ->
	    {error,Reason}
    end.

loop(Mode,TestData,StartDir,CustomStylesheet) ->
    receive 
	{update_last_run_index,From} ->
	    ct_logs:make_last_run_index(),
	    return(From,ok),
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{{save_suite_data,{Key,Name,Value}},From} ->
	    ets:insert(?suite_table, #suite_data{key=Key,
						 name=Name,
						 value=Value}),
	    return(From,ok),
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{{read_suite_data,Key},From} ->
	    case ets:lookup(?suite_table, Key) of
		[#suite_data{key=Key,name=undefined,value=Value}] ->
		    return(From,Value);
		[#suite_data{key=Key,name=Name,value=Value}] ->
		    return(From,{Name,Value});
		_ ->
		    return(From,undefined)
	    end,
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{{delete_suite_data,Key},From} ->
	    if Key == all ->
		    ets:delete_all_objects(?suite_table);
	       true ->
		    ets:delete(?suite_table, Key)
	    end,
	    return(From,ok),
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{{match_delete_suite_data,KeyPat},From} ->
	    ets:match_delete(?suite_table, #suite_data{key=KeyPat,
						       name='_',
						       value='_'}),
	    return(From,ok),
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{delete_testdata,From} ->
	    return(From,ok),
	    loop(From,[],StartDir,CustomStylesheet);	
	{{delete_testdata,Key},From} ->
	    TestData1 = lists:keydelete(Key,1,TestData),
	    return(From,ok),
	    loop(From,TestData1,StartDir,CustomStylesheet);
	{{match_delete_testdata,{Key1,Key2}},From} ->
	    %% handles keys with 2 elements
	    TestData1 =
		lists:filter(fun({Key,_}) when not is_tuple(Key) ->
				     true;
				({Key,_}) when tuple_size(Key) =/= 2 ->
				     true;
				({{_,KeyB},_}) when Key1 == '_' ->
				     KeyB =/= Key2; 
				({{KeyA,_},_}) when Key2 == '_' ->
				     KeyA =/= Key1; 
				(_) when Key1 == '_' ; Key2 == '_' ->
				     false;
				(_) ->
				     true
			     end, TestData),
	    return(From,ok),
	    loop(From,TestData1,StartDir,CustomStylesheet);
	{{set_testdata,New = {Key,_Val}},From} ->
	    TestData1 = lists:keydelete(Key,1,TestData),
	    return(From,ok),
	    loop(Mode,[New|TestData1],StartDir,CustomStylesheet);
	{{get_testdata, all}, From} ->
	    return(From, TestData),
	    loop(From, TestData, StartDir,CustomStylesheet);
	{{get_testdata,Key},From} ->
	    case lists:keysearch(Key,1,TestData) of
		{value,{Key,Val}} ->
		    return(From,Val);
		_ ->
		    return(From,undefined)
	    end,
	    loop(From,TestData,StartDir,CustomStylesheet);
	{{update_testdata,Key,Fun,Opts},From} ->
	    TestData1 =
		case lists:keysearch(Key,1,TestData) of
		    {value,{Key,Val}} ->
			try Fun(Val) of
			    '$delete' ->
				return(From,deleted),
				lists:keydelete(Key,1,TestData);
			    NewVal ->
				return(From,NewVal),
				[{Key,NewVal}|lists:keydelete(Key,1,TestData)]
			catch
			    _:Error ->
				return(From,{error,Error}),
				TestData
			end;
		    _ ->
			case lists:member(create,Opts) of
			    true ->
				InitVal = Fun(undefined),
				return(From,InitVal),
				[{Key,InitVal}|TestData];
			    false ->
				return(From,undefined),
				TestData
			end
		end,
	    loop(From,TestData1,StartDir,CustomStylesheet);
	{{set_cwd,Dir},From} ->
	    return(From,file:set_cwd(Dir)),
	    loop(From,TestData,StartDir,CustomStylesheet);
	{reset_cwd,From} ->
	    return(From,file:set_cwd(StartDir)),
	    loop(From,TestData,StartDir,CustomStylesheet);
	{get_start_dir,From} ->
	    return(From,StartDir),
	    loop(From,TestData,StartDir,CustomStylesheet);
	{{stop,Info},From} ->
	    test_server_io:reset_state(),
	    {MiscIoName,MiscIoDivider,MiscIoFooter} =
		proplists:get_value(misc_io_log,TestData),
	    {ok,MiscIoFd} = file:open(MiscIoName,
				      [append,{encoding,utf8}]),
	    io:put_chars(MiscIoFd, MiscIoDivider),
	    test_server_io:set_fd(unexpected_io, MiscIoFd),

	    Time = calendar:local_time(),
	    ct_event:sync_notify(#event{name=test_done,
					node=node(),
					data=Time}),
	    Callbacks =
		try ets:lookup_element(?suite_table,
				       ct_hooks,
				       #suite_data.value) of
		    CTHMods -> CTHMods
		catch
		    %% this is because ct_util failed in init
		    error:badarg -> []
		end,
	    ct_hooks:terminate(Callbacks),

	    close_connections(ets:tab2list(?conn_table)),
	    ets:delete(?conn_table),
	    ets:delete(?board_table),
	    ets:delete(?suite_table),
	    ets:delete(?verbosity_table),

	    io:put_chars(MiscIoFd, "\n</pre>\n"++MiscIoFooter),
	    test_server_io:stop([unexpected_io]),
	    test_server_io:finish(),

	    ct_logs:close(Info, StartDir, CustomStylesheet),
	    ct_event:stop(),
	    ct_config:stop(),
	    ct_default_gl:stop(),
	    ok = file:set_cwd(StartDir),
	    return(From, Info);
	{Ref, _Msg} when is_reference(Ref) ->
	    %% This clause is used when doing cast operations.
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{get_mode,From} ->
	    return(From,Mode),
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{'EXIT',_Pid,normal} ->
	    loop(Mode,TestData,StartDir,CustomStylesheet);
	{'EXIT',Pid,Reason} ->
	    case ets:lookup(?conn_table,Pid) of
		[#conn{address=A,callback=CB}] ->
		    ErrorStr = io_lib:format("~tp", [Reason]),
		    ErrorHtml = ct_logs:escape_chars(ErrorStr),
		    %% A connection crashed - remove the connection but don't die
		    ct_logs:tc_log_async(ct_error_notify,
					 ?MAX_IMPORTANCE,
					 "CT Error Notification",
					 "Connection process died: "
					 "Pid: ~w, Address: ~tp, "
					 "Callback: ~w\n"
					 "Reason: ~ts\n\n",
					 [Pid,A,CB,ErrorHtml]),
		    catch CB:close(Pid),
		    %% in case CB:close failed to do this:
		    unregister_connection(Pid),
		    loop(Mode,TestData,StartDir,CustomStylesheet);
		_ ->
		    %% Let process crash in case of error, this shouldn't happen!
		    io:format("\n\nct_util_server got EXIT "
			      "from ~w: ~tp\n\n", [Pid,Reason]),
		    ok = file:set_cwd(StartDir),
		    exit(Reason)
	    end
    end.

close_connections([#conn{handle=Handle,callback=CB}|Conns]) ->
    CB:close(Handle),
    close_connections(Conns);
close_connections([]) ->
    ok.

get_key_from_name(Name)->
    ct_config:get_key_from_name(Name).

%%%-----------------------------------------------------------------
%%% -spec register_connection(TargetName,Address,Callback,Handle) ->
%%%                                              ok | {error,Reason}
%%%      TargetName = ct:target_name()
%%%      Address = term()
%%%      Callback = atom()
%%%      Handle = term
%%%
%%% Register a new connection (tool-internal use only).
%%%
%%% This function can be called when a new connection is
%%% established. The connection data is stored in the connection
%%% table, and ct_util will close all registered connections when the
%%% test is finished by calling Callback:close/1.
register_connection(TargetName,Address,Callback,Handle) ->
    %% If TargetName is a registered alias for a config
    %% variable, use it as reference for the connection,
    %% otherwise use the Handle value.
    TargetRef = 
	case ct_config:get_key_from_name(TargetName) of
	    {ok,_Key} ->
		TargetName;
	    _ ->
		%% no config name associated with connection,
		%% use handle for identification instead
		Handle
	end,
    ets:insert(?conn_table,#conn{handle=Handle,
				 targetref=TargetRef,
				 address=Address,
				 callback=Callback}),
    ok.

%%%-----------------------------------------------------------------
%%% -spec unregister_connection(Handle) -> ok
%%%      Handle = term
%%%
%%% Unregister a connection (tool-internal use only).
%%%
%%% This function should be called when a registered connection is
%%% closed. It removes the connection data from the connection
%%% table.
unregister_connection(Handle) ->
    ets:delete(?conn_table,Handle),
    ok.


%%%-----------------------------------------------------------------
%%% -spec does_connection_exist(TargetName,Address,Callback) ->
%%%                                              {ok,Handle} | false
%%%      TargetName = ct:target_name()
%%%      Address = address
%%%      Callback = atom()
%%%      Handle = term()
%%%
%%% Check if a connection already exists.
does_connection_exist(TargetName,Address,Callback) ->
    case ct_config:get_key_from_name(TargetName) of
	{ok,_Key} ->
	    case ets:select(?conn_table,[{#conn{handle='$1',
						targetref=TargetName,
						address=Address,
						callback=Callback},
					  [],
					  ['$1']}]) of
		[Handle] ->
		    {ok,Handle};
		[] ->
		    false
	    end;
	_ ->
	    false
    end.

%%%-----------------------------------------------------------------
%%% -spec get_connection(TargetName,Callback) ->
%%%                                {ok,Connection} | {error,Reason}
%%%      TargetName = ct:target_name()
%%%      Callback = atom()
%%%      Connection = {Handle,Address}
%%%      Handle = term()
%%%      Address = term()
%%%
%%% Return the connection for Callback on the
%%% given target (TargetName).
get_connection(TargetName,Callback) ->
    %% check that TargetName is a registered alias
    case ct_config:get_key_from_name(TargetName) of
	{ok,_Key} ->
	    case ets:select(?conn_table,[{#conn{handle='$1',
						address='$2',
						targetref=TargetName,
						callback=Callback},
					  [],
					  [{{'$1','$2'}}]}]) of
		[Result] ->
		    {ok,Result};
		[] ->
		    {error,no_registered_connection}
	    end;
	Error ->
	    Error
    end.

%%%-----------------------------------------------------------------
%%% -spec get_connections(ConnPid) ->
%%%                                {ok,Connections} | {error,Reason}
%%%      Connections = [Connection]
%%%      Connection = {TargetName,Handle,Callback,Address}
%%%      TargetName = ct:target_name() | undefined
%%%      Handle = term()
%%%      Callback = atom()
%%%      Address = term()
%%%
%%% Get data for all connections associated with a particular
%%% connection pid (see Callback:init/3).
get_connections(ConnPid) ->
    Conns = ets:tab2list(?conn_table),
    lists:flatmap(fun(#conn{targetref=TargetName,
			    handle=Handle,
			    callback=Callback,
			    address=Address}) ->
			  case ct_gen_conn:get_conn_pid(Handle) of
			      ConnPid when is_atom(TargetName) ->
				  [{TargetName,Handle,
				    Callback,Address}];
			      ConnPid ->
				  [{undefined,Handle,
				   Callback,Address}];
			      _ ->
				  []
			  end
		  end, Conns).

%%%-----------------------------------------------------------------
%%% Equivalent to ct:get_target_name/1
get_target_name(Handle) ->
    case ets:select(?conn_table,[{#conn{handle=Handle,targetref='$1',_='_'},
				  [],
				  ['$1']}]) of
	[TargetName] when is_atom(TargetName) ->
	    {ok,TargetName};
	_ ->
	    {error,{unknown_connection,Handle}}
    end.

%%%-----------------------------------------------------------------
%%% -spec close_connections() -> ok
%%%
%%% Close all open connections.
close_connections() ->
    close_connections(ets:tab2list(?conn_table)),
    ok.

%%%-----------------------------------------------------------------
override_silence_all_connections() ->
    Protocols = [telnet,ftp,rpc,snmp,ssh],
    override_silence_connections(Protocols),
    Protocols.

override_silence_connections(Conns) when is_list(Conns) ->
    Conns1 = lists:map(fun({C,B}) -> {C,B};
			  (C)     -> {C,true}
		       end, Conns),
    set_testdata({override_silent_connections,Conns1}).

get_overridden_silenced_connections() ->
    case get_testdata(override_silent_connections) of
	{error,_} ->
	    undefined;
	Conns ->      % list() or undefined
	    Conns
    end.

delete_overridden_silenced_connections() ->    
    delete_testdata(override_silent_connections).

silence_all_connections() ->
    Protocols = [telnet,ftp,rpc,snmp],
    silence_connections(Protocols),
    Protocols.

silence_connections(Conn) when is_tuple(Conn) ->
    silence_connections([Conn]);
silence_connections(Conn) when is_atom(Conn) ->
    silence_connections([{Conn,true}]);
silence_connections(Conns) when is_list(Conns) ->
    Conns1 = lists:map(fun({C,B}) -> {C,B};
			  (C)     -> {C,true}
		       end, Conns),
    set_testdata({silent_connections,Conns1}).

is_silenced(Conn) ->
    is_silenced(Conn, infinity).

is_silenced(Conn, Timeout) ->
    case get_testdata(silent_connections, Timeout) of
	Conns when is_list(Conns) ->
	    case lists:keysearch(Conn,1,Conns) of
		{value,{Conn,true}} ->
		    true;
		_ ->
		    false
	    end;
	Error = {error,_} ->
	    Error;
	_ ->
	    false
    end.
    
reset_silent_connections() ->
    delete_testdata(silent_connections).
    

%%%-----------------------------------------------------------------
%%% -spec stop(Info) -> ok
%%%
%%% Stop the ct_util_server and close all existing connections
%%% (tool-internal use only).
%%%
%%% See ct.
stop(Info) ->
    case whereis(ct_util_server) of
	undefined -> 
	    ok;
	CtUtilPid ->
	    Ref = monitor(process, CtUtilPid),
	    call({stop,Info}),
	    receive
		{'DOWN',Ref,_,_,_} -> ok
	    end
    end.

%%%-----------------------------------------------------------------
%%% -spec update_last_run_index() -> ok
%%%
%%% Update ct_run.<timestamp>/index.html
%%% (tool-internal use only).
update_last_run_index() ->
    call(update_last_run_index).


%%%-----------------------------------------------------------------
%%% -spec get_mode() -> Mode
%%%   Mode = normal | interactive
%%%
%%% Return the current mode of the ct_util_server
%%% (tool-internal use only).
get_mode() ->
    call(get_mode).

%%%-----------------------------------------------------------------
%%% Equivalent to ct:listenv/1
listenv(Telnet) ->
    case ct_telnet:send(Telnet,"listenv") of
	ok ->
	    {ok,Data,_} = ct_telnet:expect(Telnet,
					   ["(^.+)=(.*$)"],
					   [{timeout,seconds(3)},
					    repeat]),
	    {ok,[{Name,Val} || [_,Name,Val] <- Data]};
	{error,Reason} ->
	    {error,{could_not_send_command,Telnet,"listenv",Reason}}
    end.

%%%-----------------------------------------------------------------
%%% Equivalent to ct:parse_table/1
parse_table(Data) ->
    {Heading, Rest} = get_headings(Data),
    Lines = parse_row(Rest,[],tuple_size(Heading)),
    {Heading,Lines}.

get_headings(["|" ++ Headings | Rest]) ->
    {remove_space(string:lexemes(Headings, "|"),[]), Rest};
get_headings([_ | Rest]) ->
    get_headings(Rest);
get_headings([]) ->
    {{},[]}.

parse_row(["|" ++ _ = Row | T], Rows, NumCols) when NumCols > 1 ->
    case string:lexemes(Row, "|") of
	Values when length(Values) =:= NumCols ->
	    parse_row(T,[remove_space(Values,[])|Rows], NumCols);
	Values when length(Values) < NumCols ->
	    parse_row([Row ++"\n"++ hd(T) | tl(T)], Rows, NumCols)
    end;
parse_row(["|" ++ X = Row | T], Rows, 1 = NumCols) ->
    case string:find(X, [$|]) of
	nomatch ->
	    parse_row([Row ++"\n"++hd(T) | tl(T)], Rows, NumCols);
	_Else ->
	    parse_row(T, [remove_space(string:lexemes(Row,"|"),[])|Rows],
		      NumCols)
    end;
parse_row([_Skip | T], Rows, NumCols) ->
    parse_row(T, Rows, NumCols);
parse_row([], Rows, _NumCols) ->
    lists:reverse(Rows).

remove_space([Str|Rest],Acc) ->
    remove_space(Rest,[string:trim(string:trim(Str,both,[$\s]),both,[$'])|Acc]);
remove_space([],Acc) ->
    list_to_tuple(lists:reverse(Acc)).


%%%-----------------------------------------------------------------
is_test_dir(Dir) ->
    lists:last(string:lexemes(filename:basename(Dir), "_")) == "test".

%%%-----------------------------------------------------------------
get_testdir(Dir, all) ->
    Abs = abs_name(Dir),
    case is_test_dir(Abs) of
	true ->
	    Abs;
	false ->
	    AbsTest = filename:join(Abs, "test"),
	    case filelib:is_dir(AbsTest) of
		true -> AbsTest;
		false -> Abs
	    end		    
    end;

get_testdir(Dir, [Suite | _]) when is_atom(Suite) ->
    get_testdir(Dir, atom_to_list(Suite));

get_testdir(Dir, [Suite | _]) when is_list(Suite) ->
    get_testdir(Dir, Suite);

get_testdir(Dir, Suite) when is_atom(Suite) ->
    get_testdir(Dir, atom_to_list(Suite));

get_testdir(Dir, Suite) when is_list(Suite) ->
    Abs = abs_name(Dir),
    case is_test_dir(Abs) of
	true ->
	    Abs;
	false ->
	    AbsTest = filename:join(Abs, "test"),
	    Mod = case filename:extension(Suite) of
		      ".erl" -> Suite;
		      _ -> Suite ++ ".erl"
		  end,    
	    case filelib:is_file(filename:join(AbsTest, Mod)) of
		true -> AbsTest;
		false -> Abs
	    end		    
    end;

get_testdir(Dir, _) ->
    get_testdir(Dir, all).

%%%-----------------------------------------------------------------
get_attached(TCPid) ->
    case dbg_iserver:safe_call({get_attpid,TCPid}) of
	{ok,AttPid} when is_pid(AttPid) ->
	    AttPid;
	_ ->
	    undefined
    end.

%%%-----------------------------------------------------------------
kill_attached(undefined,_AttPid) ->
    ok;
kill_attached(_TCPid,undefined) ->
    ok;
kill_attached(TCPid,AttPid) ->
    case process_info(TCPid) of
	undefined ->
	    exit(AttPid,kill);
	_ ->
	    ok
    end.
	    

%%%-----------------------------------------------------------------
warn_duplicates(Suites) ->
    Warn = 
	fun(Mod) ->
		case catch apply(Mod,sequences,[]) of
		    {'EXIT',_} ->
			ok;
		    [] ->
			ok;
		    _ ->
			io:format(?def_gl,
				  "~nWARNING! Deprecated function: ~w:sequences/0.~n"
				  "         Use group with sequence property instead.~n",[Mod])
		end
	end,
    lists:foreach(Warn, Suites),
    ok.

%%%-----------------------------------------------------------------
mark_process() ->
    mark_process(system).

mark_process(Type) ->
    put(ct_process_type, Type).

is_marked(Pid) ->
    is_marked(Pid, system).

is_marked(Pid, Type) ->
    case process_info(Pid, dictionary) of
        {dictionary,List} ->
            Type == proplists:get_value(ct_process_type, List);
        undefined ->
            false
    end.

remaining_test_procs() ->
    Procs = processes(),
    {SharedGL,OtherGLs,Procs2} =
        lists:foldl(
          fun(Pid, ProcTypes = {Shared,Other,Procs1}) ->
                  case is_marked(Pid, group_leader) of
                      true ->
                          if not is_pid(Shared) ->
                                  case test_server_io:get_gl(true) of
                                      Pid ->
                                          {Pid,Other,
                                           lists:delete(Pid,Procs1)};
                                      _ ->
                                          {Shared,[Pid|Other],Procs1}
                                  end;
                             true ->          % SharedGL already found
                                  {Shared,[Pid|Other],Procs1}
                          end;
                      false ->
                          case is_marked(Pid) of
                              true ->
                                  {Shared,Other,lists:delete(Pid,Procs1)};
                              false ->
                                  ProcTypes
                          end
                  end
          end, {undefined,[],Procs}, Procs),

    AllGLs = [SharedGL | OtherGLs],
    TestProcs =
        lists:flatmap(fun(Pid) ->
                              case process_info(Pid, group_leader) of
                                  {group_leader,GL} ->
                                      case lists:member(GL, AllGLs) of
                                          true  -> [{Pid,GL}];
                                          false -> []
                                      end;
                                  undefined ->
                                      []
                              end
                      end, Procs2),
    {TestProcs, SharedGL, OtherGLs}.

%%%-----------------------------------------------------------------
get_profile_data() ->
    get_profile_data(all).

get_profile_data(KeyOrStartDir) ->
    if is_atom(KeyOrStartDir) ->
	    get_profile_data(KeyOrStartDir, get_start_dir());
       is_list(KeyOrStartDir) ->
	    get_profile_data(all, KeyOrStartDir)
    end.

get_profile_data(Key, StartDir) ->
    Profile = case application:get_env(common_test, profile) of
		  {ok,undefined} -> default;
		  {ok,Prof}      -> Prof;
		  _              -> default
	      end,
    get_profile_data(Profile, Key, StartDir).

get_profile_data(Profile, Key, StartDir) ->
    File = case Profile of
	       default ->
		   ?ct_profile_file;
	       _ when is_list(Profile) ->
		   ?ct_profile_file ++ "." ++ Profile;
	       _ when is_atom(Profile) ->
		   ?ct_profile_file ++ "." ++ atom_to_list(Profile)
	   end,
    FullNameWD = filename:join(StartDir, File),
    {WhichFile,Result} =
	case file:consult(FullNameWD) of
	    {error,enoent} ->
		case init:get_argument(home) of
		    {ok,[[HomeDir]]} ->
			FullNameHome = filename:join(HomeDir, File),
			{FullNameHome,file:consult(FullNameHome)};
		    _ ->
			{File,{error,enoent}}
		end;
	    Consulted ->
		{FullNameWD,Consulted}
	end,
    case Result of
	{error,enoent} when Profile /= default ->
	    io:format(?def_gl, "~nERROR! Missing profile file ~tp~n", [File]),
	    undefined;
	{error,enoent} when Profile == default ->
	    undefined;
	{error,Reason} ->
	    io:format(?def_gl,"~nERROR! Error in profile file ~tp: ~tp~n",
		      [WhichFile,Reason]),
	    undefined;
	{ok,Data} ->
	    Data1 = case Data of
			[List] when is_list(List) ->
			    List;
			_ when is_list(Data) ->
			    Data;
			_ ->
			    io:format(?def_gl,
				      "~nERROR! Invalid profile data in ~tp~n",
				      [WhichFile]),
			    []
		    end,
	    if Key == all ->
		    Data1;
	       true ->
		    proplists:get_value(Key, Data)
	    end
    end.

%%%-----------------------------------------------------------------
%%% Internal functions
call(Msg) ->
    call(Msg, infinity).

call(Msg, Timeout) ->
    case {self(),whereis(ct_util_server)} of
	{_,undefined} ->
	    {error,ct_util_server_not_running};
	{Pid,Pid} ->
	    %% the caller is ct_util_server, which must
	    %% be a mistake
	    {error,bad_invocation};
	{Self,Pid} ->
	    MRef = erlang:monitor(process, Pid),
	    Ref = make_ref(),
	    ct_util_server ! {Msg,{Self,Ref}},
	    receive
		{Ref, Result} -> 
		    erlang:demonitor(MRef, [flush]),
		    Result;
		{'DOWN',MRef,process,_,Reason}  -> 
		    {error,{ct_util_server_down,Reason}}
	    after
		Timeout -> {error,timeout}
	    end
    end.

return({To,Ref},Result) ->
    To ! {Ref, Result},
    ok.

cast(Msg) ->
    ct_util_server ! {Msg, {ct_util_server, make_ref()}},
    ok.

seconds(T) ->
    test_server:seconds(T).

abs_name("/") ->
    "/";
abs_name(Dir0) ->
    Abs = filename:absname(Dir0),
    Dir = case lists:reverse(Abs) of
	      [$/|Rest] -> lists:reverse(Rest);
	      _ -> Abs
	  end,
    abs_name1(Dir,[]).

abs_name1([Drv,$:,$/],Acc) ->
    Split = [[Drv,$:,$/]|Acc],
    abs_name2(Split,[]);
abs_name1("/",Acc) ->
    Split = ["/"|Acc],
    abs_name2(Split,[]);
abs_name1(Dir,Acc) ->
    abs_name1(filename:dirname(Dir),[filename:basename(Dir)|Acc]).

abs_name2([".."|T],[_|Acc]) ->
    abs_name2(T,Acc);
abs_name2(["."|T],Acc) ->
    abs_name2(T,Acc);
abs_name2([H|T],Acc) ->
    abs_name2(T,[H|Acc]);
abs_name2([],Acc) ->
    filename:join(lists:reverse(Acc)).

open_url(iexplore, Args, URL) ->
    {ok,R} = win32reg:open([read]),
    ok = win32reg:change_key(R,"applications\\iexplore.exe\\shell\\open\\command"),
    _ = case win32reg:values(R) of
	{ok, Paths} ->
	    Path = proplists:get_value(default, Paths),
	    [Cmd | _] = string:lexemes(Path, "%"),
	    Cmd1 = Cmd ++ " " ++ Args ++ " " ++ URL,
	    io:format(?def_gl, "~nOpening ~ts with command:~n  ~ts~n", [URL,Cmd1]),
	    open_port({spawn,Cmd1}, []);
	_ ->
	    io:format(?def_gl, "~nNo path to iexplore.exe~n",[])
    end,
    win32reg:close(R),
    ok;

open_url(Prog, Args, URL) ->
    ProgStr = if is_atom(Prog) -> atom_to_list(Prog);
		 is_list(Prog) -> Prog
	      end,
    Cmd = ProgStr ++ " " ++ Args ++ " " ++ URL,
    io:format(?def_gl, "~nOpening ~ts with command:~n  ~ts~n", [URL,Cmd]),
    open_port({spawn,Cmd},[]),
    ok.
