%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2013-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(cdv_wx).
-moduledoc false.

-behaviour(wx_object).

-export([start/1]).
-export([get_attrib/1, set_status/1, create_txt_dialog/4]).

-export([init/1, handle_event/2, handle_cast/2, terminate/2, code_change/3,
	 handle_call/3, handle_info/2, check_page_title/1]).

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

-include("observer_defs.hrl").

%% Defines

-define(SERVER, cdv_wx).

-define(ID_UG, 1).
-define(ID_HOWTO, 2).
-define(ID_NOTEBOOK, 3).

-define(GEN_STR,   "General").
-define(PRO_STR,   "Processes").
-define(PORT_STR,  "Ports").
-define(ETS_STR,   "ETS Tables").
-define(TIMER_STR, "Timers").
-define(SCHEDULER_STR, "Schedulers").
-define(FUN_STR,   "Funs").
-define(ATOM_STR,  "Atoms").
-define(DIST_STR,  "Nodes").
-define(MOD_STR,   "Modules").
-define(MEM_STR,   "Memory").
-define(PERSISTENT_STR, "Persistent Terms").
-define(INT_STR,   "Internal Tables").

%% Records
-record(state,
	{server,
	 file,
	 frame,
	 menubar,
	 menus = [],
	 status_bar,
	 notebook,
	 main_panel,
	 gen_panel,
	 pro_panel,
	 port_panel,
	 ets_panel,
	 timer_panel,
	 sched_panel,
	 fun_panel,
	 atom_panel,
	 dist_panel,
	 mod_panel,
	 mem_panel,
         persistent_panel,
	 int_panel,
	 active_tab
	}).

start(File) ->
    case wx_object:start(?MODULE, File, []) of
	Err = {error, _} -> Err;
	_Obj -> ok
    end.

get_attrib(What) ->
    wx_object:call(?SERVER, {get_attrib, What}).

set_status(What) ->
    wx_object:cast(?SERVER, {status_bar, What}).

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

init(File0) ->
    register(?SERVER, self()),
    wx:new(),

    {ok,CdvServer} = crashdump_viewer:start_link(),

    catch wxSystemOptions:setOption("mac.listctrl.always_use_generic", 1),
    Scale = observer_wx:get_scale(),
    Frame = wxFrame:new(wx:null(), ?wxID_ANY, "Crashdump Viewer",
			[{size, {Scale*850, Scale*600}}, {style, ?wxDEFAULT_FRAME_STYLE}]),
    IconFile = filename:join(code:priv_dir(observer), "erlang_observer.png"),
    Icon = wxIcon:new(IconFile, [{type,?wxBITMAP_TYPE_PNG}]),
    wxFrame:setIcon(Frame, Icon),
    wxIcon:destroy(Icon),

    %% Setup panels
    Panel = wxPanel:new(Frame, []),
    Notebook = wxNotebook:new(Panel, ?ID_NOTEBOOK, [{style, ?wxBK_DEFAULT}]),

    %% Setup "statusbar" to show warnings
    StatusBar = observer_lib:create_status_bar(Panel),

    %% Setup sizer create early to get it when window shows
    MainSizer = wxBoxSizer:new(?wxVERTICAL),

    wxSizer:add(MainSizer, Notebook, [{proportion, 1}, {flag, ?wxEXPAND}]),
    wxSizer:add(MainSizer, StatusBar, [{flag, ?wxEXPAND bor ?wxALL},
				       {proportion, 0},
				       {border,4}]),
    wxPanel:setSizer(Panel, MainSizer),

    wxNotebook:connect(Notebook, command_notebook_page_changing),
    wxFrame:connect(Frame, close_window, [{skip, true}]),
    wxMenu:connect(Frame, command_menu_selected),

    case load_dump(Frame,File0) of
	{ok,File} ->
	    %% Set window title
	    T1 = "Crashdump Viewer: ",
            FileLength = string:length(File),
	    Title =
		if FileLength > 70 ->
			T1 ++ filename:basename(File);
		   true ->
			T1 ++ File
		end,
	    wxFrame:setTitle(Frame, Title),

	    setup(#state{server=CdvServer,
			 file=File,
			 frame=Frame,
			 status_bar=StatusBar,
			 notebook=Notebook,
			 main_panel=Panel});
	error ->
	    wxFrame:destroy(Frame),
	    wx:destroy(),
	    crashdump_viewer:stop(),
	    ignore
    end.

setup(#state{frame=Frame, notebook=Notebook}=State) ->

    %% Setup Menubar & Menus
    MenuBar = wxMenuBar:new(),
    DefMenus = default_menus(),
    observer_lib:create_menus(DefMenus, MenuBar, default),
    wxFrame:setMenuBar(Frame, MenuBar),

    %% General information Panel
    GenPanel = add_page(Notebook, ?GEN_STR, cdv_info_wx, cdv_gen_cb),

    %% Process Panel
    ProPanel = add_page(Notebook, ?PRO_STR, cdv_virtual_list_wx, cdv_proc_cb),

    %% Port Panel
    PortPanel = add_page(Notebook, ?PORT_STR, cdv_virtual_list_wx, cdv_port_cb),

    %% Table Panel
    EtsPanel = add_page(Notebook, ?ETS_STR, cdv_virtual_list_wx, cdv_ets_cb),

    %% Timer Panel
    TimerPanel = add_page(Notebook, ?TIMER_STR, cdv_virtual_list_wx,cdv_timer_cb),

    %% Scheduler Panel
    SchedPanel = add_page(Notebook, ?SCHEDULER_STR, cdv_virtual_list_wx, cdv_sched_cb),

    %% Fun Panel
    FunPanel = add_page(Notebook, ?FUN_STR, cdv_virtual_list_wx, cdv_fun_cb),

    %% Atom Panel
    AtomPanel = add_page(Notebook, ?ATOM_STR, cdv_virtual_list_wx, cdv_atom_cb),

    %% Distribution Panel
    DistPanel = add_page(Notebook, ?DIST_STR, cdv_virtual_list_wx, cdv_dist_cb),

    %% Loaded Modules Panel
    ModPanel = add_page(Notebook, ?MOD_STR, cdv_virtual_list_wx, cdv_mod_cb),

    %% Memory Panel
    MemPanel = add_page(Notebook, ?MEM_STR, cdv_multi_wx, cdv_mem_cb),

    %% Persistent Terms Panel
    PersistentPanel = add_page(Notebook, ?PERSISTENT_STR, cdv_html_wx, cdv_persistent_cb),

    %% Memory Panel
    IntPanel = add_page(Notebook, ?INT_STR, cdv_multi_wx, cdv_int_tab_cb),

    %% Show the window
    wxFrame:show(Frame),

    GenPid = wx_object:get_pid(GenPanel),
    GenPid ! active,
    observer_lib:destroy_progress_dialog(),
    process_flag(trap_exit, true),
    {Frame, State#state{menubar = MenuBar,
			gen_panel = GenPanel,
			pro_panel = ProPanel,
			port_panel = PortPanel,
			ets_panel = EtsPanel,
			timer_panel = TimerPanel,
			sched_panel = SchedPanel,
			fun_panel = FunPanel,
			atom_panel = AtomPanel,
			dist_panel = DistPanel,
			mod_panel = ModPanel,
			mem_panel = MemPanel,
                        persistent_panel = PersistentPanel,
			int_panel = IntPanel,
			active_tab = GenPid
		       }}.

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

%%Callbacks
handle_event(#wx{event=#wxBookCtrl{type=command_notebook_page_changing}},
	     #state{active_tab=Previous} = State) ->
    case get_active_pid(State) of
	Previous -> {noreply, State};
	Pid ->
	    Pid ! active,
	    {noreply, State#state{active_tab=Pid}}
    end;

handle_event(#wx{event = #wxClose{}}, State) ->
    {stop, normal, State};

handle_event(#wx{id = ?wxID_OPEN,
		 event = #wxCommand{type = command_menu_selected}},
	     State) ->
    NewState =
	case load_dump(State#state.frame,undefined) of
	    {ok,File} ->
		Panels = [State#state.gen_panel,
			  State#state.pro_panel,
			  State#state.port_panel,
			  State#state.ets_panel,
			  State#state.timer_panel,
			  State#state.fun_panel,
			  State#state.atom_panel,
			  State#state.dist_panel,
			  State#state.mod_panel,
			  State#state.mem_panel,
			  State#state.persistent_panel,
			  State#state.int_panel],
		_ = [wx_object:call(Panel,new_dump) || Panel<-Panels],
		wxNotebook:setSelection(State#state.notebook,0),
		observer_lib:destroy_progress_dialog(),
		State#state{file=File};
	    error ->
		State
    end,
    {noreply,NewState};

handle_event(#wx{id = ?wxID_EXIT,
		 event = #wxCommand{type = command_menu_selected}},
	     State) ->
    {stop, normal, State};

handle_event(#wx{id = HelpId,
		 event = #wxCommand{type = command_menu_selected}},
	     State) when HelpId==?wxID_HELP; HelpId==?ID_UG; HelpId==?ID_HOWTO ->
    Help = get_help_doc(HelpId),
    wx_misc:launchDefaultBrowser(Help) orelse
	create_txt_dialog(State#state.frame,
			  "Could not launch browser: ~n " ++ Help,
			  "Error", ?wxICON_ERROR),
    {noreply, State};

handle_event(#wx{id = ?wxID_ABOUT,
		 event = #wxCommand{type = command_menu_selected}},
	     State = #state{frame=Frame}) ->
    AboutString = "Display information from an erlang crash dump",
    Style = [{style, ?wxOK bor ?wxSTAY_ON_TOP},
	     {caption, "About"}],
    wxMessageDialog:showModal(wxMessageDialog:new(Frame, AboutString, Style)),
    {noreply, State};

handle_event(Event, State) ->
    Pid = get_active_pid(State),
    Pid ! Event,
    {noreply, State}.

handle_cast({status_bar, Msg}, State=#state{status_bar=SB}) ->
    wxTextCtrl:clear(SB),
    wxTextCtrl:writeText(SB, Msg),
    {noreply, State};

handle_cast(_Cast, State) ->
    {noreply, State}.

handle_call({get_attrib, Attrib}, _From, State) ->
    {reply, get(Attrib), State};

handle_call(_Msg, _From, State) ->
    {reply, ok, State}.

handle_info({'EXIT', Pid, normal}, #state{server=Pid}=State) ->
    {stop, normal, State};

handle_info({'EXIT', Pid, _Reason}, State) ->
    io:format("Child (~s) crashed exiting:  ~p ~tp~n",
	      [pid2panel(Pid, State), Pid,_Reason]),
    {stop, normal, State};

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, #state{frame = Frame}) ->
    wxFrame:destroy(Frame),
    wx:destroy(),
    crashdump_viewer:stop(),
    ok.

code_change(_, _, State) ->
    {ok, State}.

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

add_page(Notebook,Title,Callback,Extra) ->
    Panel = Callback:start_link(Notebook, Extra),
    wxNotebook:addPage(Notebook, Panel, Title, []),
    Panel.

create_txt_dialog(Frame, Msg, Title, Style) ->
    MD = wxMessageDialog:new(Frame, Msg, [{style, Style}]),
    wxMessageDialog:setTitle(MD, Title),
    wxDialog:showModal(MD),
    wxDialog:destroy(MD).

check_page_title(Notebook) ->
    Selection = wxNotebook:getSelection(Notebook),
    wxNotebook:getPageText(Notebook, Selection).

get_active_pid(#state{notebook=Notebook, gen_panel=Gen, pro_panel=Pro,
		      port_panel=Ports, ets_panel=Ets, timer_panel=Timers,
		      fun_panel=Funs, atom_panel=Atoms, dist_panel=Dist,
		      mod_panel=Mods, mem_panel=Mem, persistent_panel=Persistent,
                      int_panel=Int, sched_panel=Sched
		     }) ->
    Panel = case check_page_title(Notebook) of
		?GEN_STR -> Gen;
		?PRO_STR -> Pro;
		?PORT_STR -> Ports;
		?ETS_STR -> Ets;
		?TIMER_STR -> Timers;
		?SCHEDULER_STR -> Sched;
		?FUN_STR -> Funs;
		?ATOM_STR -> Atoms;
		?DIST_STR -> Dist;
		?MOD_STR -> Mods;
		?MEM_STR -> Mem;
		?PERSISTENT_STR -> Persistent;
		?INT_STR -> Int
	    end,
    wx_object:get_pid(Panel).

pid2panel(Pid, #state{gen_panel=Gen, pro_panel=Pro, port_panel=Ports,
		      ets_panel=Ets, timer_panel=Timers, fun_panel=Funs,
		      atom_panel=Atoms, dist_panel=Dist, mod_panel=Mods,
		      mem_panel=Mem, persistent_panel=Persistent, int_panel=Int}) ->
    case Pid of
	Gen -> ?GEN_STR;
	Pro -> ?PRO_STR;
	Ports -> ?PORT_STR;
	Ets -> ?ETS_STR;
	Timers -> ?TIMER_STR;
	Funs -> ?FUN_STR;
	Atoms -> ?ATOM_STR;
	Dist -> ?DIST_STR;
	Mods -> ?MOD_STR;
	Mem -> ?MEM_STR;
        ?PERSISTENT_STR -> Persistent;
	Int -> ?INT_STR;
	_ -> "unknown"
    end.

default_menus() ->
    Open  = #create_menu{id = ?wxID_OPEN, text = "Open new crash dump"},
    Quit  = #create_menu{id = ?wxID_EXIT, text = "Quit"},
    About = #create_menu{id = ?wxID_ABOUT, text = "About"},
    Help  = #create_menu{id = ?wxID_HELP},
    UG    = #create_menu{id = ?ID_UG, text = "Crashdump viewer user's guide"},
    Howto = #create_menu{id = ?ID_HOWTO, text = "How to interpret crash dump"},
    case os:type() =:= {unix, darwin} of
	false ->
	    FileMenu = {"File", [Open,Quit]},
	    HelpMenu = {"Help", [About,Help,UG,Howto]},
	    [FileMenu, HelpMenu];
	true ->
	    %% On Mac quit and about will be moved to the "default' place
	    %% automagicly, so just add them to a menu that always exist.
	    [{"File", [Open, About,Quit]}, {"&Help", [Help,UG,Howto]}] 
    end.


load_dump(Frame,undefined) ->
    FD  =  wxFileDialog:new(wx:null(),
			    [{style,?wxFD_OPEN bor ?wxFD_FILE_MUST_EXIST}]),
    case wxFileDialog:showModal(FD) of
	?wxID_OK ->
	    Path = wxFileDialog:getPath(FD),
	    wxDialog:destroy(FD),
	    load_dump(Frame,Path);
	_ ->
	    wxDialog:destroy(FD),
	    error
    end;
load_dump(Frame,FileName) ->
    case maybe_warn_filename(FileName) of
        continue ->
            do_load_dump(Frame,FileName);
        stop ->
            error
    end.

do_load_dump(Frame,FileName) ->
    ok = observer_lib:display_progress_dialog(wx:null(),
                                              "Crashdump Viewer",
					      "Loading crashdump"),
    crashdump_viewer:read_file(FileName),
    case observer_lib:wait_for_progress() of
	ok    ->
	    %% Set window title
	    T1 = "Crashdump Viewer: ",
            FileLength = string:length(FileName),
	    Title =
		if FileLength > 70 ->
			T1 ++ filename:basename(FileName);
		   true ->
			T1 ++ FileName
		end,
	    wxFrame:setTitle(Frame, Title),
	    {ok,FileName};
	error ->
	    error
    end.

maybe_warn_filename(FileName) ->
    case os:getenv("ERL_CRASH_DUMP_SECONDS")=="0" orelse
        os:getenv("ERL_CRASH_DUMP_BYTES")=="0" of
        true ->
            continue;
        false ->
            DumpName = filename:absname(os:getenv("ERL_CRASH_DUMP", "erl_crash.dump")),
            case filename:absname(FileName) of
                DumpName ->
                    Warning =
                        "WARNING: the current crashdump might be overwritten "
                        "if the crashdump_viewer node crashes.\n\n"
                        "Renaming the file before inspecting it will "
                        "remove the problem.\n\n"
                        "Do you want to continue?",
                    case observer_lib:display_yes_no_dialog(Warning) of
                        ?wxID_YES -> continue;
                        ?wxID_NO -> stop
                    end;
                _ ->
                    continue
            end
    end.

%%%-----------------------------------------------------------------
%%% Find help document (HTML files)
get_help_doc(HelpId) ->
    Internal = get_internal_help_doc(HelpId),
    case filelib:is_file(Internal) of
	true -> Internal;
	false -> get_external_help_doc(HelpId)
    end.

get_internal_help_doc(?ID_HOWTO) ->
    filename:join(erts_doc_dir(),help_file(?ID_HOWTO));
get_internal_help_doc(HelpId) ->
    filename:join(observer_doc_dir(),help_file(HelpId)).

get_external_help_doc(?ID_HOWTO) ->
    filename:join("http://www.erlang.org/doc/apps/erts",help_file(?ID_HOWTO));
get_external_help_doc(HelpId) ->
    filename:join("http://www.erlang.org/doc/apps/observer",help_file(HelpId)).

observer_doc_dir() ->
    filename:join([code:lib_dir(observer),"doc","html"]).

erts_doc_dir() ->
    ErtsVsn = erlang:system_info(version),
    RootDir = code:root_dir(),
    VsnErtsDir = filename:join(RootDir,"erts-"++ErtsVsn),
    DocDir = filename:join(["doc","html"]),
    case filelib:is_dir(VsnErtsDir) of
	true ->
	    filename:join(VsnErtsDir,DocDir);
	false ->
	    %% So this can be run in source tree
	    filename:join([RootDir,"erts",DocDir])
    end.

help_file(?wxID_HELP)  -> "crashdump_help.html";
help_file(?ID_UG)    -> "crashdump_ug.html";
help_file(?ID_HOWTO) -> "crash_dump.html".
