%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1998-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%

%%% Due to the fact that the application buttons in the appmon window
%%% gets too small to read when the number of applications increases,
%%% this listbox window has been created. 
%%% Because of the limitations of GS a listbox was chosen to keep
%%% the nodes and applications. When it's possible to scroll a frame I
%%% think one should put in scrollbars in the appmon main window.
%%% The listbox solution is too slow with lots of applications.
%%%
%%% In the listbox the nodes are shown with their applications beneith.
%%% By double clicking on an application name, or a single click and
%%% then pressing the load button, its application window is started.

-module(appmon_lb).
-compile([{nowarn_deprecated_function,{gs,button,3}},
          {nowarn_deprecated_function,{gs,config,2}},
          {nowarn_deprecated_function,{gs,destroy,1}},
          {nowarn_deprecated_function,{gs,listbox,3}},
          {nowarn_deprecated_function,{gs,read,2}},
          {nowarn_deprecated_function,{gs,start,0}},
          {nowarn_deprecated_function,{gs,window,3}}]).

-export ([
	  start/1, 
	  stop/1, 
	  add_node/2,
	  remove_node/2,
	  add_apps/3,
	  remove_app/3,
	  open_win/2,
	  update_status/3
	 ]).

-export ([init/1]).

-define (LB_W, 200).                     % List box width
-define (LB_H, 400).                     % List box height
-define (BUTT_W, 100).
-define (WIN_W, ?LB_W + ?BUTT_W + 25).   % Window width
-define (WIN_H, ?LB_H + 20).             % Window height


%%% #node{} 
%%%
%%% The record 'node' contains the name of the node, its status and 
%%% the applications running on that node.
%%%
%%% node    ==  atom ()
%%% status  ==  alive  ||  dead
%%% apps    ==  [#app{}]
%%%

-record (node, {node,             %% Name of the node
		status = alive,
		apps = []}). 

%%% #app{}
%%%
%%% The record 'app' contains the name of the application and its pid
%%%
%%% app  ==  atom ()
%%% pid  ==  pid ()
%%%

-record (app, {app,
		pid}).


%%% #win{}
%%%
%%% The record 'win' contains the pid of the listbox window,
%%% its x and y position, its width and height.
%%%
%%% pid     ==  win_closed  ||  pid ()
%%% x       ==  integer ()
%%% y       ==  integer ()
%%% width   ==  integer ()
%%% height  ==  integer ()
%%%

-record (win, {pid = win_closed,
	       x = 50, 
	       y = 50,
	       width = ?WIN_W,
	       height = ?WIN_H}).



%%% Every function in the interface is called with the pid 
%%% of this recieve loop, called 'LbPid'.
%%%


%%% start  /1
%%%
%%% start returns the pid of the spawned receive loop or 
%%% it will call exit/2 after a timeout.
%%%
%%% Pre:
%%%    CallingPid  ==  pid ()
%%%
%%% Def:
%%%    pid ()  ||  exit/2
%%%

start (CallingPid) ->
    PidInit = spawn (?MODULE, init, [CallingPid]),

    %% Wait for a initialization completion message from
    %% the spawned process before returning its Pid.

    receive
	{initialization_complete, PidInit} ->
	    PidInit
    
	    %% (Conditional) Failure to start within the time limit 
	    %% will result in termination (Timeout may be infinite).

    after
	60000 ->
	    exit (PidInit, kill),
	    exit ({startup_timeout, ?MODULE})
    end.



%%% stop  /1
%%%
%%% stop exits the receive loop
%%%
%%% Post:
%%%    exiting the receive loop
%%%

stop (LbPid) ->
    call (LbPid, stop).



%%% add_node  /2
%%%
%%% add_node adds the given node to the DB list.
%%%
%%% Pre:
%%%    Node  ==  atom ()
%%%
%%% Post:
%%%    Node is added to the DB list
%%%

add_node (LbPid, Node) ->
    call (LbPid, {add_node, Node}).



%%% remove_node  /2
%%%
%%% remove_node removes the given node from the DB list.
%%%
%%% Pre:
%%%    Node  ==  atom ()
%%%
%%% Post:
%%%    Node is removed from the DB list
%%%

remove_node (LbPid, Node) ->
    call (LbPid, {remove_node, Node}).



%%% add_apps  /3
%%%
%%% add_apps add the given applications to the given 
%%% node in the DB list.
%%%
%%% Pre:
%%%    Apps  ==  [App]
%%%    App   ==  {Name, Pid}
%%%    Name  ==  atom ()
%%%    Pid   ==  pid ()
%%%    Node  ==  atom ()
%%%
%%% Post:
%%%    Node#node{apps = Apps}
%%%

add_apps (LbPid, Apps, Node) ->
    call (LbPid, {add_apps, Apps, Node}).



%%% remove_app  /3
%%%
%%% remove_app remove the given application from the 
%%% given node in the DB list.
%%%
%%% Pre:
%%%    App   ==  atom ()
%%%    Node  ==  atom ()
%%%
%%% Def:
%%%    Node#node{apps = OldApps - App}
%%%

remove_app (LbPid, App, Node) ->
    call (LbPid, {remove_app, App, Node}).



%%% open_win  /3
%%%
%%% open_win opens the listbox window with the given nodes
%%% and their applications.
%%%
%%% Pre:
%%%    Nodes_apps  ==  [{Node, Status, Apps}]
%%%    Node        ==  atom ()
%%%    Status      ==  alive  ||  dead
%%%    Apps        ==  [App]
%%%    App         ==  {AppName, AppPid}
%%%    AppName     ==  atom ()
%%%    AppPid      ==  pid ()
%%%
%%% Post:
%%%    Window with listbox
%%%

open_win (LbPid, Nodes_apps) ->
    call (LbPid, {open_win, Nodes_apps}).



%%% update_status  /3
%%%
%%% update_status changes the status for the given node.
%%%
%%% Pre:
%%%    Node    ==  atom ()
%%%    Status  ==  alive  ||  dead
%%%
%%% Def:
%%%    Node#node{status = Status}
%%%

update_status (LbPid, Node, Status) ->
    call (LbPid, {update_status, Node, Status}).



%%% call  /2
%%%
%%% call sends the given action to the listbox receive loop.
%%%
%%% Pre: 
%%%    Action  ==  atom ()  ||  tuple ()
%%%

call (LbPid, Action) ->
    LbPid ! Action. 
 


%%% init  /1
%%%

init (CallingPid) ->
    CallingPid ! {initialization_complete, self ()},
    loop (#win{}, []).



%%% loop  /2
%%%
%%% loop is the recive loop for the listbox window process.
%%%
%%% Pre:
%%%    Win   ==  #win{}
%%%    Data  ==  [#node{}]
%%%

loop (Win, Data) ->
    receive
	{add_node, Node} ->
	    NewData = add_node_1 (Node, Data),
	    update (NewData, Win#win.pid),
	    loop (Win, NewData);
	
	{remove_node, Node} ->
	    NewData = dead_node (Node, Data),
	    update (NewData, Win#win.pid),
	    loop (Win, NewData);

	{add_apps, Apps, Node} ->
	    NewData = add_apps_1 (Apps, Node, Data),
	    update (NewData, Win#win.pid),
	    loop (Win, NewData);
	
	{remove_app, App, Node} ->
	    NewData = remove_app_1 (App, Node, Data),
	    update (NewData, Win#win.pid),
	    loop (Win, NewData);
	
	{open_win, Nodes_apps} ->
	    NewData = parse_data ([], Nodes_apps),
	    NewWin = Win#win{pid = init_win ({Win#win.x, Win#win.y})},
	    update (NewData, NewWin#win.pid),
	    loop (NewWin, NewData);
	    
	{update_status, Node, Status} ->
	    NewData = update_status_1 (Node, Status, Data),
	    update (NewData, Win#win.pid),
	    loop (Win, NewData);
	
	stop ->
	    true;


	{gs, _Id, destroy, _D, _Arg} -> 
	    bye;

	{gs, _Id, configure, _D, [W, H | _]} ->
	    NewWin = configure (Win#win.pid, W, H),
	    loop (NewWin, Data);

	{gs, lb, doubleclick, _, _Txt} ->
	    load_app (gs:read (lb, selection), Data),
	    loop (Win, Data);
	    
	{gs, lb, click, _, _Txt} ->
	    loop (Win, Data);

	{gs, close, click, _D, _Arg} -> 
	    case Win#win.pid of
		win_closed ->
		    true;
		
		_opened ->
		    gs:destroy (Win#win.pid)
	    end,
	    loop (#win{}, Data);

	{gs, load, click, _D, _Txt} ->
	    load_app (gs:read (lb, selection), Data),
	    loop (Win, Data);
	
	{gs, clear, click, _D, _Txt} ->
	    gs:config (lb, {selection, clear}),
	    loop (Win, Data);
	

	_ ->
	    loop (Win, Data)
    end.



%%% init_win  /1
%%%

init_win ({X, Y}) ->
    GS = gs:start (),

    Win = gs:window (win, GS, [{x, X},
			       {y, Y},
			       {width, ?WIN_W},
			       {height, ?WIN_H},
			       {title,"Appmon: nodes and applications"},
			       {configure, true}]),

    gs:listbox (lb, Win, [{x, 5},
			  {y, 10},
			  {width, ?LB_W},
			  {height, ?LB_H},
			  {vscroll, right},
			  {hscroll, bottom},
			  {selectmode, single},
			  {click, true},
			  {doubleclick, true}]),

    gs:button (load, Win, [{x, ?WIN_W - ?BUTT_W - 10},
			    {y, ?WIN_H - 120},
			    {width, ?BUTT_W},
			    {label, {text, "Load"}}]),

    gs:button (clear, Win, [{x, ?WIN_W - ?BUTT_W - 10},
			    {y, ?WIN_H - 80},
			    {width, ?BUTT_W},
			    {label, {text, "Clear"}}]),

    gs:button (close, Win, [{x, ?WIN_W - ?BUTT_W - 10},
			   {y, ?WIN_H - 40},
			   {width, ?BUTT_W},
			   {label, {text, "Close"}}]),

    gs:config (Win, {map, true}),
    Win.


			  			 
%%% add_node_1  /2
%%%
%%% add_node adds the given node in the given window 
%%% with its appications in a listbox.
%%%

add_node_1 (Node, []) ->
    [new_node (Node)];

add_node_1 (Node, [H | T]) ->
    T1 = lists:keysort (#node.node, [new_node (Node) | T]),
    [H | T1].



%%% dead_node  /2
%%%
%%% dead_node returns a list with the given node's 
%%% status changed to dead.
%%%

dead_node (Node, Data) ->
    case lists:keysearch (Node, #node.node, Data) of
	{value, Node_rec} ->
	    L = Node_rec#node.apps,
	    lists:keyreplace (Node, #node.node, 
			      Data, new_node (Node, dead, L));
	
	_false ->
	    Data
    end.

    



%%% add_apps_1  /3
%%%
%%% add_apps_1 returns a list with the given application 
%%% into the old list inserted.
%%%

add_apps_1 (Apps, Node, Data) ->
    case lists:keysearch (Node, #node.node, Data) of
	{value, _Node_rec} ->
	    NewApps = parse_apps (Apps, []),
	    lists:keyreplace (Node, #node.node, 
			      Data, new_node (Node, NewApps));
	
	_false ->
	    Data
    end.



%%% remove_app_1  /3
%%%
%%% remove_app_1 returns a list with the given application
%%% removed from the old list.
%%%

remove_app_1 (App, Node, Data) ->
    
    case lists:keysearch (Node, #node.node, Data) of
	{value, Node_rec} ->
	    L = Node_rec#node.apps,
	    L2 = lists:keydelete (App, #app.app, L),
	    lists:keyreplace(Node, #node.node, Data, new_node(Node,L2));
	
	_false ->
	    Data
    end.



%%% configure  /3
%%%
%%% configure returns a win record after the window has been
%%% configured.
%%%

configure (WPid, W, H) ->
    X = gs:read (WPid, x),
    Y = gs:read (WPid, y),

    gs:config (lb, [{width, W - ?BUTT_W - 25}, {height, H - 20}]),
    gs:config (load, [{x, W - ?BUTT_W - 10}, {y, H - 120}]),
    gs:config (clear, [{x, W - ?BUTT_W - 10}, {y, H - 80}]),
    gs:config (close, [{x, W - ?BUTT_W - 10}, {y, H - 40}]),
    
    #win{pid = WPid, x = X, y = Y, width = W, height = H}.

    



%%% load_app  /2
%%%
%%% load_app opens the application window by calling 
%%% the appmon_a module.
%%%

load_app ([], _Data) ->     %% no application chosen
    ok;

load_app ([Index], Data) ->
    App = gs:read (lb, {get, Index}),
    
    case string:substr (App, 1, 3) of
	"   " ->
	    AppName = list_to_atom (string:substr (App, 4)),
	    
	    case get_node (AppName, Index, Data) of
		no_node ->
		    ok;

		NodeName ->
		    appmon_a:start (NodeName, AppName)
	    end;

	_ ->
	    ok
    end.



%%% update_status_1  /3
%%%
%%% update_status_1 returns a list with the given 
%%% node's status updated.
%%%

update_status_1 (Node, Status, Data) ->
    case lists:keysearch (Node, #node.node, Data) of
	{value, Node_rec} ->
	    lists:keyreplace (Node, 
			      #node.node, 
			      Data, 
			      new_node(Node,Status,Node_rec#node.apps));

	_not_found ->
	    Data
    end.



%%% update  /2
%%%
%%% update updates the listbox with new data.
%%%

update (_Data, win_closed) ->
    true;

update (Data, _Win) ->
    gs:config (lb, clear),
    lb_print (Data).



%%% lb_print  /1
%%%
%%% lb_print prints the list into the listbox.
%%%

lb_print ([]) ->
    ok;

lb_print ([#node{node = Node, status = Status, apps = Apps} | T]) ->
    Str = io_lib:format ("~p (~p)", [Node, Status]),
    gs:config (lb, {add, Str}),

    case Status of
	alive ->
	    lb_print_apps (Apps);

	_dead ->
	    gs:config (lb, {add, ""}),
	    ok
    end,

    lb_print (T).



%%% lb_print_apps  /1
%%%
%%% lb_print_apps prints the applications into the listbox.
%%%

lb_print_apps ([]) ->
    ok;

lb_print_apps ([#app{app = App} | T]) ->
    Str = io_lib:format ("   ~p", [App]),
    gs:config (lb, {add, Str}),
    lb_print_apps (T).



%%% new_node  /1, 2, 3
%%%
%%% new_node returna a new node record constructed 
%%% with the given data
%%%

new_node (Node) ->
    #node{node = Node}.

new_node (Node, Apps) ->
    #node{node = Node, apps = Apps}.

new_node (Node, Status, Apps) ->
    #node{node = Node, status = Status, apps = Apps}.



%%% new_app  /2
%%%
%%% new_app returns a new application record 
%%% constructed with the given data.
%%%

new_app (App, Pid) ->
    #app{app = App, pid = Pid}.



%%% parse_apps  /2
%%%
%%% parse_apps returns a list of application records.
%%%

parse_apps ([], [H | T]) ->
    [H | lists:keysort (#app.app, T)];

parse_apps ([App | T], L) ->
    Pid = element (1, App),
    Name = element (2, App),
    parse_apps (T, [new_app (Name, Pid) | L]). 



%%% get_node  /3
%%%
%%% get_node returns the node from the given list 
%%% or else no_node if it doesn't exists.
%%%

get_node (_App, _Index, []) ->
    no_node;

get_node (App, Index, [Node | T]) ->
    Length = length (Node#node.apps) + 1,

    case Length < Index of
	true ->
	    get_node (App, Index - Length, T);

	false ->
	    Node#node.node
    end.



%%% parse_data  /2
%%%
%%% parse_data returns a list with node records.
%%%

parse_data (Data, []) ->
    Data;

parse_data (Data, [{Node, Status, Apps} | T]) ->
    Apps_1 = parse_apps (Apps, []),
    parse_data ([new_node (Node, Status, Apps_1) | Data], T).

    

