%% ``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 via the world wide web 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.
%% 
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''
%% 
%%     $Id$
%%
-module(tool_utils).
-include_lib("kernel/include/file.hrl").

%%%---------------------------------------------------------------------
%%% Auxiliary functions to be used by the tools (internal module)
%%%---------------------------------------------------------------------

%% External exports
-export([open_help/2]).
-export([file_dialog/1]).
-export([notify/2, confirm/2, confirm_yesno/2, request/2]).

-record(state, {type,        % notify | confirm[_yesno] | request
		win,         % gsobj(), window
		entry,       % gsobj(), entry
		in_focus,    % 0 | 1 | undefined  Entry is in focus
		is_cursor,   % bool() | undefined  Cursor is over Entry
		buttons,     % [gsobj()], buttons
		highlighted  % int() highlighted buttone
	       }).


%% Browser executable list (openURL command line protocol required)
-define(BROWSERS, ["netscape", "mozilla", "MozillaFirebird", "opera"]).

%%----------------------------------------------------------------------
%% open_help(Parent, File)
%%   Parent = gsobj()  (GS root object or parent window)
%%   File = string() | nofile
%% View the help file File, which can be an URL, an HTML file or a text
%% file.
%% This function is OS dependant.
%% Unix: Assumes Netscape is up & running, and use Netscape remote
%%   commands to display the file.
%% NT: If File is a file, use the NT command 'start' which will open the
%%   default tool for viewing the file.
%%   If File is an URL, try to view it using Netscape.exe which 
%%   requires that the path Netscape.exe must be in TBD.
%%   (TEMPORARY solution..., can be done better)
%%----------------------------------------------------------------------
open_help(Parent, nofile) ->
    notify(Parent, "Sorry, no help information exists");
open_help(Parent, File) ->
    case application:get_env(kernel, browser_cmd) of
	undefined ->
	    open_help_default(Parent, File);
	{ok, Cmd} when is_list(Cmd) ->
	    spawn(os, cmd, [Cmd ++ " " ++ File]);
	{ok, {M, F, A}} ->
	    apply(M, F, [File|A]);
	_Other ->
	    Str = ["Bad Kernel configuration parameter browser_cmd",
		   "Do not know how to display help file"],
	    notify(Parent, Str)
    end.

open_help_default(Parent, File) ->
    Cmd = case file_type(File) of

	      %% Local file
	      local ->
		  case os:type() of
		      {unix,Type} ->
                          case Type of
                               darwin -> "open " ++ File;
                               _Else -> unix_url_command("file:" ++ File)
			  end;
		      {win32,_AnyType} ->
			  "start " ++ filename:nativename(File);

		      _Other ->
			  unknown
		  end;

	      %% URL
	      remote ->
		  case os:type() of
		      {unix,Type} ->
                          case Type of
                               darwin -> "open " ++ File;
                               _Else -> unix_url_command("file:" ++ File)
			  end;
		      {win32,_AnyType} ->
			  "netscape.exe -h " ++ regexp:gsub(File,"\\\\","/");
		      _Other ->
			  unknown
		  end;

	      Error -> % {error,Reason}
		  Error
	  end,

    if
	is_list(Cmd) ->
	    spawn(os, cmd, [Cmd]);
	Cmd==unknown ->
	    Str = ["Sorry, do not know how to",
		   "display HTML files at this platform"],
	    notify(Parent, Str);
	true ->
	    {error, Reason} = Cmd,
	    Str = file:format_error(Reason),
	    notify(Parent, [File,Str])
    end.

%% file_type(File) -> local | remote | {error,Reason}
%%   File = string()
%%   Reason - see file(3)
%% Returns local if File is an existing, readable file
%% Returns remote if File is a remote URL (ie begins with 'http:')
file_type(File) ->
    case File of
	"http://"++_URL ->
	    remote;
	_ ->
	    %% HTML files can have a tag (<name>.html#tag), this must be
	    %% removed when checking if the file exists
	    File2 = case filename:extension(File) of
			".html#"++_Index ->
			    filename:rootname(File)++".html";
			_ ->
			    File
		    end,

            case file:read_file_info(File2) of
	        {ok, FileInfo} when FileInfo#file_info.type==regular,
				    FileInfo#file_info.access/=none ->
		    local;
		{ok, FileInfo} when FileInfo#file_info.type/=regular ->
		    {error,einval};
		{ok, FileInfo} when FileInfo#file_info.access==none ->
		    {error,eacces};
		Error ->
		    Error
	    end
    end.


%%----------------------------------------------------------------------
%% file_dialog(Options) -> tbd
%%----------------------------------------------------------------------
file_dialog(Options) ->
    tool_file_dialog:start(Options).


%%----------------------------------------------------------------------
%% notify(Parent, Strings) -> ok
%% confirm(Parent, Strings) -> ok | cancel
%% confirm_yesno(Parent, Strings) -> yes | no | cancel
%% request(Parent, Strings) -> {ok,string()} | cancel
%%   Parent = gsobj()  (GS root object or parent window)
%%   Strings = string() | [string()]
%% Opens a window with the specified message (Strings) and locks the GUI
%% until the user confirms the message.
%% If the Parent argument is the parent window, the help window will be
%% centered above it, otherwise it can end up anywhere on the screen.
%% A 'notify' window contains an 'Ok' button.
%% A 'confirm' window contains an 'Ok' and a 'Cancel' button.
%% A 'confirm_yesno' window contains a 'Yes', a 'No', and a 'Cancel'
%% button.
%% A 'request' window contains an entry, an 'Ok' and a 'Cancel' button.
%%----------------------------------------------------------------------
-define(Wlbl, 130).
-define(Hlbl, 30).
-define(Hent, 30).
-define(Wbtn, 50).
-define(Hbtn, 30).
-define(PAD,  10).

notify(Parent, Strings) ->
     help_win(notify, Parent, Strings).
confirm(Parent, Strings) ->
    help_win(confirm, Parent, Strings).
confirm_yesno(Parent, Strings) ->
    help_win(confirm_yesno, Parent, Strings).
request(Parent, Strings) ->
    help_win(request, Parent, Strings).

help_win(Type, Parent, Strings) ->
    GenOpts = [{keypress,true}],
    GenOpts2 = [{font,{screen,12}} | GenOpts],
    Buttons = buttons(Type),
    Nbtn = length(Buttons),

    %% Create the window and its contents
    Win = gs:create(window, Parent, [{title,title(Type)} | GenOpts]),
    Top = gs:create(frame, Win, GenOpts),
    Lbl = gs:create(label, Top, [{align,c}, {justify,center}|GenOpts2]),
    Mid = if
	      Type==request -> gs:create(frame, Win, GenOpts);
	      true -> ignore
	  end,
    Ent = if
	      Type==request ->
		  Events = [{setfocus,true},
			    {focus,true},{enter,true},{leave,true}],
		  gs:create(entry, Mid, GenOpts2++Events);
	      true -> ignore
	  end,
    Bot = gs:create(frame, Win, GenOpts),

    %% Find out minimum size required for label, entry and buttons
    Font = gs:read(Parent, {choose_font, {screen,12}}),
    Text = insert_newlines(Strings),
    {Wlbl0,Hlbl0} = gs:read(Lbl, {font_wh,{Font,Text}}),
    {_Went0,Hent0} = gs:read(Lbl, {font_wh,{Font,"Entry"}}),
    {Wbtn0,Hbtn0} = gs:read(Lbl, {font_wh,{Font,"Cancel"}}),
    
    %% Compute size of the objects and adjust the graphics accordingly
    Wbtn = max(Wbtn0+10, ?Wbtn),
    Hbtn = max(Hbtn0+10, ?Hbtn),
    Hent = max(Hent0+10, ?Hent),
    Wlbl = max(Wlbl0, max(Nbtn*Wbtn+(Nbtn-1)*?PAD, ?Wlbl)),
    Hlbl = max(Hlbl0, ?Hlbl),

    Wwin = ?PAD+Wlbl+?PAD,

    Htop = ?PAD+Hlbl,
    Hmid = if Type==request -> ?PAD+Hent; true -> 0 end,
    Hbot = ?PAD+Hbtn+?PAD,
    Hwin = Htop+Hmid+Hbot,

    case catch get_coords(Parent, Wwin, Hwin) of
	{Xw, Yw} when is_integer(Xw), is_integer(Yw) ->
	    gs:config(Win, [{x,Xw}, {y,Yw}]);
	_ ->
	    ignore
    end,
	    
    gs:config(Win, [                       {width,Wwin},{height,Hwin}]),

    gs:config(Top, [{x,0},   {y,0},        {width,Wwin},{height,Htop}]),
    gs:config(Lbl, [{x,?PAD},{y,?PAD},     {width,Wlbl},{height,Hlbl}]),

    gs:config(Mid, [{x,0},   {y,Htop},     {width,Wwin},{height,Hmid}]),
    gs:config(Ent, [{x,?PAD},{y,?PAD},     {width,Wlbl},{height,Hent}]),

    gs:config(Bot, [{x,0},   {y,Htop+Hmid},{width,Wwin},{height,Hbot}]),

    %% Insert the label text
    gs:config(Lbl, {label,{text,Text}}),

    %% Add the buttons
    Xbtns = xbuttons(Buttons, Wbtn, Wwin, Wlbl),
    BtnObjs =
	lists:map(fun({Btext,BX}) ->
			  gs:create(button, Bot, [{x,BX-1}, {y,?PAD-1},
						  {width,Wbtn+2},
						  {height,Hbtn+2},
						  {label,{text,Btext}},
						  {data,data(Btext)}
						  | GenOpts2])
		  end,
		  Xbtns),
    Highlighted = highlight(undef, 1, BtnObjs),

    gs:config(Win, [{map,true}]),

    State = if
		Type==request ->
		    #state{in_focus=1, is_cursor=false};
		true ->
		    #state{}
	    end,
    event_loop(State#state{type=Type, win=Win, entry=Ent,
			   buttons=BtnObjs, highlighted=Highlighted}).

title(notify) ->        "Notification";
title(confirm) ->       "Confirmation";
title(confirm_yesno) -> "Confirmation";
title(request) ->       "Request".

buttons(notify) ->        ["Ok"];
buttons(confirm) ->       ["Ok", "Cancel"];
buttons(confirm_yesno) -> ["Yes", "No", "Cancel"];
buttons(request) ->       ["Ok", "Cancel"].

data("Ok") ->     {helpwin,ok};
data("Yes") ->    {helpwin,yes};
data("No") ->     {helpwin,no};
data("Cancel") -> {helpwin,cancel}.

max(X, Y) when X>Y -> X;
max(_X, Y) -> Y.

get_coords(Parent, W, H) ->
    case gs:read(Parent, x) of
	X when is_integer(X) ->
	    case gs:read(Parent, y) of
		Y when is_integer(Y) ->
		    case gs:read(Parent, width) of
			W0 when is_integer(W0) ->
			    case gs:read(Parent, height) of
				H0 when is_integer(H0) ->
				    {round((X+W0/2)-W/2),
				     round((Y+H0/2)-H/2)};
				_ -> error
			    end;
			_ -> error
		    end;
		_ -> error
	    end;
	_ -> error
    end.

xbuttons([B], Wbtn, Wwin, _Wlbl) ->
    [{B, round(Wwin/2-Wbtn/2)}];
xbuttons([B1,B2], Wbtn, Wwin, Wlbl) ->
    Margin = (Wwin-Wlbl)/2,
    [{B1,round(Margin)}, {B2,round(Wwin-Margin-Wbtn)}];
xbuttons([B1,B2,B3], Wbtn, Wwin, Wlbl) ->
    Margin = (Wwin-Wlbl)/2,
    [{B1,round(Margin)},
     {B2,round(Wwin/2-Wbtn/2)},
     {B3,round(Wwin-Margin-Wbtn)}].

highlight(Prev, New, BtnObjs) when New>0, New=<length(BtnObjs) ->
    if
	Prev==undef -> ignore;
	true ->
	    gs:config(lists:nth(Prev, BtnObjs), [{highlightbw,0}])
    end,
    gs:config(lists:nth(New, BtnObjs), [{highlightbw,1},
					{highlightbg,black}]),
    New;
highlight(Prev, _New, _BtnObjs) -> % New is outside allowed range
    Prev.

event_loop(State) ->
    receive
	GsEvent when element(1, GsEvent)==gs ->
	    case handle_event(GsEvent, State) of
		{continue, NewState} ->
		    event_loop(NewState);

		{return, Result} ->
		    gs:destroy(State#state.win),
		    Result
	    end
    end.

handle_event({gs,_,click,{helpwin,Result},_}, State) ->
    if
	State#state.type/=request; Result==cancel ->
	    {return, Result};

	State#state.type==request, Result==ok ->
	    case gs:read(State#state.entry, text) of
		"" ->
		    {continue, State};
		Info ->
		    {return, {ok, Info}}
	    end
    end;

%% When the entry (Type==request) is in focus and the mouse pointer is
%% over it, don't let 'Left'|'Right' keypresses affect which button is
%% selected
handle_event({gs,Ent,enter,_,_}, #state{entry=Ent}=State) ->
    {continue, State#state{is_cursor=true}};
handle_event({gs,Ent,leave,_,_}, #state{entry=Ent}=State) ->
    {continue, State#state{is_cursor=false}};
handle_event({gs,Ent,focus,_,[Int|_]}, #state{entry=Ent}=State) ->
    {continue, State#state{in_focus=Int}};

handle_event({gs,Win,keypress,_,['Right'|_]}, #state{win=Win}=State) ->
    if
	State#state.type==request,
	State#state.in_focus==1, State#state.is_cursor==true ->
	    {continue, State};
	true ->
	    Prev = State#state.highlighted,
	    New = highlight(Prev, Prev+1, State#state.buttons),
	    {continue, State#state{highlighted=New}}
    end;
handle_event({gs,Win,keypress,_,['Left'|_]}, #state{win=Win}=State) ->
    if
	State#state.type==request,
	State#state.in_focus==1, State#state.is_cursor==true ->
	    {continue, State};
	true ->
	    Prev = State#state.highlighted,
	    New = highlight(Prev, Prev-1, State#state.buttons),
	    {continue, State#state{highlighted=New}}
    end;

handle_event({gs,Ent,keypress,_,['Tab'|_]}, #state{entry=Ent}=State) ->
    gs:config(hd(State#state.buttons), {setfocus,true}),
    gs:config(Ent, {select,clear}),
    {continue, State#state{in_focus=0}};

handle_event({gs,Win,keypress,_,['Return'|_]}, #state{win=Win}=State) ->
    Selected = lists:nth(State#state.highlighted, State#state.buttons),
    Data = gs:read(Selected, data),
    handle_event({gs,Win,click,Data,undef}, State);

handle_event({gs,Win,destroy,_,_}, #state{win=Win}=State) ->
    if
	State#state.type==notify -> {return, ok};
	true -> {return, cancel}
    end;

%% Flush any other GS events
handle_event({gs,_Obj,_Event,_Data,_Arg}, State) ->
    {continue, State}.

%% insert_newlines(Strings) => string()
%%   Strings - string() | [string()]
%% If Strings is a list of strings, return a string where all these
%% strings are concatenated with newlines in between,otherwise return
%% Strings.
insert_newlines([String|Rest]) when is_list(String),Rest/=[]->
    String ++ "\n" ++ insert_newlines(Rest);
insert_newlines([Last]) ->
    [Last];
insert_newlines(Other) ->
    Other.

%% find_browser(BrowserList) => string() | false
%%   BrowserList - [string()]
%% Given a list of basenames, find the first available executable.

find_browser([]) ->
    false;

find_browser([H | T]) ->
    case os:find_executable(H) of
        false ->
          find_browser(T);
        Browser ->
          Browser
    end.

%% unix_url_command(URL) => string()
%%   URL - string()
%% Open an URL, using a browser which supports the openURL command
%% line protocol. If no browser is found, the empty string will be
%% returned.

unix_url_command(URL) ->
    Template = "BROWSER -remote \"openURL(" ++ URL ++ ")\" || BROWSER " ++ URL ++ "&",

    case os:getenv("BROWSER") of
	false ->
	    %% look for a compatible browser
	    case find_browser(?BROWSERS) of
		false ->
		    "";
		Browser ->
		    case regexp:gsub(Template, "BROWSER", Browser) of
			{ok, Command, 0} ->
			    %% Template does not contain "BROWSER" placeholder
			    "";
			{ok, Command, _} ->
			    Command
		    end
	    end;

	Value ->
	    case regexp:gsub(Template, "BROWSER", Value) of
		{ok, Command2, 0} ->
		    %% no placeholder
		    "";
		{ok, Command2, _} ->
		    Command2
	    end
    end.
