%%
%% %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(reltool_utils).
-moduledoc false.

%% Public
-export([root_dir/0, erl_libs/0, lib_dirs/1,
	 split_app_name/1, prim_consult/1,
	 default_rels/0, choose_default/3,
	 normalize_dir/1,

	 assign_image_list/1, get_latest_resize/1, wait_for_stop_motion/2,
	 mod_conds/0, list_to_mod_cond/1, mod_cond_to_index/1,
	 incl_conds/0, list_to_incl_cond/1, incl_cond_to_index/1, elem_to_index/2,
	 app_dir_test/2, split_app_dir/1,
	 get_item/1, get_items/1, get_selected_items/3,
	 select_items/3, select_item/2,
	 get_column_width/1,

	 safe_keysearch/5, print/4, add_warning/3,

	 create_dir/1, list_dir/1, read_file_info/1,
	 write_file_info/2, read_file/1, write_file/2,
	 recursive_delete/1, delete/2, recursive_copy_file/2, copy_file/2,

	 throw_error/2,

	 decode_regexps/3,
	 default_val/2,
	 escript_foldl/3,

	 call/2, cast/2, reply/3]).

%% For testing
-export([erl_libs/2]).

-include_lib("kernel/include/file.hrl").
-include_lib("wx/include/wx.hrl").
-include("reltool.hrl").

root_dir() ->
    code:root_dir().

erl_libs() ->
    erl_libs(os:getenv("ERL_LIBS", ""), os:type()).

erl_libs(ErlLibs, OsType) when is_list(ErlLibs) ->
  Sep =
    case OsType of
      {win32, _} -> ";";
      _          -> ":"
    end,
  string:lexemes(ErlLibs, Sep).

lib_dirs(Dir) ->
    case erl_prim_loader:list_dir(Dir) of
        {ok, Files} ->
	    [F || F <- Files,
		  filelib:is_dir(filename:join([Dir, F]),
				 erl_prim_loader)];
	error ->
	    []
    end.

%% "asn1-1.6.2" -> {"asn1", "1.6.2"}; "asn1" -> {"asn1", ""}
split_app_name(Name) ->
    Pred =
	fun(Elem) ->
		if
		    Elem =:= $\. -> true;
                    Elem >= $0, Elem =< $9 -> true;
                    true -> false
                end
        end,
    case lists:splitwith(Pred, lists:reverse(Name)) of
	{Vsn, [$- | App]} ->
	    {list_to_atom(lists:reverse(App)), lists:reverse(Vsn)};
	_ ->
	    {list_to_atom(Name), ""}
    end.


normalize_dir(RelDir) ->
    Tokens = filename:split(filename:absname(RelDir)),
    filename:join(lists:reverse(normalize_dir(Tokens, []))).

normalize_dir([".."|Dirs], [_Dir|Path]) ->
    normalize_dir(Dirs, Path);
normalize_dir(["."|Dirs], Path) ->
    normalize_dir(Dirs, Path);
normalize_dir([Dir|Dirs], Path) ->
    normalize_dir(Dirs, [Dir|Path]);
normalize_dir([], Path) ->
    Path.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

prim_consult(Bin) when is_binary(Bin) ->
    case erl_scan:string(unicode:characters_to_list(Bin,encoding(Bin))) of
	{ok, Tokens, _EndLine} ->
	    prim_parse(Tokens, []);
	{error, {_ErrorLine, Module, Reason}, _EndLine} ->
	    {error, Module:format_error(Reason)}
    end;
prim_consult(FullName) when is_list(FullName) ->
    case erl_prim_loader:read_file(FullName) of
        {ok, Bin} ->
	    prim_consult(Bin);
        error ->
            {error, file:format_error(enoent)}
    end.

encoding(Bin) when is_binary(Bin) ->
    case epp:read_encoding_from_binary(Bin) of
	none ->
	    epp:default_encoding();
	E ->
	    E
    end.

prim_parse(Tokens, Acc) ->
    case lists:splitwith(fun(T) -> element(1,T) =/= dot end, Tokens) of
        {[], []} ->
            {ok, lists:reverse(Acc)};
        {Tokens2, [{dot,_} = Dot | Rest]} ->
            case erl_parse:parse_term(Tokens2 ++ [Dot]) of
                {ok, Term} ->
                    prim_parse(Rest, [Term | Acc]);
		{error, {_ErrorLine, Module, Reason}} ->
		    {error, Module:format_error(Reason)}
            end;
        {Tokens2, []} ->
            case erl_parse:parse_term(Tokens2) of
                {ok, Term} ->
                    {ok, lists:reverse([Term | Acc])};
		{error, {_ErrorLine, Module, Reason}} ->
		    {error, Module:format_error(Reason)}
            end
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

default_rels() ->
    %% kernel and stdlib are added automatically in every release
    [
     #rel{name = ?DEFAULT_REL_NAME,
	  vsn = "1.0",
	  rel_apps = []},
     #rel{name = "start_sasl",
	  vsn = "1.0",
          rel_apps = [#rel_app{name = sasl}]},
     #rel{name = "no_dot_erlang", %% Needed by escript and erlc
          vsn = "1.0",
          rel_apps = [],
          load_dot_erlang = false
         }
    ].

choose_default(Tag, Profile, InclDefs)
  when Profile =:= ?DEFAULT_PROFILE; InclDefs ->
    case Tag of
	incl_sys_filters  -> ?DEFAULT_INCL_SYS_FILTERS;
	excl_sys_filters  -> ?DEFAULT_EXCL_SYS_FILTERS;
	incl_app_filters  -> ?DEFAULT_INCL_APP_FILTERS;
	excl_app_filters  -> ?DEFAULT_EXCL_APP_FILTERS;
	embedded_app_type -> ?DEFAULT_EMBEDDED_APP_TYPE
    end;
choose_default(Tag, standalone, _InclDefs) ->
    case Tag of
	incl_sys_filters  -> ?STANDALONE_INCL_SYS_FILTERS;
	excl_sys_filters  -> ?STANDALONE_EXCL_SYS_FILTERS;
	incl_app_filters  -> ?STANDALONE_INCL_APP_FILTERS;
	excl_app_filters  -> ?STANDALONE_EXCL_APP_FILTERS;
	embedded_app_type -> ?DEFAULT_EMBEDDED_APP_TYPE
    end;
choose_default(Tag, embedded, _InclDefs) ->
    case Tag of
	incl_sys_filters  -> ?EMBEDDED_INCL_SYS_FILTERS;
	excl_sys_filters  -> ?EMBEDDED_EXCL_SYS_FILTERS;
	incl_app_filters  -> ?EMBEDDED_INCL_APP_FILTERS;
	excl_app_filters  -> ?EMBEDDED_EXCL_APP_FILTERS;
	embedded_app_type -> ?EMBEDDED_APP_TYPE
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

assign_image_list(ListCtrl) ->
    Art = wxImageList:new(16,16),
    [wxImageList:add(Art, wxArtProvider:getBitmap(Image, [{size, {16,16}}]))
     || Image <- ["wxART_ERROR",
		  "wxART_WARNING",
                  "wxART_QUESTION",
                  "wxART_TICK_MARK",
		  "wxART_CROSS_MARK",
		  "wxART_GO_HOME"]],
    wxListCtrl:assignImageList(ListCtrl, Art, ?wxIMAGE_LIST_SMALL).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

get_latest_resize(#wx{obj = ObjRef, event = #wxSize{}} = Wx) ->
    receive
	#wx{obj = ObjRef, event = #wxSize{}} = Wx2 ->
	    get_latest_resize(Wx2)
    after 10 ->
	    Wx
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

wait_for_stop_motion(ObjRef, {_,_}=Pos) ->
    receive
	#wx{obj = ObjRef, event = #wxMouse{type = motion, x=X, y=Y}} ->
	    wait_for_stop_motion(ObjRef, {X,Y})
    after 100 ->
	    Pos
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

mod_conds() ->
    ["all (ebin + app file)", "ebin + derived", "app file + derived", "derived", "none"].

list_to_mod_cond(List) ->
    case List of
	"all" ++ _   -> all;
 	"ebin" ++ _  -> ebin;
	"app" ++ _   -> app;
	"derived"    -> derived;
	"none"       -> none
    end.

mod_cond_to_index(ModCond) ->
    case ModCond of
	all       -> 0;
	ebin      -> 1;	
	app       -> 2;
	derived   -> 3;
	undefined -> 3;
	none      -> 4
    end.

incl_conds() ->
    ["include", "exclude", "derived"].

list_to_incl_cond(List) ->
    case List of
	"include" -> include;
 	"exclude" -> exclude;
	"derived" -> derived
    end.

incl_cond_to_index(ModCond) ->
    case ModCond of
	include -> 0;
	exclude -> 1;	
	derived -> 2
    end.

elem_to_index(Elem, List) ->
    elem_to_index(Elem, List, 1).

elem_to_index(Elem, [H | T], Index) ->
    case Elem =:= H of
	true -> Index;
	false -> elem_to_index(Elem, T, Index + 1)
    end;
elem_to_index(Elem, [], _) ->
    erlang:error({not_found, Elem}).

app_dir_test(Dir1, Dir2) ->
    {Name1, Vsn1, Parent1} = split_app_dir(Dir1),
    {Name2, Vsn2, Parent2} = split_app_dir(Dir2),
    if
	Name1 < Name2 -> true;
	Name1 > Name2 -> false;
	Vsn1 < Vsn2 -> false;
	Vsn1 > Vsn2 -> true;
	Parent1 =< Parent2 -> true;
	true -> false
    end.

split_app_dir(Dir) ->
    ParentDir = filename:dirname(Dir),
    Base = filename:basename(Dir),
    {Name, Vsn} = split_app_name(Base),
    Vsn2 =
	try
	    [list_to_integer(N) || N <- string:lexemes(Vsn, ".")]
	catch
	    _:_ ->
		Vsn
	end,
    {Name, Vsn2, ParentDir}.

get_item(ListCtrl) ->
    case wxListCtrl:getItemCount(ListCtrl) of
	0 ->
	    undefined;
	_ ->
	    case wxListCtrl:getNextItem(ListCtrl,
					-1,
					[{geometry, ?wxLIST_NEXT_ALL},
					 {state, ?wxLIST_STATE_SELECTED}]) of
		-1 ->
		    ItemNo = wxListCtrl:getTopItem(ListCtrl),
		    case wxListCtrl:getItemText(ListCtrl, ItemNo) of
			"" ->
			    undefined;
			Text ->
			    {ItemNo, Text}
		    end;
		ItemNo ->
		    Text = wxListCtrl:getItemText(ListCtrl, ItemNo),
		    {ItemNo, Text}
	    end
    end.

get_items(ListCtrl) ->
    case wxListCtrl:getItemCount(ListCtrl) of
	0 ->
	    [];
	Count ->
	    case get_selected_items(ListCtrl, -1, []) of
		[] ->
		    ItemNo = wxListCtrl:getTopItem(ListCtrl),
		    case wxListCtrl:getItemText(ListCtrl, ItemNo) of
			"" ->
			    [];
			Text when Text =/= ?MISSING_APP_TEXT ->
			    [{ItemNo, Text}];
			_MissingText when Count > 1 ->
			    case wxListCtrl:getItemText(ListCtrl, ItemNo + 1) of
				"" ->
				    [];
				Text ->
				    [{ItemNo, Text}]
			    end;
			_MissingText ->
			    []
		    end;
		Items ->
		    Items
	    end
    end.

get_selected_items(ListCtrl, PrevItem, Acc) ->
    case wxListCtrl:getNextItem(ListCtrl,
                                PrevItem,
                                [{geometry, ?wxLIST_NEXT_ALL},
                                 {state, ?wxLIST_STATE_SELECTED}]) of
        -1 ->
	    Acc;
        ItemNo ->
	    case wxListCtrl:getItemText(ListCtrl, ItemNo) of
		Text when Text =/= ?MISSING_APP_TEXT ->
		    get_selected_items(ListCtrl,
				       ItemNo,
				       [{ItemNo, Text} | Acc]);
		_Text ->
		    get_selected_items(ListCtrl, ItemNo, Acc)
	    end
    end.

select_items(_ListCtrl, _OldItems, []) ->
    %% No new items. Nothing to select.
    false;
select_items(ListCtrl, [], Items) ->
    %% No old selection. Select first.
    select_item(ListCtrl, Items);
select_items(ListCtrl, _OldItems, [Item]) ->
    %% Only one new item. Select it.
    select_item(ListCtrl, [Item]);
select_items(ListCtrl, OldItems, NewItems) ->
    %% Try to propagate old selection to new items.
    Filter =
	fun({_OldItemNo, Text}) ->
		case lists:keysearch(Text, 2, NewItems) of
		    {value, Item} -> {true, Item};
		    false -> false
		end
	end,
    case lists:zf(Filter, OldItems) of
	[] ->
	    %% None of the old selections are valid. Select the first.
	    select_item(ListCtrl, NewItems);
	ValidItems ->
	    %% Some old selections are still valid. Select them again.
	    lists:foreach(fun(Item) -> select_item(ListCtrl, [Item]) end,
			  ValidItems)
    end.

select_item(ListCtrl, [{ItemNo, Text} | Items]) ->
    case Text =:= ?MISSING_APP_TEXT of
	true ->
	    select_item(ListCtrl, Items);
	false ->
	    StateMask = ?wxLIST_STATE_SELECTED,
	    State = wxListCtrl:getItemState(ListCtrl, ItemNo, StateMask),
	    State2 = State bor ?wxLIST_STATE_SELECTED,
	    wxListCtrl:setItemState(ListCtrl, ItemNo, State2, StateMask),
	    wxListCtrl:refreshItem(ListCtrl, ItemNo)
    end;
select_item(_ListCtrl, []) ->
    ok.

get_column_width(ListCtrl) ->
    wx:batch(fun() ->
		     {Total, _} = wxWindow:getClientSize(ListCtrl),
		     Total - scroll_size(ListCtrl)
	     end).

scroll_size(ObjRef) ->
    case os:type() of
	{win32, nt} -> 0;
	{unix, darwin} ->
	    %% I can't figure out is there is a visible scrollbar
	    %% Always make room for it
	    wxSystemSettings:getMetric(?wxSYS_VSCROLL_X);
	_ ->
	    case wxWindow:hasScrollbar(ObjRef, ?wxVERTICAL) of
		true -> wxSystemSettings:getMetric(?wxSYS_VSCROLL_X);
		false -> 0
	    end
    end.

safe_keysearch(Key, Pos, List, Mod, Line) ->
    case lists:keysearch(Key, Pos, List) of
        false ->
            io:format("~w(~w): lists:keysearch(~tp, ~w, ~tp) -> false\n",
                      [Mod, Line, Key, Pos, List]),
            erlang:error({Mod, Line, lists, keysearch, [Key, Pos, List]});
        {value, Val} ->
            Val
    end.

print(X, X, Format, Args) ->
    io:format(Format, Args);
print(_, _, _, _) ->
    ok.

add_warning(Format, Args, {ok,Warnings}) ->
    Warning = lists:flatten(io_lib:format(Format,Args)),
    case lists:member(Warning,Warnings) of
	true ->
	    {ok,Warnings};
	false ->
	    {ok,[Warning|Warnings]}
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

create_dir(Dir) ->
    filelib:ensure_dir(Dir),
    case file:make_dir(Dir) of
        ok ->
            ok;
        {error, eexist} ->
            ok;
        {error, Reason} ->
            Text = file:format_error(Reason),
            throw_error("create dir ~ts: ~ts", [Dir, Text])
    end.

list_dir(Dir) ->
    case erl_prim_loader:list_dir(Dir) of
        {ok, Files} ->
	    Files;
        error ->
            Text = file:format_error(enoent),
            throw_error("list dir ~ts: ~ts", [Dir, Text])
    end.

read_file_info(File) ->
    case file:read_file_info(File) of
        {ok, Info} ->
	    Info;
        {error, Reason} ->
            Text = file:format_error(Reason),
            throw_error("read file info ~ts: ~ts", [File, Text])
    end.

write_file_info(File, Info) ->
    case file:write_file_info(File, Info) of
        ok ->
	    ok;
        {error, Reason} ->
            Text = file:format_error(Reason),
            throw_error("write file info ~ts: ~ts", [File, Text])
    end.

read_file(File) ->
    case file:read_file(File) of
        {ok, Bin} ->
	    Bin;
        {error, Reason} ->
            Text = file:format_error(Reason),
            throw_error("read file ~ts: ~ts", [File, Text])
    end.

write_file(File, Bin) ->
    case file:write_file(File, Bin) of
        ok ->
	    ok;
        {error, Reason} ->
            Text = file:format_error(Reason),
            throw_error("write file ~ts: ~ts", [File, Text])
    end.

recursive_delete(Dir) ->
    case filelib:is_dir(Dir) of
	true ->
	    case file:list_dir(Dir) of
		{ok, Files} ->
		    Fun =
			fun(F) -> recursive_delete(filename:join([Dir, F])) end,
		    lists:foreach(Fun, Files),
		    delete(Dir, directory);
		{error, enoent} ->
		    ok;
		{error, Reason} ->
		    Text = file:format_error(Reason),
		    throw_error("delete file ~ts: ~ts\n", [Dir, Text])
	    end;
	false ->
            delete(Dir, regular)
    end.

delete(File, Type) ->
    case do_delete(File, Type) of
        ok ->
            ok;
        {error, enoent} ->
            ok;
        {error, Reason} ->
            Text = file:format_error(Reason),
            throw_error("delete file ~ts: ~ts\n", [File, Text])
    end.

do_delete(File, regular) ->
    file:delete(File);
do_delete(Dir, directory) ->
    file:del_dir(Dir).

recursive_copy_file(From, To) ->
    case erl_prim_loader:list_dir(From) of
        {ok, Files} ->
	    %% Copy all files in the directory
            create_dir(To),
            Copy =
                fun(F) ->
                        recursive_copy_file(filename:join([From, F]),
					    filename:join([To, F]))
                end,
            lists:foreach(Copy, Files);
        error ->
            %% Copy single file
	    copy_file(From, To)
    end.

copy_file(From, To) ->
    case erl_prim_loader:read_file(From) of
	{ok, Bin} ->
	    case file:write_file(To, Bin) of
		ok ->
		    FromInfo = read_file_info(From),
		    ToInfo = read_file_info(To),
		    FromMode = FromInfo#file_info.mode,
		    ToMode = ToInfo#file_info.mode,
		    ToMode2 = FromMode bor ToMode,
		    FileInfo = ToInfo#file_info{mode = ToMode2},
		    write_file_info(To, FileInfo),
		    ok;
		{error, Reason} ->
		    Text = file:format_error(Reason),
		    throw_error("copy file ~ts -> ~ts: ~ts\n", [From, To, Text])
	    end;
	error ->
	    Text = file:format_error(enoent),
	    throw_error("copy file ~ts -> ~ts: ~ts\n", [From, To, Text])
    end.

throw_error(Format, Args) ->
    throw({error, lists:flatten(io_lib:format(Format, Args))}).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

decode_regexps(Key, Regexps, undefined) ->
    decode_regexps(Key, Regexps, []);
decode_regexps(Key, {add, Regexps}, Old) when is_list(Regexps) ->
    do_decode_regexps(Key, Regexps, Old);
decode_regexps(_Key, {del, Regexps}, Old)  when is_list(Regexps) ->
    [Re || Re <- Old, not lists:member(Re#regexp.source, Regexps)];
decode_regexps(Key, Regexps, _Old) when is_list(Regexps) ->
    do_decode_regexps(Key, Regexps, []).

do_decode_regexps(Key, [Regexp | Regexps], Acc) ->
    case catch re:compile(Regexp, [unicode]) of
        {ok, MP} ->
            do_decode_regexps(Key,
			      Regexps,
			      [#regexp{source = Regexp, compiled = MP} | Acc]);
        _ ->
            Text = lists:flatten(io_lib:format("~tp", [{Key, Regexp}])),
            throw({error, "Illegal option: " ++ Text})
    end;
do_decode_regexps(_Key, [], Acc) ->
    lists:sort(Acc).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

default_val(Val, Default) ->
    case Val of
        undefined -> Default;
        _         -> Val
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

escript_foldl(Fun, Acc, File) ->
    case escript:extract(File, [compile_source]) of
	{ok, [_Shebang, _Comment, _EmuArgs, Body]} ->
	    case Body of
		{source, BeamCode} ->
		    GetInfo = fun() -> file:read_file_info(File) end,
		    GetBin = fun() -> BeamCode end,
		    {ok, Fun(".", GetInfo, GetBin, Acc)};
		{beam, BeamCode} ->
		    GetInfo = fun() -> file:read_file_info(File) end,
		    GetBin = fun() -> BeamCode end,
		    {ok, Fun(".", GetInfo, GetBin, Acc)};
		{archive, ArchiveBin} ->
		    zip:foldl(Fun, Acc, {File, ArchiveBin})
	    end;
	{error, Reason} ->
	    {error, Reason}
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

call(Name, Msg) when is_atom(Name) ->
    case whereis(Name) of
	undefined ->
	    {error, {noproc, Name}};
	Pid ->
	    call(Pid, Msg)
    end;
call(Pid, Msg) when is_pid(Pid) ->
    Ref = erlang:monitor(process, Pid),
    Pid ! {call, self(), Ref, Msg},
    receive
        {Ref, Reply} ->
            Reply;
        {'EXIT', Pid, Reason} ->
            erlang:demonitor(Ref, [flush]),
            {error, Reason};
	{'DOWN', Ref, _, _, Reason} ->
            {error, Reason}
    end.

cast(Pid, Msg) ->
    Pid ! {cast, self(), Msg},
    ok.

reply(Pid, Ref, Msg) ->
    Pid ! {Ref, Msg}.
