%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1997-2009. 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(tool_file_dialog).
-export([start/1]).

-record(opts, {type,          % open | save | multiselect
	       dir,           % string()  Current directory
	       file,          % string()  Filename, no path
	       extensions,    % [string()]  Filtered file extensions
	       hidden}).      % [{Dir, [File]}]  Hidden files per dir.

-define(WIDTH,  250).
-define(HEIGHT, 400).
-define(BTNW,   65).
-define(BTNH,   30).

%% start(Opts) -> {ok, AbsFile, Dir} | {error,cancel} | pid()
%%   Opts = [Opt]
%%     Opt = {type, open|save|multiselect}
%%         | {extensions, [string()]}  % For example ".erl"
%%         | {dir, string()}           % Absolute path
%%         ! {file, string()           % Filename (no path)
%%   AbsFile = string()
%%   Dir = string()
%% An open/save dialog returns {ok, AbsFile, Dir} or {error,cancel}
%% (the latter, ridiculous, return value is kept for backwards
%%  compatibility reasons only).
%%
%% A multiselect box returns a pid and delivers messages on the form:
%%   {select, AbsFile} | {close, Dir}
%%
%% Dir is the current directory displayed and can be used to start a
%% a new filedialog with the same directory.

start(Opts0) ->
    Opts = parse_opts(Opts0),
    Self = self(),
    case Opts#opts.type of
	multiselect ->
	    spawn_link(fun() -> init(Self, Opts) end);
	_Type -> % open | save
	    spawn_link(fun() -> init(Self, Opts) end),
	    receive
		{fd_result, Res} ->
		    Res
	    end
    end.

parse_opts(Opts) ->
    {ok, CWD} = file:get_cwd(),
    DefOpts = #opts{type=open, dir=CWD, file="NoName",
		    extensions=[], hidden=[]},
    parse_opts(Opts, DefOpts).

parse_opts([{type, Type}|Opts], DefOpts) ->
    if
	Type==open; Type==save; Type==multiselect ->
	    parse_opts(Opts, DefOpts#opts{type=Type});
	true ->
	    erlang:error(badarg, [{type,Type}])
    end;
parse_opts([{extensions, Exts}|Opts], DefOpts) ->
    case lists:all(fun(Ext) -> is_list(Ext) end, Exts) of
	true ->
	    parse_opts(Opts, DefOpts#opts{extensions=Exts});
	false ->
	    erlang:error(badarg, [{extension, Exts}])
    end;
parse_opts([{dir, Dir}|Opts], DefOpts) ->
    case filelib:is_dir(Dir) of
	true ->
	    case filename:pathtype(Dir) of
		absolute ->
		    parse_opts(Opts, DefOpts#opts{dir=Dir});
		_ ->
		    parse_opts(Opts,
			       DefOpts#opts{dir=filename:absname(Dir)})
	    end;
	false ->
	    erlang:error(badarg, [{dir, Dir}])
    end;
parse_opts([{file, Name}|Opts], DefOpts) ->
    if
	is_list(Name) ->
	    parse_opts(Opts, DefOpts#opts{file=Name});
	true ->
	    erlang:error(badarg, [{file, Name}])
    end;
parse_opts([_|Opts], DefOpts) -> % ignore unknown options
    parse_opts(Opts, DefOpts);
parse_opts([], DefOpts) ->
    DefOpts.

%%--Loop----------------------------------------------------------------

init(From, Opts) ->
    make_window(Opts),
    loop(From, {?WIDTH,?HEIGHT}, Opts).

loop(From, {OldW,OldH}=Size, Opts) ->
    receive

	%% Window is closed
	{gs, win, destroy, _, _} when Opts#opts.type==multiselect ->
	    From ! {close, Opts#opts.dir};
	{gs, win, destroy, _, _} ->
	    From ! {fd_result, {error, cancel}};

	%% Window is moved or resized
	{gs, win, configure, _, [OldW,OldH|_]} ->
	    loop(From, Size, Opts);
	{gs, win, configure, _, [W,H|_]} ->
	    gs:config(resizer, [{width,W},{height,H}]),
	    loop(From, {W,H}, Opts);

	%% Up button is selected
	{gs, up, click, _, _} ->
	    Opts2 = set_dir(up, Opts),
	    loop(From, Size, Opts2);

	%% A listbox item (dir or file) is selected
	{gs, lb, click, _, [_I,Item|_]} ->
	    Entry = case lists:last(Item) of
			$/ -> "";
			_Ch -> Item
		    end,
	    gs:config(entry, {text,Entry}),
	    loop(From, Size, Opts);

	%% A listbox item (dir or file) is double-clicked
	{gs, lb, doubleclick, _, [_I,Item|_]} ->
	    case lists:last(Item) of
		$/ ->  do_select({dir, Item}, From, Size, Opts);
		_Ch -> do_select({file, Item}, From, Size, Opts)
	    end;

	%% Open/Save/Select button is selected
	{gs, select, click, _, _} ->
	    case gs:read(entry, text) of
		"" ->
		    case gs:read(lb, selection) of
			[] ->
			    gs:config(select, beep),
			    loop(From, Size, Opts);
			[I] ->
			    Item = gs:read(lb, {get,I}),
			    case lists:last(Item) of
				$/ ->
				    do_select({dir, Item},
					      From, Size, Opts);
				_Ch ->
				    do_select({file, Item},
					      From, Size, Opts)
			    end
		    end;
		Item -> do_select(Item, From, Size, Opts)
	    end;
			   
	%% 'Return' is pressed
	{gs, entry, keypress, _, ['Return'|_]} ->
	    case gs:read(entry, text) of
		"" ->
		    gs:config(select, beep),
		    loop(From, Size, Opts);
		Item ->
		    do_select(Item, From, Size, Opts)
	    end;

	%% All button is selected (multiselect dialog)
	{gs, all, click, _, _} ->
	    {_Dirs, Files} = select_all(),
	    lists:foreach(fun(File) ->
				  AbsFile = filename:join(Opts#opts.dir,
							  File),
				  From ! {select, AbsFile}
			  end,
			  Files),
	    From ! {close, Opts#opts.dir};

	%% Cancel button is selected (open/save dialog)
	{gs, cancel, click, _, _} ->
	    From ! {fd_result, {error, cancel}};

	%% Close button is selected (multiselect dialog)
	{gs, close, click, _, _} ->
	    From ! {close, Opts#opts.dir};

	Msg ->
	    io:format("GOT: ~p~n", [Msg]),
	    loop(From, Size, Opts)
    end.

do_select({dir, Name}, From, Size, Opts) ->
    do_select_dir(filename:join(Opts#opts.dir, Name), From, Size, Opts);
do_select({file, Name}, From, Size, Opts) ->
    do_select_file(filename:join(Opts#opts.dir, Name), From, Size,Opts);
do_select(Entry, From, Size, Opts) ->
    AbsName = case filename:pathtype(Entry) of
		  absolute -> Entry;
		  _ -> filename:join(Opts#opts.dir, Entry)
	      end,
    case filelib:is_dir(AbsName) of
	true -> do_select_dir(AbsName, From, Size, Opts);
	false -> do_select_file(AbsName, From, Size, Opts)
    end.
	    
do_select_dir(Dir, From, Size, Opts) ->
    Opts2 = set_dir(Dir, Opts),
    loop(From, Size, Opts2).

do_select_file(File, From, Size, Opts) ->
    case filelib:is_file(File) of
	true when Opts#opts.type==multiselect ->
	    From ! {select, File},
	    Opts2 = update(File, Opts),
	    loop(From, Size, Opts2);
	true -> % open | save
	    From ! {fd_result, {ok, File, Opts#opts.dir}};
	false when Opts#opts.type==save ->
	    case filelib:is_dir(filename:dirname(File)) of
		true ->
		    From ! {fd_result, {ok, File, Opts#opts.dir}};
		false ->
		    gs:config(select, beep),
		    loop(From, Size, Opts)
	    end;
	false -> % multiselect | open
	    gs:config(select, beep),
	    loop(From, Size, Opts)
    end.

%%--Common GUI functions------------------------------------------------

-define(UPW, 35).
-define(UPH, 30).
-define(ENTRYH, 30).

make_window(Opts) ->
    GS = gs:start(),

    Title = case Opts#opts.type of
		open -> "Open File";
		save -> "Save File";
		multiselect -> "Select Files"
	    end,

    Font = case gs:read(GS, {choose_font,{screen,[],12}}) of
	       Font0 when element(1, Font0)==screen ->
		   Font0;
	       _ ->
		   gs:read(GS, {choose_font,{courier,[],12}})
	   end,

    gs:window(win, GS, [{title,Title},
			{width,?WIDTH}, {height,?HEIGHT},
			{configure,true}]),

    Marg = {fixed,5},
    Parent = gs:frame(resizer, win, [{packer_x,[Marg,{stretch,1},Marg]},
				     {packer_y,[Marg,
						{stretch,10},
						{stretch,1,2*?BTNH},
						Marg]}]),
    gs:frame(btnframe, resizer, [{packer_x, [{stretch,1},
					     {fixed,?BTNW},
					     {stretch,1},
					     {fixed,?BTNW},
					     {stretch,1},
					     {fixed,?BTNW},
					     {stretch,1}]},
				 {packer_y, [{stretch,1},
					     {fixed,?BTNH},
					     {stretch,1}]},
				 {pack_x,2}, {pack_y,3}]),

    gs:frame(frame, Parent, [{packer_x,[{fixed,?UPW},{stretch,1}]},
			     {packer_y,[{fixed,?UPH},{fixed,?ENTRYH},
					{stretch,1}]},
			     {pack_x,2}, {pack_y,2}]),

    Fup = filename:join([code:priv_dir(gs),"bitmap","fup.bm"]),
    gs:button(up, frame, [{label,{image, Fup}},
			  {pack_x,1}, {pack_y,1}]),
    gs:label(infodir, frame, [{label,{text," Dir:"}}, {font,Font},
			      {pack_x,2}, {pack_y,1}, {align,w}]),
    gs:label(l1, frame, [{label,{text,"File:"}}, {font,Font}, {align,e},
			 {pack_x,1}, {pack_y,2}]),

    gs:entry(entry, frame, [{font,Font}, {keypress,true},
			    {pack_x,2}, {pack_y,2}]),
    gs:listbox(lb, frame, [{font,Font}, {pack_x,{1,2}}, {pack_y,3},
			   {selectmode,single},
			   {vscroll,right},
			   {click,true}, {doubleclick,true}]),

    set_dir(Opts#opts.dir, Opts),

    case Opts#opts.type of
	multiselect ->
	    gs:button(select, btnframe, [{label,{text,"Select"}},
					 {font,Font},
					 {pack_x,2}, {pack_y,2}]),
	    gs:button(all, btnframe, [{label,{text,"All"}}, {font,Font},
				      {pack_x,4}, {pack_y,2}]),
	    gs:button(close,btnframe,[{label,{text,"Done"}},
				      {font,Font},
				      {pack_x,6}, {pack_y,2}]);
	Type ->
	    Text = case Type of
		       open -> "Open";
		       save -> "Save"
		   end,
	    gs:button(select, btnframe, [{label,{text,Text}},
					 {font,Font},
					 {pack_x,2}, {pack_y,2}]),
	    gs:button(cancel, btnframe, [{label,{text,"Cancel"}},
					 {font,Font},
					 {pack_x,6}, {pack_y,2}])
    end,

    gs:config(resizer, [{width,?WIDTH}, {height,?HEIGHT}]),
    gs:config(win, {map,true}).

%% update(AbsFile, Opts) -> Opts'
update(AbsFile, Opts) ->
    Dir = filename:dirname(AbsFile),
    File = filename:basename(AbsFile),

    %% Hide the file
    Hidden0 = Opts#opts.hidden,
    Hidden = case lists:keysearch(Dir, 1, Hidden0) of
		 {value, {_Dir, Files}} ->
		     lists:keyreplace(Dir, 1, Hidden0,
				      {Dir, [File|Files]});
		 false ->
		     [{Dir, [File]} | Hidden0]
	     end,
    Opts2 = Opts#opts{hidden=Hidden},
    set_dir(Dir, Opts2).

%% select_all() -> {Dirs, Files}
select_all() ->
    Is = lists:seq(0, gs:read(lb, size)-1),
    sort_selected(Is, [], []).

sort_selected([I|Is], Dirs, Files) ->
    FileOrDir = gs:read(lb, {get,I}),
    case lists:last(FileOrDir) of
	$/ ->
	    sort_selected(Is, [drop_last(FileOrDir)|Dirs], Files);
	_Ch ->
	    sort_selected(Is, Dirs, [FileOrDir|Files])
    end;
sort_selected([], Dirs, Files) ->
    {Dirs, Files}.

drop_last(Str) ->
    lists:sublist(Str, length(Str)-1).

%% set_dir(Dir0, Opts) -> Opts'
%%   Dir0 = up | string() absolute path only
set_dir(Dir0, Opts) ->
    Dir = if
	      Dir0==up -> filename:dirname(Opts#opts.dir);
	      true ->Dir0
	  end,

    case filelib:is_dir(Dir) of
	true ->
	    gs:config(frame, {cursor,busy}),
	    gs:config(lb, clear),
	    Items = get_files(Dir, Opts#opts.hidden,
			      Opts#opts.extensions),
	    case Opts#opts.type of
		save ->
		    gs:config(entry, {text,Opts#opts.file});
		_ ->
		    gs:config(entry, {text,""})
	    end,
	    gs:config(lb, [{items,Items}]),
	    gs:config(lb, {selection, clear}),
	    gs:config(infodir, {label,{text,["Dir: "|Dir]}}),
	    gs:config(frame, {cursor,parent}),
	    Opts#opts{dir=Dir};
	false ->
	    gs:config(select, beep),
	    Opts
    end.

get_files(Dir, Hidden, Exts) ->
    {ok, Items0} = file:list_dir(Dir),
    
    Items = case lists:keysearch(Dir, 1, Hidden) of
		{value, {_Dir, HiddenHere}} ->
		    lists:filter(fun(Item0) ->
					 not lists:member(Item0,
							  HiddenHere)
				 end,
				 Items0);
		false ->
		    Items0
	    end,

    get_files(Dir, Items, [], [], Exts).

get_files(Dir, [Item0|Items], Dirs, Files, Exts) ->
    Item = filename:join(Dir, Item0),
    case filelib:is_dir(Item) of
	true ->
	    get_files(Dir, Items, [Item0++"/"|Dirs], Files, Exts);
	false ->
	    case filelib:is_regular(Item) of
		true when Exts==[] ->
		    get_files(Dir, Items, Dirs, [Item0|Files], Exts);
		true ->
		    case lists:member(filename:extension(Item), Exts) of
			true ->
			    get_files(Dir,Items,Dirs,[Item0|Files],Exts);
			false ->
			    get_files(Dir, Items, Dirs, Files, Exts)
		    end;
		false ->
		    get_files(Dir, Items, Dirs, Files, Exts)
	    end
    end;
get_files(_Dir, [], Dirs, Files, _Exts) ->
    lists:sort(Dirs) ++ lists:sort(Files).
