%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2012. 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(appmon).
-behaviour(gen_server).
-compile([{nowarn_deprecated_function,{gs,config,2}},
          {nowarn_deprecated_function,{gs,create,3}},
          {nowarn_deprecated_function,{gs,destroy,1}},
          {nowarn_deprecated_function,{gs,read,2}},
          {nowarn_deprecated_function,{gs,start,1}}]).

%%%---------------------------------------------------------------------
%%% Appmon main module.
%%% Creates the main window and receives load and application
%%% information from all connected nodes.
%%%---------------------------------------------------------------------

%% External exports
-export([start/0, stop/0]).

%% gen_server callbacks
-export([init/1, handle_cast/2, handle_info/2, terminate/2]).
-export([handle_call/3, code_change/3]).        % not used

%% Canvas button data
-record(canvasbutton, {text, ul, ll, rect, x, y, w, h}).

%% Options - all the fields are GS radio buttons
-record(options, {single, many, time, queue, prog, linear}).

%% Main window data
-record(win, {name,                             % atom() Monitored node
	      window,                           % gsobj()
	      wwindow,                          % int() Window width
	      hwindow,                          % int() Window height
	      options,                          % #options{}
	      canvas,                           % gsobj()
	      wcanvas,                          % int() Canvas width
	      hcanvas,                          % int() Canvas height
	      l1, l2,                           % gsobj() Canvas lines
	      leds,                             % [gsobj()] Load meter
	      nodelabel,                        % {gsobj(),gsobj()}
	      appobjs=[],                       % [gsobj()] Buttons etc.
	      nodemenu}).                       % gsobj() Node menu

%% Node data
-record(mnode, {name,                           % atom() Node name
		status,                         % alive | dead
		pid,                            % pid()
		apps,                           % [{Pid,App,Descr}]
		load}).                         % {Old, New}

%% Internal state data
-record(state, {gs,                             % pid()
		wins=[],                        % [#win()] GUIs
		window_mode,                    % single | many
		load_mode1,                     % time | queue
		load_mode2,                     % prog | linear
		lbpid,                          % pid()
		mnodes=[]}).                    % [#mnode{}] 

%%%---------------------------------------------------------------------
%%% External exports
%%%---------------------------------------------------------------------

start() ->
     gen_server:start({local, appmon}, ?MODULE, [], []).

stop() ->
    gen_server:cast(appmon, stop).


%%%---------------------------------------------------------------------
%%% gen_server callbacks
%%%---------------------------------------------------------------------

%%----------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, State}          |
%%          {ok, State, Timeout} |
%%          ignore               |
%%          {stop, Reason}
%%----------------------------------------------------------------------
init([]) ->
    process_flag(trap_exit, true),

    %% Subscribe to {nodeup,Node} and {nodedown,Node} messages
    net_kernel:monitor_nodes(true),
    
    LbPid = appmon_lb:start(self ()),

    %% Check which remote nodes have appmon code available (OTP-4887)
    NodesOk = lists:filter(fun(Node) -> check_node(Node) end, nodes()),
    Nodes = [node()|NodesOk],

    %% Start monitoring the existing nodes
    MNodes = mk_mnodes(Nodes, LbPid),

    %% Draw the main window
    GS = gs:start([{kernel,true}]),
    GUI = draw_win(GS, node()),

    %% Update the Nodes menu with all known nodes
    lists:foreach(fun(Node) ->
			  display_addnode(GUI, Node)
		  end,
		  Nodes),

    %% Mark the default options as selected in the Options menu
    display_setopt(GUI, single),
    display_setopt(GUI, time),
    display_setopt(GUI, prog),

    {ok, #state{gs=GS, wins=[GUI],
		window_mode=single, load_mode1=time, load_mode2=prog,
		lbpid=LbPid, mnodes=MNodes}}.

check_node(Node) ->
    case rpc:call(Node, code, which, [appmon]) of
	File when is_list(File) ->
	    true;
	_ -> % non_existing (| cover_compiled)
	    false
    end.

%%----------------------------------------------------------------------
%% Func: handle_call/3
%% Returns: {reply, Reply, State}          |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
%%----------------------------------------------------------------------
handle_call(norequest, _From, State) ->
    {reply, null, State}.

%%----------------------------------------------------------------------
%% Func: handle_cast/2
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%----------------------------------------------------------------------
handle_cast(stop, State) ->
    {stop, normal, State}.
    
%%----------------------------------------------------------------------
%% Func: handle_info/2
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%----------------------------------------------------------------------
%% Load information from a node
handle_info({delivery, _Serv, load, Node, Load}, State) ->
    
    %% Update node information
    MNode = get_mnode(Node, State#state.mnodes),
    MNode1 = MNode#mnode{load=Load},
    MNodes = replace_mnode(Node, MNode1, State#state.mnodes),

    %% If Node is currently displayed, update graphics
    case get_win(Node, State#state.wins) of
	{ok, GUI} ->
	    display_load(GUI, Load);
	false ->
	    ignore
    end,
    
    {noreply, State#state{mnodes=MNodes}};

%% Application information from a node
handle_info({delivery, _Serv, app_ctrl, Node, Apps}, State) ->
    
    %% Update node information
    MNode = get_mnode(Node, State#state.mnodes),
    MNode1 = MNode#mnode{apps=Apps},
    MNodes = replace_mnode(Node, MNode1, State#state.mnodes),

    %% If Node is currently displayed, update graphics
    Wins = case get_win(Node, State#state.wins) of
	       {ok, GUI} ->
		   draw_clear(GUI),
		   GUI1 = draw_apps(GUI, Apps),
		   replace_win(Node, GUI1, State#state.wins);
	       false ->
		   State#state.wins
	   end,
    
    appmon_lb:add_apps (State#state.lbpid, Apps, Node),
    {noreply, State#state{wins=Wins, mnodes=MNodes}};

handle_info({nodeup, Node}, State) ->

    %% First, make sure appmon code is available at remode node,
    %% or the node should be ignored (OTP-3591)
    case check_node(Node) of
	true ->

	    %% If this is a previously unknown node, update window's
	    %% 'Nodes' menu
	    case get_mnode(Node, State#state.mnodes) of
		false ->
		    display_addnode(State#state.wins, Node);
		_OldMnode ->
		    ignore
	    end,

	    %% Update node information (=> state is automatically
	    %% changed to 'alive')
	    MNode = mk_mnode(Node, State#state.lbpid),
	    MNodes = replace_mnode(Node, MNode, State#state.mnodes),

	    %% If Node is currently displayed, update graphics
	    case get_win(Node, State#state.wins) of
		{ok, GUI} ->
		    display_nodeup(GUI, Node);
		false ->
		    ignore
	    end,

	    appmon_lb:update_status(State#state.lbpid, Node, alive),
	    {noreply, State#state{mnodes=MNodes}};

	false ->
	    {noreply, State}
    end;

handle_info({nodedown, Node}, State) ->
    
    %% If this is a previously unknown node, ignore the message.
    %% (The situation occurs when failing to connect to another node).
    %% Otherwise, update the node information.
    case get_mnode(Node, State#state.mnodes) of
	false ->
	    {noreply, State};
	MNode ->
	    MNode1 = MNode#mnode{status=dead},
	    MNodes = replace_mnode(Node, MNode1, State#state.mnodes),

	    %% If Node is currently displayed, update graphics
	    Wins = case get_win(Node, State#state.wins) of
		       {ok, GUI} ->
			   display_nodedown(GUI),
			   GUI1 = draw_clear(GUI),
			   replace_win(Node, GUI1, State#state.wins);
		       false ->
			   State#state.wins
		   end,

	    appmon_lb:remove_node(State#state.lbpid, Node),
	    {noreply, State#state{wins=Wins, mnodes=MNodes}}
    end;

%% Application 'button' events
handle_info({gs, _Obj, buttonpress, Data, _Arg}, State) ->
    {canvasbutton, CBtn, _App} = Data,
    press(CBtn),
    {noreply, State};
handle_info({gs, _Obj, buttonrelease, Data, [_,X,Y|_]}, State) ->
    {canvasbutton, CBtn, {application, App, Node}} = Data,
    release(CBtn),

    %% Check that mouse button was released over the button!
    L = CBtn#canvasbutton.x, R = L + CBtn#canvasbutton.w,
    T = CBtn#canvasbutton.y, B = T + CBtn#canvasbutton.h,
    if
	X>L, X<R, Y>T, Y<B ->
	    MNode = get_mnode(Node, State#state.mnodes),
	    {value, {Pid, _App, _Descr}} =
		lists:keysearch(App, 2, MNode#mnode.apps),
	    appmon_a:start(Node, App, Pid);
	true ->
	    ignore
    end,
    {noreply, State};

handle_info({gs, _Button, click, Data, _Arg}, State) ->
    ThisNode = node(),
    case Data of
	
	%% File menu item
	listbox ->
	    appmon_lb:open_win(State#state.lbpid,
			       parse_nodes(State#state.mnodes)),
	    {noreply, State};
	{close, WinObj} ->
	    {ok, GUI} = get_win2(WinObj, State#state.wins),
	    gs:destroy(WinObj),

	    %% Terminate if this was the only open window
	    case remove_win(GUI#win.name, State#state.wins) of
		[] ->
		    {stop, normal, State};
		Wins ->
		    {noreply, State#state{wins=Wins}}
	    end;
	exit ->
	    {stop, normal, State};

	%% Actions menu item
	{action, Action, WinObj} ->
	    {ok, GUI} = get_win2(WinObj, State#state.wins),
	    Node = GUI#win.name,

	    if
		Node==ThisNode ->
		    case Action of
			ping ->
			    %% Ignore - makes no sense to ping yourself
			    ignore;
			_ -> % reboot | restart | stop
			    apply(init, Action, [])
		    end;

		Node/=ThisNode ->
		    case Action of
			ping ->
			    net_adm:ping(Node);
			_ -> % reboot | restart | stop
			    rpc:cast(Node, init, Action, [])
		    end
	    end,
	    {noreply, State};

	%% Options menu item
	{window_mode, Mode} ->
	    
	    %% Update windows so they all show the same options
	    lists:foreach(fun(GUI) ->
				  display_setopt(GUI, Mode)
			  end,
			  State#state.wins),
	    {noreply, State#state{window_mode=Mode}};
	
	{option, Tag, Option} ->
	    
	    %% Update windows so they all show the same options
	    lists:foreach(fun(GUI) ->
				  display_setopt(GUI, Tag)
			  end,
			  State#state.wins),

	    %% Update all appmon_info processes about which kind of
	    %% load data is desired
	    lists:foreach(fun(MNode) ->
				  appmon_info:load(MNode#mnode.pid,
						   MNode#mnode.name,
						   true,
						   Option)
			  end,
			  State#state.mnodes),

	    if
		Tag==time; Tag==queue ->
		    {noreply, State#state{load_mode1=Tag}};
		Tag==prog; Tag==linear ->
		    {noreply, State#state{load_mode2=Tag}}
	    end;

	%% Nodes menu item
	{node, Node, WinObj} ->

	    %% Check first if this window is already displayed
	    case get_win(Node, State#state.wins) of
		{ok, GUI} ->

		    %% Node is already displayed, raise its window
		    gs:config(GUI#win.window, raise),

		    {noreply, State};

		%% Node is not displayed
		false ->

		    %% Redraw existing window or create a new window
		    %% depending on window mode
		    case State#state.window_mode of

			single ->
			    {ok, GUI} =
				get_win2(WinObj, State#state.wins),
			    	    
			    %% Clear window and correct the node name
			    draw_clear(GUI),
			    GUI1 = draw_nodename(GUI, Node),

			    %% Update window with the correct node name
			    %% and the applications running at the node
			    MNode = get_mnode(Node, State#state.mnodes),
			    GUI2 = case MNode#mnode.status of
				       dead ->
					   display_nodedown(GUI1),
					   GUI1;
				       alive ->
					   display_nodeup(GUI1, Node),
					   draw_apps(GUI1,
						     MNode#mnode.apps)
				   end,
			    Wins = replace_win(GUI#win.name, GUI2,
					       State#state.wins),

			    {noreply, State#state{wins=Wins}};

			many ->
			    GUI = draw_win(State#state.gs, Node),

			    %% Update Nodes menu with all known nodes -
			    %% use MNodes to get them in the right order
			    lists:foreach(fun(MNode) ->
						  Name =
						      MNode#mnode.name,
						  display_addnode(GUI,
								  Name)
					  end,
					  State#state.mnodes),

			    %% Mark selected options in the Options menu
			    display_setopt(GUI, many),
			    display_setopt(GUI, State#state.load_mode1),
			    display_setopt(GUI, State#state.load_mode2),

			    %% Add the applications running at the node
			    MNode = get_mnode(Node, State#state.mnodes),

			    GUI1 = case MNode#mnode.status of
				       dead ->
					   display_nodedown(GUI),
					   GUI;
				       alive ->
					   display_nodeup(GUI, Node),
					   draw_apps(GUI,
						     MNode#mnode.apps)
				   end,
			    Wins = [GUI1|State#state.wins],
			    
			    {noreply, State#state{wins=Wins}}
		    end
	    end;

        %% Help menu = Help button
	help ->
	    HelpFile = filename:join([code:lib_dir(appmon),
				     "doc", "html", "part_frame.html"]),
	    case State#state.wins of
		[Win] ->
		    tool_utils:open_help(Win#win.window, HelpFile);
		_ ->
		    tool_utils:open_help(State#state.gs, HelpFile)
	    end,
	    {noreply, State};
		
	_Other ->
	    {noreply, State}
    end;
handle_info({gs, WinObj, configure, _, [WWindow, HWindow|_]}, State) ->
    {ok, GUI} = get_win2(WinObj, State#state.wins),
    GUI1 = draw_resize(GUI, WWindow, HWindow),
    display_scrollbar(GUI1),
    Wins = replace_win(GUI#win.name, GUI1, State#state.wins),
    {noreply, State#state{wins=Wins}};
handle_info({gs, WinObj, destroy, _, _}, State) ->	% OTP-1179
    {ok, GUI} = get_win2(WinObj, State#state.wins),

    %% Terminate if this was the only open window
    case remove_win(GUI#win.name, State#state.wins) of
	[] ->
	    {stop, normal, State};
	Wins ->
	    {noreply, State#state{wins=Wins}}
    end;

handle_info(stop, State) ->
    {stop, normal, State};
handle_info({'EXIT', Pid, Reason}, State) ->
    case Reason of
	shutdown ->
	    %% Appmon is being asked to shut down, eg during reboot
	    {stop, Reason, State};
	_ ->
	    case State#state.gs of
		
		%% GS exited, kill appmon
		{0, Pid} ->
		    {stop, normal, State};
	
		_ ->
		    {noreply, State}
	    end
    end;
handle_info(_Info, State) ->
    {noreply, State}.
	
%%----------------------------------------------------------------------
%% Func: terminate/2
%% Purpose: Shutdown the server
%% Returns: any (ignored by gen_server)
%%----------------------------------------------------------------------
terminate(_Reason, State) ->
    bcast(State#state.mnodes, {kill}),
    appmon_lb:stop(State#state.lbpid),
    ok.

%%----------------------------------------------------------------------
%% Func: code_change/3
%% Purpose: Convert process state when code is changed
%% Returns: {ok, NewState}
%%----------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.
    

%%%---------------------------------------------------------------------
%%% Internal functions
%%%---------------------------------------------------------------------

%%----------------------------------------------------------------------
%% MNode manipulating functions
%%----------------------------------------------------------------------

%% mk_mnodes(Nodes, LbPid) -> MNodes
%%   Nodes -> [atom()]
%%   LbPid -> pid()
%%   MNodes -> [#mnode{}]
mk_mnodes([Node|Nodes], LbPid) ->
    [mk_mnode(Node, LbPid) | mk_mnodes(Nodes, LbPid)];
mk_mnodes([], _LbPid) ->
    [].

mk_mnode(Node, LbPid) ->

    %% Create an appmon process at the node
    {ok, Pid} = appmon_info:start_link(Node, self(), []),
    
    appmon_lb:add_node(LbPid, Node),
    appmon_info:load(Pid, Node, true, [{timeout,1000}]),
    appmon_info:app_ctrl(Pid, Node, true, []),

    #mnode{name=Node, status=alive, pid=Pid}.

%% get_mnode(Node, MNodes) -> MNode | false
%%   Node -> atom()
%%   MNodes -> [#mnode{}]
%%   MNode -> #mnode{}
get_mnode(Node, MNodes) ->
    case lists:keysearch(Node, #mnode.name, MNodes) of
	{value, MNode} ->
	    MNode;
	false ->
	    false
    end.

%% replace_mnode(Node, MNode, MNodes1) -> Mnodes2
%%   Node -> atom()
%%   MNode -> #mnode{}
%%   MNodes1 -> MNodes2 -> [#mnode{}]
%% Replaces, or adds if previously not included, the mnode with name
%% Node in MNodes1 with MNode.
replace_mnode(Node, MNode, [#mnode{name=Node} | MNodes]) ->
    [MNode | MNodes];
replace_mnode(Node, MNode, [MNode2 | MNodes]) ->
    [MNode2 | replace_mnode(Node, MNode, MNodes)];
replace_mnode(_Node, MNode, []) ->
    [MNode].
	

%%----------------------------------------------------------------------
%% GUI list manipulating functions
%%----------------------------------------------------------------------

%% get_win(Node, Wins) -> Win
%%   Node -> atom()
%%   Wins -> [#win{}]
%%   Win -> #win{}
get_win(Node, Wins) ->
    case lists:keysearch(Node, #win.name, Wins) of
	{value, Win} ->
	    {ok, Win};
	false ->
	    false
    end.

%% get_win2(WinObj, Wins) -> Win
%%   Window -> gsobj()
%%   Wins -> [#win{}]
%%   Win -> #win{}
get_win2(WinObj, Wins) ->
    case lists:keysearch(WinObj, #win.window, Wins) of
	{value, Win} ->
	    {ok, Win};
	false ->
	    false
    end.

%% replace_win(Node, Win, Wins) -> Wins2
%%   Node -> atom()
%%   Win -> #win{}
%%   Wins -> Wins2 -> [#win{}]
replace_win(Node, Win, Wins) ->
    lists:keyreplace(Node, #win.name, Wins, Win).

%% remove_win(Node, Wins) -> Wins2
%%   Node -> atom()
%%   Wins -> Wins2 -> [#win{}]
remove_win(Node, Wins) ->
    lists:keydelete(Node, #win.name, Wins).




%%----------------------------------------------------------------------
%% GUI manipulating functions
%%----------------------------------------------------------------------
-define(PAD, 10).                         % Pad between objects
-define(PAD2, 4*?PAD).                    % Pad betw. node lbl and app

-define(hMENUBAR, 25).                    % Note: Hardwired in Tcl/Tk

-define(xNODELBL, 60).                    % Node label
-define(yNODELBL, 35).
-define(hNODELBL, 20).

-define(xMETER, 5).                       % Meter
-define(yMETER, ?yNODELBL).
-define(wMETER, 20).
-define(hMETER, ?hNODELBL + ?PAD + ?PAD2 + ?hBTN).
-define(LEDCOUNT, 16).

-define(xBTN, ?xNODELBL).                 % Application buttons
-define(yBTN, ?yNODELBL + ?hNODELBL + ?PAD + ?PAD2).
-define(wBTN, 70).                        % min width
-define(hBTN, 20).

-define(wCANVAS, 470 + ?wMETER + 3*?PAD). % Canvas
-define(hCANVAS, ?yNODELBL + ?hNODELBL + ?PAD + ?PAD2 + ?hBTN +	2*?PAD).

-define(wWIN, ?wCANVAS).                  % Window
-define(hWIN, ?hMENUBAR + ?hCANVAS).

%%--Main window---------------------------------------------------------

draw_win(GS, Node) ->

    %% Main window
    NodeStr = atom_to_list(Node),
    Win = gs:create(window, GS, [{title,
				  "APPMON: Overview on " ++ NodeStr},
				 {width, ?wWIN}, {height, ?hWIN},
				 {configure, true}]),
    Canvas = gs:create(canvas, Win, [{x, 0}, {y, ?hMENUBAR},
				     {width, ?wCANVAS},
				     {height, ?hCANVAS}]),
    L1 = gs:create(line, Canvas, [{coords,
				   [{0,?yNODELBL-?PAD},
				    {?wCANVAS,?yNODELBL-?PAD}]}]),
    L2 = gs:create(line, Canvas, [{coords,
				   [{0,?hCANVAS-?PAD},
				    {?wCANVAS,?hCANVAS-?PAD}]}]),
    
    %% Standard buttons
    MenuBar = gs:create(menubar, Win, [{height, ?hMENUBAR}]),

    FileMenuBtn = gs:create(menubutton, MenuBar,
			    [{label, {text,"File"}}]),
    FileMenu = gs:create(menu, FileMenuBtn, []),
    gs:create(menuitem, FileMenu, [{label, {text,"Show List Box..."}},
				   {data, listbox}]),
    gs:create(menuitem, FileMenu, [{label, {text, "Close"}},
				   {data, {close, Win}}]),
    gs:create(menuitem, FileMenu, [{itemtype, separator}]),
    gs:create(menuitem, FileMenu, [{label, {text, "Exit"}},
				   {data, exit}]),

    ActionMenuBtn = gs:create(menubutton, MenuBar,
			      [{label,{text,"Actions"}}]),
    ActionMenu = gs:create(menu, ActionMenuBtn, []),
    gs:create(menuitem, ActionMenu, [{label, {text,"Reboot"}},
				     {data, {action, reboot, Win}}]),
    gs:create(menuitem, ActionMenu, [{label, {text,"Restart"}},
				     {data, {action, restart, Win}}]),
    gs:create(menuitem, ActionMenu, [{label, {text,"Stop"}},
				     {data, {action, stop, Win}}]),
    gs:create(menuitem, ActionMenu, [{label, {text,"Ping"}},
				     {data, {action, ping, Win}}]),

    OptMenuBtn = gs:create(menubutton, MenuBar,
			   [{label, {text,"Options"}}]),
    OptMenu = gs:create(menu, OptMenuBtn, []),
    G0 = now(), % Group identity unique per window!
    SMI = gs:create(menuitem, OptMenu, [{label, {text,"One window"}},
					{itemtype, radio}, {group, G0},
					{data, {window_mode, single}}]),
    MMI = gs:create(menuitem, OptMenu, [{label, {text,"Many windows"}},
					{itemtype, radio}, {group, G0},
					{data, {window_mode, many}}]),
    gs:create(menuitem, OptMenu, [{itemtype, separator}]),
    G1 = now(),
    TMI = gs:create(menuitem, OptMenu, [{label, {text,"Load: time"}},
					{itemtype, radio}, {group, G1},
					{data,
					 {option, time,
					  [{load_method,time}]}}]),
    QMI = gs:create(menuitem, OptMenu, [{label, {text,"Load: queue"}},
					{itemtype, radio}, {group, G1},
					{data,
					 {option, queue,
					  [{load_method,queue}]}}]),
    G2 = now(),
    PMI = gs:create(menuitem, OptMenu,
		    [{label, {text,"Load: progressive"}},
		     {itemtype, radio}, {group, G2},
		     {data, {option, prog, [{load_scale,prog}]}}]),
    LMI = gs:create(menuitem, OptMenu, [{label, {text,"Load: linear"}},
					{itemtype, radio}, {group, G2},
					{data,
					 {option, linear,
					  [{load_scale,linear}]}}]),

    NodeMenuBtn = gs:create(menubutton, MenuBar,
			    [{label, {text,"Nodes"}}]),
    NodeMenu = gs:create(menu, NodeMenuBtn, []),

    HelpMenuBtn = gs:create(menubutton, MenuBar,
			    [{label, {text,"Help"}}, {side, right}]),
    HelpMenu = gs:create(menu, HelpMenuBtn, []),
    gs:create(menuitem, HelpMenu, [{label, {text,"Help"}},
				   {data, help}]),

    %% Meter
    HLed = trunc((?hMETER)/(?LEDCOUNT)),
    Leds = draw_leds(?LEDCOUNT, Canvas, ?yMETER, HLed, []),
    leds_down(Leds, ?LEDCOUNT, 0),
    gs:create(text, Canvas, [{coords,
			      [{?xMETER, ?yMETER+HLed*?LEDCOUNT}]},
			     {anchor, nw},
			     {font, {screen,8}},
			     {text, "Load"}]),
    gs:create(text, Canvas, [{coords, [{?xMETER+?wMETER, ?yMETER}]},
			     {anchor, nw},
			     {font, {screen,8}},
			     {text, "Hi"}]),
    gs:create(text, Canvas, [{coords, [{?xMETER+?wMETER,
					?yMETER+HLed*?LEDCOUNT}]},
			     {anchor, w},
			     {font, {screen,8}},
			     {text, "Lo"}]),

    %% Node label
    WNodeLbl = 8*length(NodeStr)+10,
    NLRect = gs:create(rectangle, Canvas,
		       [{coords, [{?xNODELBL,?yNODELBL},
				  {?xNODELBL+WNodeLbl,
				   ?yNODELBL+?hNODELBL}]},
			{fill, black}]),
    Xc = ?xNODELBL + round(WNodeLbl/2),
    Yc = ?yNODELBL + round(?hNODELBL/2),
    NLText = gs:create(text, Canvas, [{text, NodeStr},
				      {fg, {250,235,215}},
				      {coords, [{Xc,Yc}]},
				      {anchor, c}]),
    NodeLbl = {NLRect, NLText},
    
    gs:config(Win, {map, true}),
    #win{name=Node,
	 window=Win, wwindow=?wWIN, hwindow=?hCANVAS,
	 options=#options{single=SMI, many=MMI,
			  time=TMI, queue=QMI, prog=PMI, linear=LMI},
	 canvas=Canvas, wcanvas=?wCANVAS, hcanvas=?hCANVAS,
	 l1=L1, l2=L2, leds=Leds, nodelabel=NodeLbl, nodemenu=NodeMenu}.

draw_leds(N, Canvas, Y, HLed, Leds) when N>0 ->
    Led = gs:create(rectangle, Canvas,
		    [{coords,
		      [{?xMETER,Y}, {?xMETER+?wMETER,Y+HLed}]}]),
    draw_leds(N-1, Canvas, Y+HLed, HLed, [Led | Leds]);
draw_leds(0, _Canvas, _Y, _HLed, Leds) ->
    Leds.

%%--Draw functions------------------------------------------------------
%% Functions that modify the GUI and its data (win{})

%% Display the node name in the window title
%% (The name in the node label is changed by display_nodeup|nodedown)
%% Used when a changing the node to display
draw_nodename(GUI, Node) ->
    NodeStr = atom_to_list(Node),
    gs:config(GUI#win.window,
	      {title, "APPMON: Overview on " ++ NodeStr}),
    GUI#win{name=Node}.

%% Resize the canvas (when the window has been resized)
draw_resize(GUI, W, H) ->
    Hc = H - ?hMENUBAR,
    gs:config(GUI#win.canvas, [{width, W}, {height, Hc}]),
    Yline1 = ?yNODELBL-?PAD,
    Yline2 = ?hCANVAS-?PAD,
    gs:config(GUI#win.l1, [{coords, [{0,Yline1},{W,Yline1}]}]),
    gs:config(GUI#win.l2, [{coords, [{0,Yline2},{W,Yline2}]}]),
    GUI#win{wwindow=W, hwindow=Hc}.

%% Clear the GUI from applications and connecting lines
draw_clear(GUI) ->
    draw_clear2(GUI#win.appobjs),
    gs:config(GUI#win.canvas, [{hscroll, false}]),
    GUI#win{appobjs=[]}.
draw_clear2([CBtn | AppObjs]) when is_record(CBtn, canvasbutton) ->
    gs:destroy(CBtn#canvasbutton.text),
    gs:destroy(CBtn#canvasbutton.ul),
    gs:destroy(CBtn#canvasbutton.ll),
    gs:destroy(CBtn#canvasbutton.rect),
    draw_clear2(AppObjs);
draw_clear2([GSObj | AppObjs]) ->
    gs:destroy(GSObj),
    draw_clear2(AppObjs);
draw_clear2([]) ->
    ignore.

%% Display the applications, which are a list of tuples: {Pid,App,Descr}
%% Display them in the reversed order to get them chronologically
%% from left to right.
draw_apps(GUI, Apps) ->
    {AppObjs, WCanvas} = draw_apps(GUI, lists:reverse(Apps), ?xNODELBL,
				   undefined, 0, []),
    NewGUI = GUI#win{wcanvas=WCanvas, appobjs=AppObjs},
    display_scrollbar(NewGUI),
    NewGUI.

draw_apps(GUI, [App | Apps], X, Lx0, N, GSObjs) ->

    %% Some necessary data
    {_Pid, AppName, _Descr} = App,
    Text = atom_to_list(AppName),
    Width = erlang:max(8*length(Text)+10, ?wBTN),

    %% Connect the application to the node label with a line
    %% Lx0 = leftmost X coordinate (above previous application button)
    %% Lx = X coordinate, Ly1, Ly2 = top and bottom Y coordinates
    Lx = X + trunc(Width/2),
    Line = case N of
	       %% First (leftmost application) - draw a vertical line
	       %% between the node label and application button
	       0 ->
		   Ly1 = ?yNODELBL + ?hNODELBL +?PAD,
		   Ly2 = Ly1 + ?PAD2,
		   gs:create(line, GUI#win.canvas,
			     [{coords, [{Lx, Ly1}, {Lx, Ly2}]}]);
	       %% Nth application, N>1 - draw a horizontal line from
	       %% line connecting to the previous application button,
	       %% to above this application button, then vertically down
	       %% to the application button
	       _ ->
		   Ly1 = ?yNODELBL + ?hNODELBL + ?PAD + ?PAD2/2,
		   Ly2 = Ly1 + ?PAD2/2,
		   gs:create(line, GUI#win.canvas,
			     [{coords, [{Lx0, Ly1}, {Lx, Ly1},
					{Lx, Ly2}]}])
	   end,
    
    %% The application is represented using a 'canvasbutton'
    Data = {application, AppName, GUI#win.name},
    AppBtn = canvasbutton(GUI#win.canvas, Text, X, ?yBTN, Width, ?hBTN,
			  Data),

    draw_apps(GUI, Apps, X+Width+?PAD, Lx, N+1, [AppBtn, Line|GSObjs]);
draw_apps(_GUI, [], X, _N, _Lx0, GSObjs) ->
    {GSObjs, X}.

%%--Display functions---------------------------------------------------
%% Functions that modify the GUI but not its data

%% Add a new node to the Nodes menu
%% Used when a new node has connected
display_addnode([GUI|GUIs], Node) ->
    display_addnode(GUI, Node),
    display_addnode(GUIs, Node);
display_addnode([], _Node) ->
    ignore;
display_addnode(GUI, Node) ->
    Txt = "Show " ++ atom_to_list(Node),
    gs:create(menuitem, GUI#win.nodemenu,
	      [{label, {text,Txt}},
	       {data, {node, Node, GUI#win.window}}]).

%% Show that a node has come back up
display_nodeup(GUI, Node) ->
    {Rect, Text} = GUI#win.nodelabel,

    %% Check coordinates for the rectangle and compute the new width
    [{L, T}, {_R, B}] = gs:read(Rect, coords),
    NodeStr = atom_to_list(Node),
    W = 8*length(NodeStr)+10,
    
    gs:config(Rect, [{coords, [{L, T}, {L+W, B}]}, {fill, black}]),
    gs:config(Text, [{text, NodeStr}, {fg, {250,235,215}},
		     {coords,
		      [{L+round(W/2), T+round((?hNODELBL)/2)}]}]).

%% Show that a node has gone down
display_nodedown(GUI) ->
    {Rect, Text} = GUI#win.nodelabel,
    
    [{L, T}, {_R, B}] = gs:read(Rect, coords),
    gs:config(Rect, [{coords, [{L, T}, {L+114, B}]}, {fill, gray}]),
    gs:config(Text, [{text, "No connection"}, {fg, black},
		     {coords, [{L+57, T+round((?hNODELBL)/2)}]}]).

%% Add/remove scrollbars as necessary
display_scrollbar(GUI) ->

    WWindow = GUI#win.wwindow,
    HWindow = GUI#win.hwindow,
    WCanvas = GUI#win.wcanvas,
    HCanvas = GUI#win.hcanvas,
    if
	WCanvas>WWindow ->
	    gs:config(GUI#win.canvas,
		      [{hscroll, bottom},
		       {scrollregion,{0,0,WCanvas,HCanvas}}]);
	true ->
	    gs:config(GUI#win.canvas, [{hscroll, false}])
    end,
    if
	HCanvas>HWindow ->
	    gs:config(GUI#win.canvas,
		      [{vscroll, left},
		       {scrollregion,{0,0,WCanvas,HCanvas}}]);
					
	true ->
	    gs:config(GUI#win.canvas, [{vscroll, false}])
    end.

%% Select option radio buttons
display_setopt(GUI, Option) ->
    gs:config(radiobutton(GUI, Option), {select,true}).

radiobutton(GUI, single) -> (GUI#win.options)#options.single;
radiobutton(GUI, many) ->   (GUI#win.options)#options.many;
radiobutton(GUI, time) ->   (GUI#win.options)#options.time;
radiobutton(GUI, queue) ->  (GUI#win.options)#options.queue;
radiobutton(GUI, prog) ->   (GUI#win.options)#options.prog;
radiobutton(GUI, linear) -> (GUI#win.options)#options.linear.

%% Display load
%% Used when load information is received from the displayed node
-define(highloadfg, {255,99,71}).
-define(midloadfg, yellow).
-define(lowloadfg, green).
-define(highloadbg, {140,157,178}).
-define(midloadbg, ?highloadbg).
-define(lowloadbg, ?highloadbg).

display_load(GUI, {Old, New}) ->
    if
	Old == New ->
	    true;
	Old > New ->
	    leds_down(GUI#win.leds, Old, New);
	true ->
	    leds_up(GUI#win.leds, Old, New)
    end.

leds_down(_Leds, Old, New) when Old == New -> 
    done;
leds_down(Leds, Old, New) when Old > New -> 
    reset_led(Leds, Old),
    leds_down(Leds, Old-1, New).
leds_up(_Leds, Old, New) when Old == New -> 
    done;
leds_up(Leds, Old, New) when Old < New -> 
    set_led(Leds, Old),
    leds_up(Leds, Old+1, New).

led_on_col(N) when N > 13 -> ?highloadfg;
led_on_col(N) when N > 9 -> ?midloadfg;
led_on_col(_) -> ?lowloadfg.

led_off_col(N) when N > 13 -> ?highloadbg;
led_off_col(N) when N > 9 -> ?midloadbg;
led_off_col(_) -> ?lowloadbg.

reset_led(_Leds, 0) -> ok;
reset_led(Leds, N) ->
    gs:config(lists:nth(N, Leds), [{fill, led_off_col(N)}]).

set_led(_Leds, 0) -> ok;
set_led(Leds, N) ->
    gs:config(lists:nth(N, Leds), [{fill, led_on_col(N)}]).

%%----------------------------------------------------------------------
%% Utilities
%%----------------------------------------------------------------------

bcast(MNodes, Msg) ->
    lists:foreach(fun(MNode) ->
			  case MNode#mnode.status of
			      alive ->
				  MNode#mnode.pid ! Msg;
			      dead ->
				  ignore
			  end
		  end,
		  MNodes).

%% parse_nodes(MNodes) -> NodeApps
%%   MNodes -> [#mnode{}]
%%   NodeApps -> [{Node, Status, Apps}]
%%     Node -> atom()
%%     Status -> alive | dead
%%     Apps -> [{Pid, App}]
%%       Pid -> pid()
%%       App -> atom()
parse_nodes(MNodes) ->
    parse_nodes(MNodes, []).
parse_nodes([MNode|MNodes], NodeApps) ->
    Apps = parse_apps(MNode#mnode.apps, []),
    parse_nodes(MNodes,
		[{MNode#mnode.name,MNode#mnode.status,Apps}|NodeApps]);
parse_nodes([], NodeApps) ->
    NodeApps.
    
parse_apps([{Pid, App, _Descr}|Rest], Apps) ->
    parse_apps(Rest, [{Pid, App}|Apps]);
parse_apps([], Apps) ->
    Apps.

%%----------------------------------------------------------------------
%% Canvas buttons
%%----------------------------------------------------------------------

canvasbutton(Canvas, Text, X, Y, W, H, Data) ->

    %% Draw a rectangle (for event catching)
    Rect = gs:create(rectangle, Canvas, [{coords, [{X,Y}, {X+W,Y+H}]},
					 {fill, gs:read(Canvas, bg)},
					 {buttonpress, true},
					 {buttonrelease, true}]),
    
    %% Make the rectangle area look like a 3D button by using lines
    Ul = gs:create(line, Canvas, [{coords, [{X,Y+H},{X,Y},{X+W,Y}]},
				  {fg, white}, {width, 2}]),
    Ll = gs:create(line, Canvas, [{coords, [{X,Y+H},{X+W,Y+H},{X+W,Y}]},
				  {fg, {87,87,87}}, {width, 2}]),

    %% Write the text in the middle
    Xc = X + round(W/2),
    Yc = Y + round(H/2),
    T = gs:create(text, Canvas, [{text, Text}, {coords, [{Xc,Yc}]},
				 {anchor, c},
				 {buttonpress, true},
				 {buttonrelease, true}]),

    %% Create the canvasbutton object
    CBtn = #canvasbutton{text=T, ul=Ul, ll=Ll, rect=Rect,
			 x=X, y=Y, w=W, h=H},

    %% Configure the data
    gs:config(T, {data, {canvasbutton, CBtn, Data}}),
    gs:config(Rect, {data, {canvasbutton, CBtn, Data}}),

    CBtn.

press(Canvasbutton) ->
    gs:config(Canvasbutton#canvasbutton.ul, {fg, {87,87,87}}),
    gs:config(Canvasbutton#canvasbutton.ll, {fg, white}).

release(Canvasbutton) ->
    gs:config(Canvasbutton#canvasbutton.ul, {fg, white}),
    gs:config(Canvasbutton#canvasbutton.ll, {fg, {87,87,87}}).
