%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2002-2013. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%
-module(etop_gui).
-compile([{nowarn_deprecated_function,{gs,config,2}},
          {nowarn_deprecated_function,{gs,create,3}},
          {nowarn_deprecated_function,{gs,create,4}},
          {nowarn_deprecated_function,{gs,destroy,1}},
          {nowarn_deprecated_function,{gs,read,2}},
          {nowarn_deprecated_function,{gs,start,0}}]).

-author('siri@erix.ericsson.se').

-export([init/1,stop/1]).
-export([formatmfa/1,to_list/1]).% For etop_txt

-include("etop.hrl").
-include("etop_defs.hrl").

-import(etop, [loadinfo/1, meminfo/2, getopt/2]).

%% Heights
-define(BarH, 28).      % height of menubar
-define(LabelH, 90).    % height of label with system info
-define(GridLineH, 21). % height of one line in the table (grid)

%% Column numbers for grid - click to sort
-define(TimeCol, 3).
-define(RedsCol, 4).
-define(MemCol, 5).
-define(MsgQCol, 6).

%% Font
-define(Normal, {screen,12}).
-define(Bold, {screen,bold,12}).


%% -----------------------------------------------------------------------------
stop(_) -> ok.

init(Config) ->
    S = gs:start(),
    Width  = getopt(width, Config),
    TotLines = getopt(lines,Config)+1,

    %% Max number of processes shown in window at startup is 10
    %% If less than 10 lines is specified, window size fits number of lines
    WinH = if TotLines > 11 -> 11*?GridLineH + ?BarH + ?LabelH;
	      true -> TotLines*?GridLineH + ?BarH + ?LabelH
	   end,
    Win    = gs:create(window, S, 
		       [{title, "Erlang Top"},
			{map, true}, %% While debugging
			{configure, true},
			{width, Width}, {height, WinH}]),
    Bar  = gs:create(menubar, Win, []),
        
    FileButt    = gs:create(menubutton, Bar, [{label,{text, " File "}}]),
    OptionsButt = gs:create(menubutton, Bar, [{label,{text, " Options "}}]),
    File        = gs:create(menu, FileButt, []),
    Options     = gs:create(menu, OptionsButt, []),
    gse:named_menuitem(refresh, File,
		       [{label,{text," Refresh "}}]),
    gse:named_menuitem(dump, File,
		       [{label,{text," Dump to file "}}]),
    gse:named_menuitem(exit, File,
		       [{label,{text," Exit "}}]),
    
    gse:named_menuitem(accum, Options,
		       [{label,{text, " Accumulate "}}, 
			{itemtype, check}]),
    gse:named_menuitem(intv, Options,
    		       [{label,{text, " Update Interval "}}]),
    gse:named_menuitem(lines, Options,
    		       [{label,{text, " Number of Lines "}}]),
    Sort = gse:named_menuitem(sort, Options,
			      [{label,{text, " Sort "}},
			       {itemtype,cascade}]),
    SortMenu = gse:create(menu, Sort, []),
    gse:named_menuitem(runtime, SortMenu,
    		       [{label,{text, " Time "}},
			{itemtype,radio},{group,gr1}]),
    gse:named_menuitem(memory, SortMenu,
    		       [{label,{text, " Memory "}},
			{itemtype,radio},{group,gr1}]),
    gse:named_menuitem(reductions, SortMenu,
    		       [{label,{text, " Reductions "}},
			{itemtype,radio},{group,gr1}]),
    gse:named_menuitem(msg_q, SortMenu,
    		       [{label,{text, " Message Queue "}},
			{itemtype,radio},{group,gr1}]),
    
    SysInfo = gs:create(label,Win,[{x, 0}, {y, ?BarH},{align,sw},
				   {width, Width},{height,?LabelH}]),

    {GridH,VScroll} = calc_grid_h(WinH,TotLines),
    Grid = gse:grid(Win, 
		    [{x, 0}, {y, ?BarH+?LabelH},
		     {width, Width},
		     {height, GridH},
		     {hscroll, false},
		     {vscroll, VScroll},
		     {columnwidths, calc_column_w(Width)},
		     {rows, {1, TotLines}},
		     {font,?Normal}]),
    
    %% Header line
    GL1  = gse:gridline(Grid, [{{text, 1}, "PID"},
			       {{text, 2}, "Name or Initial Function"},
			       {{text, ?TimeCol}, "Time(us)"},
			       {{text, ?RedsCol}, "Reds"},
			       {{text, ?MemCol}, "Memory"},
			       {{text, ?MsgQCol}, "MsgQ"},
			       {{text, 7}, "Current Function"},
			       {bg, lightblue},
			       {row, 1},
			       {click, true}]),

    config_sort(GL1,getopt(sort,Config)),
    Info = do_update(Grid, SysInfo, Config),
    
    get_event(Info, Win, Grid, GL1, SysInfo, Config).

calc_column_w(W) ->
    %% W = [2x, 3x, 1x, 1x, 1x, 1x, 3x] = 12x
    RW = W-9, % just to make nice small margins on each side of grid
    X =  RW div 12,
    [2*X, 3*X, X, X, X, X, 3*X + (RW - 12*X)].   

config_sort(GL1,Sort) ->
    gs:config(Sort,[{select,true}]),
    lists:foreach(fun(S) -> 
			  gs:config(GL1,[{{font,S},?Normal}])
		  end,
		  [?TimeCol,?MemCol,?RedsCol,?MsgQCol]),
    case Sort of
	runtime -> gs:config(GL1,{{font,?TimeCol},?Bold});
	memory -> gs:config(GL1,{{font,?MemCol},?Bold});
	reductions -> gs:config(GL1,{{font,?RedsCol},?Bold});
	msg_q -> gs:config(GL1,{{font,?MsgQCol},?Bold})
    end.

config_lines(Win,Grid,TotLines) ->
    OldGridH = gs:read(Grid,height),
    NewLinesH = TotLines*?GridLineH,
    if  NewLinesH =< OldGridH -> 
	    gs:config(Win,[{height,NewLinesH+?BarH+?LabelH}]),
	    gs:config(Grid,[{rows,{1,TotLines}},
			    {height,NewLinesH},
			    {vscroll,false}]);
	true ->
	    gs:config(Grid,[{rows,{1,TotLines}},{vscroll,right}])
    end.

calc_grid_h(WinH,TotLines) ->
    LeftInWin = WinH - ?BarH - ?LabelH,
    TotGrid = TotLines * ?GridLineH,
    if LeftInWin >= TotGrid ->
	    {TotGrid,false};
       true ->
	    {LeftInWin,right}
    end.

set_win_h(Win,OrigH,TotLines) ->
    TotH = TotLines*?GridLineH + ?BarH + ?LabelH,
    if TotH >= OrigH -> OrigH;
       true -> gs:config(Win,[{height,TotH}]),
	       TotH
    end.

get_event(Info, Win, Grid, GL1, SysInfo, Config) ->
    receive 
	{gs, Win, configure,[],[W,H,_,_]} ->
	    TotLines = getopt(lines,Config)+1,
	    %% Will not make window higher than total number of lines
	    RealWinH = set_win_h(Win,H,TotLines),
	    {GridH,VScroll} = calc_grid_h(RealWinH,TotLines),
	    gs:config(Grid, [{width, W},
			     {columnwidths, calc_column_w(W)},
			     {height,GridH}, {vscroll,VScroll}]),
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);	
	{gs, refresh, _, _, _} ->
	    Info1 = do_update(Grid, SysInfo, Config),
	    get_event(Info1, Win, Grid, GL1, SysInfo, Config);
	{gs, dump, _, _, _} ->
	    case pop(Win,dump) of
		{ok,File} -> etop:dump(File);
		{error,cancel} -> ok
	    end,
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);
	{gs, Win, destroy, _, _} ->
	    normal;
	{gs, exit, _, _, _} ->
	    ok;
	{gs, accum, _, _, _} ->
	    Old = getopt(accum,Config),
	    etop:config(accumulate,not Old),
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);	
	{gs,intv,_,_,_} ->
	    case pop(Win,interval) of
		{ok,Intv} -> etop:config(interval,list_to_integer(Intv));
		{error,cancel} -> ok
	    end,
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);
	{gs,lines,_,_,_} ->
	    case pop(Win,lines) of
		{ok,Lines} -> etop:config(lines,list_to_integer(Lines));
		{error,cancel} -> ok
	    end,
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);
	{gs,Sort,_,_,_} when Sort=:=runtime; 
			     Sort=:=memory; 
			     Sort=:=reductions; 
			     Sort=:=msg_q ->
	    etop:config(sort,Sort),
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);	
	{gs,GL1,click,_,[Col,1,_]} -> 
	    case Col of 
		?TimeCol -> etop:config(sort, runtime); 
		?MemCol -> etop:config(sort, memory); 
		?RedsCol -> etop:config(sort, reductions); 
		?MsgQCol -> etop:config(sort, msg_q);
		_other -> ignore
	    end,
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);	
	{config,{Key,Value},Config1} ->
	    case Key of
		lines -> config_lines(Win,Grid,Value+1);
		sort -> config_sort(GL1,Value);
		accumulate -> gs:config(accum,[{select,Value}]);
		_ -> ok
	    end,
	    Info1 = do_update(Grid, SysInfo, Config1),
	    get_event(Info1, Win, Grid, GL1, SysInfo, Config1);
	{dump,Fd} -> 
	    etop_txt:do_update(Fd,Info,Config),
	    get_event(Info, Win, Grid, GL1, SysInfo, Config);
	Msg ->
	    io:format("~p got unexpected msg ~p~n", [?MODULE, Msg]),
	    get_event(Info, Win, Grid, GL1, SysInfo, Config)
    after getopt(intv,Config) ->
	    Info1 = do_update(Grid, SysInfo, Config),
	    get_event(Info1, Win, Grid, GL1, SysInfo, Config)
    end.

do_update(Grid, SysInfo, Config) ->
    Info = etop:update(Config),
    Lines = makegridlines(Info#etop_info.procinfo, Grid, 2),
    clear_lines(Lines, getopt(lines,Config) + 1, Grid),
    makesysinfo(getopt(node,Config),Info,SysInfo),
    Info.

%clear_lines(From, To, _Grid) when From > To -> ok;
clear_lines(From, To, Grid) ->
    case gs:read(Grid, {obj_at_row, From}) of
	undefined ->
	    ok;
	GridLine ->
	    gs:destroy(GridLine),
	    clear_lines(From + 1, To, Grid)
    end.
    
formatmfa({M, F, A}) ->
    io_lib:format("~w:~w/~w",[M, F, A]);
formatmfa(Other) ->
    %% E.g. when running hipe - the current_function for some
    %% processes will be 'undefined'
    io_lib:format("~w",[Other]).


makegridlines([#etop_proc_info{pid=Pid,
			       mem=Mem,
			       reds=Reds,
			       name=Name,
			       runtime=Time,
			       cf=MFA,
			       mq=MQ}
	       |T], Grid, Count) ->
    update_gl(Grid, Count, [{{text, 1}, pid_to_list(Pid)},
			    {{text, 2}, to_list(Name)},
			    {{text, ?TimeCol}, 
			     if is_integer(Time)->integer_to_list(Time);
				true -> Time
			     end},
			    {{text, ?RedsCol}, integer_to_list(Reds)},
			    {{text, ?MemCol}, integer_to_list(Mem)},
			    {{text, ?MsgQCol}, integer_to_list(MQ)},
			    {{text, 7}, formatmfa(MFA)},
			    {row, Count}, {click, false}]), 
     makegridlines(T, Grid, Count + 1);
makegridlines([],_Grid,Count) ->
    Count.

update_gl(Grid, Row, GL) ->
    case gs:read(Grid, {obj_at_row, Row}) of
	undefined ->
	    gse:gridline(Grid,[{row, Row}|GL]);
	GridLine ->
	    gs:config(GridLine,GL)
    end.

to_list(Name) when is_atom(Name) -> atom_to_list(Name);
to_list({_M,_F,_A}=MFA) -> formatmfa(MFA).


makesysinfo(Node,Info,SysInfo) ->
    {Cpu,NProcs,RQ,Clock} = loadinfo(Info),
    case Info#etop_info.memi of
	undefined ->
	    Str = "No memory information is available.";
	Memi ->
	    [Tot,Procs,Atom,Bin,Code,Ets] = 
		meminfo(Memi, [total,processes,atom,binary,code,ets]),
	    Str = io_lib:fwrite(?SYSFORM,
				[Node,Clock,
				 Cpu,Tot,Bin,
				 NProcs,Procs,Code,
				 RQ,Atom,Ets])
    end,
    gs:config(SysInfo,[{label,{text,Str}},{font,?Normal}]).


pop(Win,Key) ->
    Pop = gs:create(window,Win,[{title,"Config"},
			     {width,160},{height,100}]),
    gs:create(label,Pop,[{label,{text,txt(Key)}},
			 {width,160}]),
    gs:create(entry,entry,Pop,[{x,10},{y,30},{width,130},
			       {keypress,true}]),
    gs:create(button,ok,Pop,[{width,45},{y,60},{x,10},
			     {label,{text,"Ok"}}]),
    gs:create(button,cancel,Pop,[{width,60},{y,60},{x,80},
				 {label,{text,"Cancel"}}]),
    gs:config(Pop,{map,true}),
    pop_loop(Pop).

pop_loop(Pop) ->
    receive
	{gs,entry,keypress,_,['Return'|_]} ->
	    Str = gs:read(entry,text),
	    gs:destroy(Pop),
	    {ok,Str};
	{gs,entry,keypress,_,_} -> % all other keypresses
	    pop_loop(Pop);
	{gs,ok,click,_,_} ->
	    Str = gs:read(entry,text),
	    gs:destroy(Pop),
	    {ok,Str};
	{gs,cancel,click,_,_} ->
	    gs:destroy(Pop),
	    {error,cancel};
	X ->
	    io:format("Got X=~w~n",[X]),
	    pop_loop(Pop)
    end.

txt(interval) -> "Enter new interval:";
txt(lines) -> "Enter number of lines:";
txt(dump) -> "Enter file name:".
