/*  Part of XPCE --- The SWI-Prolog GUI toolkit

    Author:        Jan Wielemaker and Anjo Anjewierden
    E-mail:        jan@swi.psy.uva.nl
    WWW:           http://www.swi.psy.uva.nl/projects/xpce/
    Copyright (c)  2002-2011, University of Amsterdam
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in
       the documentation and/or other materials provided with the
       distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.
*/

:- module(find_file_dialog, []).
:- use_module(library(pce)).
:- use_module(library(file_item)).
:- require([ ignore/1
           , access_file/2
           , chain_list/2
           , atomic_list_concat/2
           , send_list/2
           , catch/3
           , atomic_list_concat/3
           , default/3
           , file_name_extension/3
           , maplist/3
           , send_list/3
           , sub_atom/5
           ]).

:- multifile
    pce_finder:file_type/2.

:- pce_begin_class(find_file_dialog, dialog,
                   "Browse for a file").

resource(up, image, image('16x16/up.png')).
resource(newdir, image, image('16x16/newdir.png')).

variable(directory,    directory*,  get,  "Current directory").
variable(message,      [code]*,     both, "Message executed on ok").
variable(mode,         {save,open}, get,  "Mode of operation").
variable(default_file, name*,       both, "Default file to open/save").
variable(confirm_overwrite, bool := @on, both, "Confirm on overwrite").

:- pce_global(@finder_directory_popup, make_directory_popup).
:- pce_global(@finder_file_popup, make_file_popup).

make_directory_popup(P) :-
    new(Finder, @event?window),
    new(P, popup(directory)),
    send(P, update_message, message(@event?receiver, selection, @arg1)),
    send_list(P, append,
              [ menu_item(rename, message(Finder, rename_dir, @arg1?key)),
                menu_item(delete, message(Finder, delete_dir, @arg1?key))
              ]).

make_file_popup(P) :-
    new(Finder, @event?window),
    new(P, popup(file)),
    send(P, update_message, message(@event?receiver, selection, @arg1)),
    send_list(P, append,
              [ menu_item(rename, message(Finder, rename_file, @arg1?key)),
                menu_item(delete, message(Finder, delete_file, @arg1?key))
              ]).


initialise(D, Mode:mode=[{open,save}], Label:[char_array]) :->
    default(Mode, open, TheMode),
    send_super(D, initialise, find_file?label_name),
    send(D, slot, message, @default),
    send(D, append, new(LookIn, finder_look_in_item(look_in))),
    send(LookIn, message, message(D, directory, @arg1)),
    send(D, append, new(Up, button(up)), right),
    send(D, append, new(New, button(newdir)), right),
    get(LookIn?value_font, height, FH),
    send_list([Up,New], reference, point(0, FH)),
    send(D, append, new(Dirs, list_browser(width := 30))),
    send(D, append, new(Files, list_browser(width := 30)), right),
    send_list([Dirs,Files],
              [ attribute(hor_stretch, 100),
                attribute(ver_stretch, 100)
              ]),
    send(Dirs, name, subdirs),
    send(Files, name, files),
    send(Dirs, select_message, message(D, subdir, @arg1?key)),
    send(Dirs, popup, @finder_directory_popup),
    send(Files, select_message, message(D, selected_file, @arg1?key)),
    send(Files, open_message, message(D, open_file, @arg1?key)),
    send(Files, popup, @finder_file_popup),
    send(D, append, new(File, file_item(file))),
    send(D, append, button(ok), right),
    send(D, append, finder_filter_item(filter)),
    send(D, append, button(cancel), right),
    send(Up, label, image(resource(up))),
    send(New, label, image(resource(newdir))),
    send(D, default_button, ok),
    send(D, resize_message, message(D, layout, @arg2)),
    send(D, mode, TheMode, Label),
    send(D, keyboard_focus, File).


layout(D, Size:[size]) :->
    "Refine layout"::
    send_super(D, layout, Size),
    get(D, member, subdirs, SubDirs),
    get(D, member, files, Files),
    get(D, width, DW),
    get(D?border, width, BW),
    get(D?gap, width, GW),
    BrW is (DW-2*BW-GW)/2,
    send(SubDirs, do_set, BW, @default, BrW),
    send(Files, do_set, BW+BrW+GW, @default, BrW),
    Right is DW-BW,
    right_to_left([ok, file], D, GW, Right),
    right_to_left([cancel, filter], D, GW, Right).

right_to_left([],  _, _, _).
right_to_left([T], D, _, Right) :-
    get(D, member, T, Item),
    send(Item, right_side, Right).
right_to_left([H|T], D, GW, Right) :-
    get(D, member, H, Item),
    get(Item, width, W),
    IX is Right - W,
    send(Item, x, IX),
    RX is IX - GW,
    right_to_left(T, D, GW, RX).


make_transient(D) :->
    "Make transient to current object"::
    get(D, frame, F),
    (   get(@event, '_value', @nil)         % no current event
    ->  send(F, application, @nil)
    ;   get(@event?window, frame, MainFrame),
        (   get(MainFrame, application, App),
            App \== @nil
        ->  send(F, application, App),
            send(F, modal, application)
        ;   send(F, modal, transient)
        ),
        send(F, transient_for, MainFrame),
        send(F, create),
        send(F, center, MainFrame?area?center)
    ).


mode(D, Mode:{open,save}, Label:[char_array]) :->
    "Mode of operation"::
    send(D, slot, mode, Mode),
    get(D, member, ok, Ok),
    send(Ok, label, Mode?label_name),
    (   Label == @default
    ->  send(D, label, Mode?label_name)
    ;   send(D, label, Label)
    ).


directory(D, Dir:directory) :->
    "Change directory"::
    (   get(D, directory, Old),
        Old \== @nil,
        send(Old, same, Dir)
    ->  send(D, slot, directory, Dir),
        send(D, fill_look_in)       % may be different
    ;   send(D, slot, directory, Dir),
        send(D, fill_look_in),
        send(D, fill_browsers),
        get(D, member, file, FileItem),
        send(FileItem, directory, Dir),
        (   get(D, default_file, Def),
            Def \== @nil
        ->  send(FileItem, selection, Def)
        ;   send(FileItem, clear)
        )
    ).


up(D) :->
    "Go one directory up"::
    get(D, directory, Dir),
    (   get(Dir, parent, Parent)
    ->  send(D, directory, Parent)
    ;   send(D, report, warning, 'No parent')
    ).


subdir(D, Name:name) :->
    "Goto selected subdiectory"::
    get(D, directory, Dir),
    get(Dir, directory, Name, Sub),
    send(D, directory, Sub).


selected_file(D, Name:name) :->
    "User selected a file"::
    get(D, member, file, FileItem),
    send(FileItem, selection, Name),
    get(D, member, files, FileBrowser),
    ignore(catch(send(FileBrowser, selection, Name), _, true)).


open_file(D, Name:name) :->
    "Select file an execute ok"::
    send(D, selected_file, Name),
    get(D, member, ok, OK),
    send(OK, execute).


fill_look_in(D) :->
    get(D, directory, Dir),
    get(D, member, look_in, Item),
    send(Item, selection, Dir?path).


favourites(D, Favourites:chain) :<-
    "List of favourite places"::
    get(D, directory, Dir),
    (   get(Dir, parent, Parent)
    ->  true
    ;   Parent = Dir                % the root
    ),
    new(Favourites, chain),
    fill_parents(Parent, Favourites),
    (   catch(expand_file_name(~, [Home]), _, fail),
        \+ send(Favourites, member, Home)
    ->  send(Favourites, append, Home)
    ;   true
    ).


fill_parents(Dir, Chain) :-
    get(Dir, path, Path),
    send(Chain, append, Path),
    (   get(Dir, parent, Parent)
    ->  fill_parents(Parent, Chain)
    ;   true
    ).


filter(D, Filter:[regex]) :<-
    "Obtain filter as a regular expression"::
    (   get(D, member, filter, Item),
        get(Item, selection, Filter)
    ->  true
    ;   Filter = @default
    ).


filter(D, Ext:'[name|tuple|chain]') :->
    get(D, member, filter, FilterItem),
    send(FilterItem, filter, Ext).


fill_browsers(D) :->
    get(D, member, subdirs, SubDirs),
    get(D, member, files, Files),
    send(SubDirs, clear),
    send(Files, clear),
    get(D, directory, Dir),
    get(D, filter, Filter),
    new(FoundDirs, chain),
    new(FoundFiles, chain),
    send(Dir, scan, FoundFiles, FoundDirs, Filter),
    send(FoundFiles, sort),
    send(FoundDirs, sort),
    send(SubDirs, members, FoundDirs),
    send(Files, members, FoundFiles).


selection(D, FileName:name) :<-
    "Return selected filename"::
    get(D, member, file, FileItem),
    get(FileItem, selection, RawName),
    get(D, ensure_extension, RawName, FileName).


ensure_extension(D, Raw:name, WithExt:name) :<-
    "Ensure to apply the extension"::
    get(D, member, filter, Item),
    get(Item, selection, Regex),
    (   send(Regex, match, Raw)
    ->  WithExt = Raw
    ;   get(Item, default_extension, Ext)
    ->  file_name_extension(Raw, Ext, WithExt)
    ;   WithExt = Raw
    ).

:- pce_group(validate).

check_file(D, FileName:name) :->
    "Perform sanity-check of the resulted file"::
    (   get(D, mode, open)
    ->  send(D, check_open_file, FileName)
    ;   send(D, check_save_file, FileName)
    ).

check_open_file(D, FileName:name) :->
    "Check for in Open mode"::
    new(File, file(FileName)),
    (   send(File, access, read)
    ->  true
    ;   (   send(File, exists)
        ->  send(D, report, error,
                 'You have no read-access on %s', FileName)
        ;   send(File, exists, @off)
        ->  send(D, report, error,
                 '%s is not a regular file', FileName)
        ;   send(D, report, error,
                 '%s does not exist', FileName)
        ),
        fail
    ).

check_save_file(D, FileName:name) :->
    "Check for in Save mode"::
    new(File, file(FileName)),
    (   access_file(FileName, write)
    ->  (   get(D, confirm_overwrite, @on),
            send(File, exists)
        ->  send(D, ask_overwrite, FileName)
        ;   true
        )
    ;   (   send(File, exists)
        ->  send(D, report, error,
                 'You have no write-access on %s', FileName)
        ;   send(File, exists, @off)
        ->  send(D, report, error,
                 '%s is not a regular file', FileName)
        ;   send(D, report, error,
                 'You cannot create a file here')
        ),
        fail
    ).

ask_overwrite(D, FileName:name) :->
    "Ask user to overwrite file"::
    send(D?display, confirm, D, 'File finder',
         'Overwrite file %s?', FileName).

:- pce_group(button).

ok(D) :->
    "User confirmed"::
    (   get(D, message, Msg),
        send(Msg, instance_of, code)
    ->  get(D, selection, FileName),
        send(D, check_file, FileName),
        send(Msg, forward_receiver, D, FileName)
    ;   true
    ).


cancel(D) :->
    "User pressed cancel"::
    send(D, destroy).

:- pce_group(edit).

newdir(D) :->
    "Create a new directory"::
    get(D, prompt_name, create_directory, DirName),
    get(D, directory, Dir),
    get(Dir, directory, DirName, SubDir),
    send(SubDir, make),
    send(D, directory, SubDir).

prompt_name(D, For:name, Name:name) :<-
    "Prompt for a (new) name"::
    new(D2, dialog(For?label_name)),
    send(D2, transient_for, D),
    send(D2, append, new(TI, text_item(name))),
    send(D2, append, button(For, message(D2, return, TI?selection?strip))),
    send(D2, append, button(cancel, message(D2, destroy))),
    send(D2, default_button, For),
    get(D2, confirm_centered, D?frame?area?center, Name),
    send(D2, destroy),
    Name \== ''.

delete_dir(D, Name:name) :->
    "Delete named directory"::
    send(D?display, confirm, 'Delete directory "%s"?', Name),
    get(D, directory, Dir),
    get(Dir, directory, Name, SubDir),
    send(SubDir, remove),
    send(D, fill_browsers).

rename_dir(D, Name:name) :->
    "Rename directory"::
    get(D, directory, Dir),
    get(Dir, file, Name, SubDir),
    get(D, prompt_name, rename, NewName),
    (   send(file(NewName), exists)
    ->  send(D, ask_overwrite, NewName)
    ;   true
    ),
    send(SubDir, name, NewName),
    send(D, fill_browsers),
    get(D, member, subdirs, Browser),
    ignore(catch(send(Browser, selection, NewName),_,true)).

delete_file(D, Name:name) :->
    "Delete named file"::
    send(D?display, confirm, 'Delete file "%s"?', Name),
    get(D, directory, Dir),
    get(Dir, file, Name, File),
    send(File, remove),
    send(D, fill_browsers).

rename_file(D, Name:name) :->
    "Rename file"::
    get(D, directory, Dir),
    get(Dir, file, Name, File),
    get(D, prompt_name, rename, NewName),
    (   send(file(NewName), exists)
    ->  send(D, ask_overwrite, NewName)
    ;   true
    ),
    send(File, name, NewName),
    send(D, fill_browsers),
    send(D, selected_file, NewName).

:- pce_end_class(find_file_dialog).


:- pce_begin_class(finder_look_in_item, directory_item,
                   "Directory item with favourites").

show_combo_box(DI, Show:bool) :->
    "User clicked right-box"::
    (   Show == @on
    ->  get(DI, device, Finder),
        get(Finder, favourites, Favourites),
        send(DI, select_completion, Favourites, '', '', 0)
    ;   send_super(DI, show_combo_box, Show)
    ).

selected_completion(DI, Selected:char_array, _Apply:[bool]) :->
    "User selected from the browser"::
    send_super(DI, selected_completion, Selected),
    send(DI, apply, @on).

enter(DI) :->
    "Update current directory"::
    send(DI, apply, @on).

:- pce_end_class(finder_look_in_item).

:- pce_begin_class(finder_filter_item, text_item,
                   "Show file patterns").

initialise(FI, Name:name) :->
    send_super(FI, initialise, Name),
    send(FI, type, regex),
    send(FI, filter, @default).

selection(FI, Filter:regex) :<-
    (   get(FI, modified, @on)
    ->  get(FI, value, Typed),
        new(Filter, regex),
        send(Filter, file_pattern, Typed),
        send(FI, slot, selection, Filter)
    ;   get(FI, slot, selection, Filter)
    ).

default_extension(FI, Ext:name) :<-
    get(FI, selection, Regex),
    get(Regex, attribute, default_extension, Ext).


selected_completion(FI, Sel:name) :->
    "User selected an alternative"::
    get(FI?value_set, find,
        @arg1?print_name == Sel,
        Re),
    send(FI, selection, Re),
    get(FI, device, Finder),
    send(Finder, fill_browsers).

enter(FI) :->
    "Update with new filter"::
    get(FI, device, Finder),
    send(Finder, fill_browsers).


%       ->filter(Filter)
%
%       Defines file-selection filter.  The argument is either single
%       or a chain to specify alternatives.  Singles are either a plain
%       atom denoting the extension, or a tuple(Label, Ext), where Ext
%       is a single atom or a chain of atoms.
%
%       For example:
%
%           ->filter: pl                % *.pl files
%           ->filter: chain(pl,qlf)     % *.pl or *.qlf files
%           ->filter: tuple(prolog, chain(pl,qlf))
%           ->filter: chain(tuple(prolog, chain(pl, qlf)),
%%                          tuple(c, chain(c,h)))

filter(FI, Ext:'[name|tuple|chain]') :->
    (   Ext == @default
    ->  send(FI, filter, tuple(all_files, *))
    ;   new(ValueSet, chain),
        (   send(Ext, instance_of, chain)
        ->  send(Ext, for_all,
                 message(ValueSet, append,
                         ?(@prolog, to_regex, @arg1)))
        ;   to_regex(Ext, Regex),
            send(ValueSet, append, Regex)
        ),
        send(FI, value_set, ValueSet),
        send(FI, selection, ValueSet?head)
    ).


to_regex(Tuple, Regex) :-
    send(Tuple, instance_of, tuple),
    !,
    get(Tuple, first, LabelName),
    get(Tuple, second, Exts),
    new(Regex, regex),
    alt_regex(Exts, Regex),
    send(Regex, attribute, print_name, LabelName?label_name).
to_regex(*, Regex) :-
    !,
    new(Regex, regex(.*)),
    send(Regex, attribute, print_name, all_files?label_name).
to_regex(Atom, Regex) :-
    new(Regex, regex),
    send(Regex, attribute, print_name, string('*.%s', Atom)),
    alt_regex(Atom, Regex).

alt_regex(*, Regex) :-
    !,
    send(Regex, pattern, '.*').
alt_regex(Atom, Regex) :-
    atom(Atom),
    !,
    ext_pattern(Atom, Plain, Pattern),
    send(Regex, pattern, Pattern),
    (   pce_finder:file_type(Plain, Name)
    ->  send(Regex, attribute, print_name, Name)
    ;   send(Regex, attribute, print_name,
             string('%s files', Atom?label_name)?value)
    ),
    send(Regex, attribute, default_extension, Atom).
alt_regex(Chain, Regex) :-
    chain_list(Chain, List),
    maplist(ext_pattern, List, Patterns),
    atomic_list_concat(Patterns, '|', AltPattern),
    send(Regex, pattern, AltPattern),
    (   List = [Def|_]
    ->  send(Regex, attribute, default_extension, Def)
    ;   true
    ).

ext_pattern(Ext, Pattern) :-
    ext_pattern(Ext, _, Pattern).

ext_pattern(Ext, Plain, Pattern) :-
    sub_atom(Ext, 0, _, _, '.'),
    !,
    sub_atom(Ext, 1, _, 0, Plain),
    atomic_list_concat(['^.*\\', Ext, '$'], Pattern).
ext_pattern(Ext, Ext, Pattern) :-
    atomic_list_concat(['^.*\\.', Ext, '$'], Pattern).

:- pce_end_class(finder_filter_item).
