/*  FILES.PL  */

/*
:- module files.


:- public open_input/2,
          open_output/2,
          open_and_reconsult/2,
          exists_file/2, exists_file/1,
          find_file/4, find_file/5,
          add_file_defaults/2,
          print_file/2, print_and_delete_file/2.
*/

/*
SPECIFICATION
-------------

This module exports file-handling predicates.

Briefly, open_input and open_output try opening files for input or
output, but instead of crashing if an error (such as illegal filename)
occurs, they return an atom saying what the error was. This makes it
possible to trap and recover from errors.

open_and_reconsult acts the same way, but reconsults the file if it's
readable.

exists_file checks whether a file exists and returns its full name.

find_file acts as a wildcarded search, looking for a file amongst a
range of directories or extensions.

add_file_defaults generates a full file name from a partial name plus
defaults.

print_file prints a file to a specified printer. print_and_delete_file
does the same but deletes it afterwards.

You must consult BUG.PL before using this module.


PUBLIC open_input( FileName+, Status? ):
----------------------------------------

"Status indicates the result of trying to open FileName for input and
redirecting the CIS to it".

Try opening the file named FileName for input. FileName should be an
atom. If it opens, unify Status with 'ok', and make the file the CIS.
Otherwise leave the CIS as it is and unify Status with one of the atoms
    {file_not_found, directory_not_found,
     access_forbidden, invalid_filename}. An error which isn't one of
these may have undefined effects (such as causing an 'abort').


PUBLIC open_and_reconsult( FileName+, Status? ):
------------------------------------------------

"Status indicates the result of trying to open and reconsult FileName".

FileName should be as for open_input. Try opening FileName for input. If
it opens, unify Status with 'ok' and reconsult the file. Otherwise unify
Status with one of the same error atoms as for open_input, and don't
consult the file. The same caution about unexpected errors holds.

This predicate does not change the CIS.


PUBLIC open_output( FileName+, Status? ):
-----------------------------------------

"Status indicates the  result of trying to open FileName  for output and
redirect the COS to it".

Try opening the file for output. If it opens, unify Status with 'ok',
and make the file the COS. Otherwise leave the COS as it is and unify
Status with one of the atoms
    {no_space_for_file, directory_not_found, access_forbidden,
     invalid_filename}.
This predicate works in the same  way as open_read, and the same caution
about unexpected errors applies.


PUBLIC exists_file( FileName+, FullName? ):
-------------------------------------------

"The file named FileName exists, and its full name is FullName".

FileName can be either an atom, or a structure of the form
    Name+defaults(Directory,Extension)
where Name, Directory, and Extension are atoms.

This predicate checks to see whether the file exists. If so, FullName is
unified with an atom which is the full name (complete with disc,
directory, extension, and version number on my system, VAX/VMS). If not,
the predicate fails.

If FileName is a +structure, its second argument is assumed to contain
defaults for the directory name and file extension. If the name doesn't
contain a component, and the default value given is not '', exists_file
adds the default before testing for existence.

Examples:

    exists_file( 'fred.pl', F )
        -  fail if no FRED.PL exists in the current directory.
        -  else set F to (e.g.) 'fs12:[me]fred.pl;12'.

    exists_file( 'fred'+defaults('[me]','.pl')
        -  if [ME]FRED.PL exists, set F to the full name as above.

    exists_file( 'fred.lsp'+defaults('[me]','.pl')
        -  if [ME]FRED.LSP exists, set F to the full name.

    exists_file( '[you]fred.lsp'+defaults('[me]','.pl')
        - if [YOU]FRED.LSP exists, set F to the full name.

Note: As exists_file is implemented here, it deems a file to be found
even if it can't be read. So finding a file is no guarantee that it can
be read (the file might, for example, have read access denied).


PUBLIC exists_file( File+ ):
----------------------------

"The file named File exists".

As for exists_file/2, ignoring the second argument.


PUBLIC find_file( File+, DefaultList+, FullName?, Status? ):
------------------------------------------------------------

"Status indicates whether there exists a file whose name is File plus
one of the defaults in DefaultList. Its full name is FullName."

File is an atom, naming a file. DefaultList is a list of structures,
each of the form
    defaults(Directory,Extension)
as for exists_file/2.

The idea is that DefaultList specifies a number of directory/extension
defaults under which to search for File. This list must not be empty.

These defaults work in the same way as for exists_file. find_file tries
each in turn, until either it runs out of defaults, or it finds the
file. If it does find the file, it unifies FullName with the full name
(as for exists_file) and Status with 'ok'. Otherwise it leaves FullName
uninstantiated and unifies Status with the atom file_not_found.

Note that (like exists_file), find_file may say that a file exists even
if it can't be read.

Examples:

    find_file( fred,
               [defaults('','.pl'),defaults('','.lsp')],
               F, Status )
        - if FRED.PL exists in the current directory, sets
          F to (e.g.) 'fs12:[me]fred.pl;12'.
        - if FRED.PL doesn't exist, but FRED.LSP does, sets F
          to (e.g.) 'fs12:[me]fred.lsp;5'.
        - else sets Status to file_not_found.

    find_file( 'fred.lib',
               [defaults('',''),defaults('[here]',''),
                defaults('[there]','')],
               F, Status )
        - searches the current directory, [HERE], and [THERE]
          in that order for FRED.LIB.


PUBLIC find_file( File+, DirectoryList+, Extension+,
                  FullName?, Status? ):
----------------------------------------------------

"Status indicates whether there exists, in one of the directories in
DirectoryList, a file whose name is File and whose extension defaults to
Extension. Its full name is FullName."

This is a different arrangement of arguments, more convenient for some
purposes; it searches a list of directories for a file with specified
extension. Once again, the list must not be empty.

Arguments File, FullName and Status are as for find_file/4.
DirectoryList is a list of directory names (each of which must be an
atom). Extension is a default extension, also an atom.

Example:

    find_file( 'fred', [ '', '[MATTHEWS]', '[LATNER]' ], '.PL',
               F, Status )
        - looks for FRED.PL, first in the current
          directory, then in [MATTHEWS], and finally
          in [LATNER]. If found, F becomes a full
          name, otherwise S becomes file_not_found.


PUBLIC add_file_defaults( FileName+, FullName? ):
-------------------------------------------------

"FullName is the full name of FileName after adding any defaults
it specifies".

FileName can be either an atom, or a structure of the form
    Name+defaults(Directory,Extension)
as for exists_file.

If either default is not '', and the file name lacks that component,
then add_file_defaults adds it to make FullName.

Examples:

    add_file_defaults( fred+defaults('[me]','.pl'), F )
        - sets F to '[me]fred.pl'.

    add_file_defaults( 'fred.lsp'+defaults('','.pl'), F )
        - sets F to 'fred.lsp'.

    add_file_defaults( fred, F )   or

    add_file_defaults( fred+defaults('','') )
        - set F to fred.


PUBLIC print_file( FileName+, Printer+ ):
-----------------------------------------

"FileName has been printed to Printer".

Prints the file on the specified printer. FileName must be an atom.
Printer is any atom naming one of the printers
    { USERAREA, PSYZOO, CTC, ... }
defined in HELP PRINTERS at Oxford.

Note there is no status returned. Under VMS, it is not possible to
pick one up (even though the file has been queued for printing, the
printer might be off-line, out of paper, etc).
*/


/*
IMPLEMENTATION
--------------


Below are notes on my implementation, and suggestions for porting. Of
course, your Prolog implementor may be nice and provide 'print_file',
'exists_file' etc. built-in. In that case, you're lucky...


1) Safe open routines.
----------------------

These rely on being able to trap and recover from errors. If you can't
do this, you won't be able to implement them. The way it's done here is
this: when an error occurs (e.g. trying to see a non-existent file,
trying to call an undefined predicate, trying to do arithmetic on a
variable, trying to square-root a negative number),
Poplog calls the system predicate
    prolog_error( ErrorMessage, Culprits )
with an error-message-atom (which may come in part from VMS) and the
item or items which caused the error (the "culprits").

There is initially one clause for prolog_error. This is activated
whenever ErrorMessage indicates that an undefined predicate was called.

You can change this action by asserting extra clauses for prolog_error.
Obviously, to be useful, they must be asserta'd, to come before the
clauses I've just mentioned. Part of the code below does this: there is
one clause asserted for trapping errors caused by open_input and
open_and_reconsult; and another for trapping errors caused by
open_output.

Consider input first. The clause for prolog_error that deals with input
translates the system message it receives into one of the atoms I use
for the Tutor's error codes. This is done by calling
    input_error( SystemMessage, MyMessage )
It then asserts
    '$open_input_error_was'( MyMessage )

So open_input can now be made safe. It calls 'see'. This will either
work normally; or cause an error. In the latter case, the error will
cause a call of prolog_error, which will assert a clause for
'$open_input_error_was'(...). open_input can then test for the presence
of this clause, retract it if it exists, and return the status value.

open_output works in the same way. There is one complication. I
sometimes want the same system message to be translated differently for
input and output errors. So I need two different error-handlers. This is
achieved by asserting another clause saying which the calling routine
was: one of
    '$calling_open_input'
    '$calling_open_output'
My clauses for prolog_error can then check this.

If your Prolog has error-trap predicates, you may be able to adapt this
mechanism. If not, but you have something akin to '$nofilerrors': i.e.
some way of making 'see' fail if it detects an error, you can use that.
You won't be able to pick up an error code, but you can at least detect
whether 'see' failed, and return status value not_ok or something.


2) Exists file.
---------------

This calls a Pop-11 routine to test for file existence.

If you don't have a built-in existence tester, or one reachable from a
foreign language, you may be able to use error traps, if you have them.
To test whether a file exists: try 'see'ing it. If this causes an
error, it probably doesn't exist. It might of course exist but be
unreadable: in that case, you will lose a bit of specificity in
the message (it's possible that the teacher could have copied the scripts
to a library directory, and forgotten to give public access).


3) Find file.
-------------

This just calls exists_file for each possible full name.


4) Add file defaults.
---------------------

This calls a Pop-11 routine for parsing filenames into their components.
If you don't have something like this, you could convert the filename to
a list of characters and use a DCG grammar to parse it.


5) Print file.
--------------

This calls the Pop-11 command issuer. If you don't have one, you might
be able to do something like this:
    print_file( File, Printer ) :-
        seeing( CIS ), telling( COS ),
        see( File ), tell( Printer ),
        repeat,
            (
                get0(C),
                C = /* your end-of-file character */
            ;
                put(C),
                fail
            ),
        seen, told,
        see( CIS ), tell( COS ).
in systems where the printer can be treated as a file.
*/

/*
:- needs bug.


:- dynamic '$open_input_error_was'/1,
           '$open_output_error_was'/1,
           '$calling_open_input'/0,
           '$calling_open_output'/0.
 */

/*  The Pop-11 parts of these predicates live in FILES.P  */
:- pop_compile( 'files.p' ).


open_input( FileName, StatusOut ) :-
    retractall( '$open_input_error_was'(_) ),
    assert( '$calling_open_input' ),
    seeing( CIS ),
    see( FileName ),
    (
        '$open_input_error_was'(Status)
    ->
        see( CIS ),
        retractall( '$open_input_error_was'(_) )
    ;
        Status = ok
    ),
    retractall( '$calling_open_input' ),
    StatusOut = Status.


open_and_reconsult( FileName, StatusOut ) :-
    retractall( '$open_input_error_was'(_) ),
    assert( '$calling_open_input' ),
    reconsult( FileName ),
    (
        '$open_input_error_was'(Status)
    ->
        retractall('$open_input_error_was'(_))
    ;
        Status = ok
    ),
    retractall( '$calling_open_input' ),
    StatusOut = Status.


:- asserta(( prolog_error('CAN\'T OPEN FILE (file not found)', File ) :-
       '$calling_open_input', !,
       asserta( '$open_input_error_was'(file_not_found))
   )).

:- asserta(( prolog_error('CAN\'T OPEN FILE (directory not found)', File ) :-
       '$calling_open_input', !,
       asserta( '$open_input_error_was'(directory_not_found))
   )).

:- asserta(( prolog_error( 'CAN\'T OPEN FILE (insufficient privilege or file protection violation)', File ) :-
       '$calling_open_input', !,
       asserta( '$open_input_error_was'(access_forbidden))
   )).

:- asserta(( prolog_error('CAN\'T OPEN FILE (error in file name)', File ) :-
       '$calling_open_input', !,
       asserta( '$open_input_error_was'(invalid_filename))
   )).

:- asserta(( prolog_error('Wrong value for argument1 of see', File ) :-
    '$calling_open_input', !,
    asserta( '$open_input_error_was'(invalid_filename))
)).


open_output( FileName, StatusOut ) :-
    assert( '$calling_open_output' ),
    retractall( '$open_output_error_was'(_) ),
    telling( COS ),
    tell( FileName ),
    (
        '$open_output_error_was'(Status)
    ->
        tell( COS ),
        retractall('$open_output_error_was'(_))
    ;
        Status = ok
    ),
    retractall( '$calling_open_output' ),
    StatusOut = Status.


:- asserta(( prolog_error('CAN\'T OPEN FILE (directory not found)', File ) :-
    '$calling_open_output', !,
    asserta( '$open_output_error_was'(directory_not_found))
)).

:- asserta(( prolog_error('CAN\'T CREATE FILE (error in file name)', File ) :-
    '$calling_open_output', !,
    asserta( '$open_output_error_was'(invalid_filename))
)).

:- asserta(( prolog_error('Wrong value for argument1 of tell', File ) :-
    '$calling_open_output', !,
    asserta( '$open_output_error_was'(invalid_filename))
)).

:- asserta(( prolog_error('CAN\'T CREATE FILE (disk quota exceeded)', File ) :-
    '$calling_open_output', !,
    asserta( '$open_output_error_was'(no_space_for_file))
)).

:- asserta(( prolog_error( 'CAN\'T OPEN FILE (insufficient privilege or file protection violation)', File ) :-
    '$calling_open_output', !,
    asserta( '$open_output_error_was'(access_forbidden))
)).


exists_file( File+defaults(Directory,Extension), FullName ) :-
    !,
    prolog_eval( exists_file(Directory,File,Extension), FullName ),
    /*  Returns 0 if the file doesn't exist.  */
    FullName \= 0.

exists_file( File, FullName ) :-
    exists_file( File+defaults('',''), FullName ).


exists_file( File ) :-
    exists_file( File, _ ).


find_file( _, [], _, _ ) :-
    bug( 'find_file: default list is empty' ).

find_file( File, DefaultList, FullNameOut, StatusOut ) :-
    find_file1( File, DefaultList, FullName, Status ),
    FullNameOut = FullName,
    StatusOut = Status.


find_file1( _, [], _, file_not_found ) :- !.

find_file1( File, [Defaults|OtherDefaults], FullName, ok ) :-
    exists_file( File+Defaults, FullName ),
    !.

find_file1( File, [_|OtherDefaults], FullName, Status ) :-
    find_file( File, OtherDefaults, FullName, Status ).


find_file( _, [], _, _, _ ) :-
    bug( 'find_file: default list is empty' ).

find_file( File, DefaultList, Extension, FullNameOut, StatusOut ) :-
    find_file1( File, DefaultList, Extension, FullName, Status ),
    FullNameOut = FullName,
    StatusOut = Status.


find_file1( _,  [], _, _, file_not_found ) :- !.

find_file1( File, [Dir|OtherDirs], Ext, FullName, ok ) :-
    exists_file( File+defaults(Dir,Ext), FullName ),
    !.

find_file1( File, [_|OtherDirs], Ext, FullName, Status ) :-
    find_file1( File, OtherDirs, Ext, FullName, Status ).


add_file_defaults( File+defaults(Directory,Extension), FullName ) :-
    !,
    prolog_eval( add_file_defaults(Directory,File,Extension), FullName ).

add_file_defaults( FileName, FileName ).


print_file( File, Printer ) :-
    prolog_eval( print_file(File,Printer,nodelete) ).


print_and_delete_file( File, Printer ) :-
    prolog_eval( print_file(File,Printer,delete) ).

/*
:- endmodule.
*/
