
:- lib(stoics_lib:mod_goal/3).
:- lib(stoics_lib:en_list/3).
:- lib(stoics_lib:io_streams/3).

os_mill_defaults( Args, Defs ) :- 
    ( memberchk(goal(MGoal),Args) -> 
        mod_goal( _Mod, Goal, MGoal ),
        functor( Goal, Psfx, _ )
        ;
        throw( internal_error(os_mill_no_goal) )
    ),
    Defs = [ recreate(false), separator('_'),
             type(file), call_options([]),
             milled(_), dir(''), homonymous(false),
             call_module(user), not_created(error), postfix(Psfx),
             on_exists(true),
             outputs_to(user), outputs_as_tty(true)
                ].

/** os_mill( +FromOs, +Goal, ?Milled, +Opts ).

Generate or recreate Milled from FromOs by calling Goal.
If Milled is not given its name is generated by applying 
os_postfix/3 on FromOs with SepPsf as the postfix.
Milled is usually a file, but it can also be a directory.
By default, Goal is trusted to generate the correct format.
If FromOs is of the form @(Callable), then =|call(Callable,Obj)|=
is called to establish the location of OsObj which is used in place of FromOs.

As of 0.3, when =|Type=dir|= and =|OutsTo|= is a filename, Milled is created 
as to house the file. Goal, should be aware of this- as it might be attempting to 
create it too.

As of v0.4 Milled returns the path without Dir, the Dir/Milled versioned can be found in option milled(Dilled)

The Goal is called as call( Goal, RelFromOs, Milled, Co ). 

Opts
  * call_options(Co=[])
    the argument is passed as the last argument to Goal. (Set to false for no options argument on the call.)
  * call_module(Mod=user)
    at which module to call Goal
  * debug(Dbg)
    whether to print debugs (see options_append/4) (default does nothing)
  * dir(Dir)
    directory for both Os and Milled (no default)
  * ext(Ext)
    if given, change the extension in Milled (ignored for dirs),
    when Milled is ground the extension is added
  * homonymous(Homon=false)
    use Goal's term name as the stem
  * milled(Dilled)
    returns the full milled OS name (file or directory)
  * not_created(Created=error)
    check Os was created at exit; if not take action: true, error, fail, debug
  * odir(ODir)
    directory for output (takes precendence over Dir)
  * on_exists(OnX=true)
    callable that is qurried if Milled already exists, eg to load an R object from a saved file
    called as call(OnX,Milled), if OnX has a module, then it is used as is, else Mod is : prepended.
  * outputs_to(OutsTo=user)
    if a different filename is given, it is used for redirecting both user_output and user_error.
    When =|Type=dir|= OutsTo is assumed to be a file within (Milled, is created in this case).
  * outputs_as_tty(OutsTty=true)
    only relevant when OutsTo is a filename. =|OutsTty=true|= makes the file a tty output (stream_property/2).
  * postfix(Psf=f(File))
    postfix for the new file name (see os_postfix/3)
  * recreate(R=false)
    whether to recreate OsEntry
  * separator(Sep='_')
    separator for Psf
  * type(Type=file)
    or dir for directory

The predicate uses os_dir_stem_ext/2 to construct OS, so its options can be used 
in addition to the above.

The default postfix (P) is taken to be the predicate name of Goal, 
minus a possible 'file_' prefix.

==
?- assert( (true(A,B,C) :- write(args(A,B,C)), nl) ).

?- os_mill( abc.txt, true, Outf, [] ).
args(abc.txt,abc_true.txt,[])
ERROR: os:os_mill/4: OS milled: abc_true.txt  was not created (source was: abc.txt)

?- os_mill( abc.txt, true, Outf, not_created(fail) ).
args(abc.txt,abc_true.txt,[])
false.

?- use_module(library(debug)).
true.

?- [user].
|: go_from_here_to_there( Here, There, HTopts ) :-
|:     debug( testo, 'Here: ~w', [Here] ),
|:     debug( testo, 'There: ~w', [There] ),
|:     debug( testo, 'TheOpts: ~w', [HTopts] ).
|: ^D% user://1 compiled 0.01 sec, 1 clauses
true.

?- debug(testo).
true.

?- Milts = [outputs_to('debug_outs.txt'),type(dir),debug(true)],
    os_mill( here, go_from_here_to_there, ex_os_milled, Milts ),
    write( milled(ex_os_milled) ), nl.
% Creating non-existing mill entity: ex_os_milled, from: here
% Calling os_mill: user:go_from_here_to_there/0
% Opened:'ex_os_milled/debug_outs.txt', at:<stream>(0x558dbb1d98c0)
% Changing channels to: io_streams(user_input,<stream>(0x558dbb1d98c0),<stream>(0x558dbb1d98c0))
% Closing: <stream>(0x558dbb1d98c0)
% Run output at: 'ex_os_milled/debug_outs.txt'
milled(ex_os_milled)
Milts = [outputs_to('debug_outs.txt'), type(dir), debug(true)].

?- shell( 'cat ex_os_milled/debug_outs.txt').
% Calling: call(user:go_from_here_to_there,here,ex_os_milled,[])
% Here: here
% There: ex_os_milled
% TheOpts: []
% Caught: exit
% Reverting streams to: io_streams(<stream>(0x7f4311820780),<stream>(0x7f4311820880),<stream>(0x7f4311820980))
true.

?- 
==

Test the difference between Milled and Dilled.
==
?- os_mill( abc.txt, true, Milled, [not_created(true),postfix(when),milled(Dilled),dir(data)] ).
args(data/abc.txt,data/abc_when.txt,[])
Milled = abc_when.txt,
Dilled = 'data/abc_when.txt'.
==

@author nicos angelopoulos
@version  0.1 2014/10/15
@version  0.2 2016/06/28
@version  0.3 2020/09/16, added outputs_to() & outputs_as_tty(), with example
@version  0.4 2024/04/24, changed Milled to path without dir
@tbd double check non atomic File and Milled 

*/
os_mill( InFile, Goal, Milled, Args ) :-
    ( (\+ var(InFile),InFile = @(FileGoal)) -> call(FileGoal,File); File = InFile ),
    en_list( Args, Argos, os_mill/4 ),
    options_append( os_mill, [goal(Goal)|Argos], Opts, process(debug) ),
    options( recreate(Rcr), Opts ),
    options( type(Type), Opts ),
    options( homonymous(Omon), Opts ),
    ( var(Milled) -> true; Nilled = Milled ),
    os_mill_destination( Omon, Nilled, Goal, File, DMilled, Opts ),
    options( dir(Dir), Opts ),
    os_path( Dir, File, DirFile ),
    os_name( File, Ftype ),
    os_cast( Ftype, DirFile, RelFromOs ),
    memberchk( milled(DMilled), Opts ),
    ( var(Milled) -> Nilled = Milled; true ),
    os_mill( Rcr, RelFromOs, Type, Goal, DMilled, Opts ).

/** os_mill_destination( +Homon, ?Milled, +Goal, +SrcFile, -DMilled, +Opts ).

Create Milled and dirpended Milled (DMilled) from either manipulating SrcFile or by taking the term name
from Goal, depending on Homon Boolean value (true: is homonymous to Goal).

==
?- os_lib:os_mill_destination( false, Milled, true, abc.txt, DMill, [postfix(post)] ).
Milled = DMill, DMill = abc_post.txt.

?- os_lib:os_mill_destination( false, Milled, true, abc.txt, DMill, [postfix(post),dir(sub)] ).
Milled = DMill, DMill = 'sub/abc_post.txt'.

?- os_lib:os_mill_destination( true, xyz.tab, true, abc.txt, DMill, [postfix(post),dir(sub)] ).
DMill = 'sub/xyz.tab'.

?- os_lib:os_mill_destination( true, Milled, true, abc.txt, DMill, [postfix(post),dir(sub)] ).
Milled = true,
DMill = 'sub/true'.
==

*/
os_mill_destination( false, Milled, _Goal, File, DMilled, Opts ) :-
    os_ext( Ext, _, File, os_mill/4 ),
    append( [from(File)|Opts], [ext(Ext)], StemOpts ),
    % os_stem( Stem, Milled, [from(File)|Opts] ),
    ( var(Milled) -> 
        options( postfix(Psfx), Opts ),
        os_postfix( Psfx, File, Posted, [ext(Ext)|Opts] ),
        os_stem( Stem, Posted, Opts )
        ;
        os_stem( Stem, Milled, StemOpts )
    ),
    append( [stem(Stem)|Opts], [ext(Ext)], DSEopts ),
    os_dir_stem_ext( DMilled, DSEopts ),
    ( var(Milled) -> options(ext(Mlt),DSEopts), os_ext(Mlt,Stem,Milled); true ).

os_mill_destination( true, Milled, Goal, File, DMilled, Opts ) :-
    Self = os_mill/4,
    ( var(Milled) -> 
        mod_goal( _Mod, Call, Goal, os_lib:os_mill/4 ),
        functor(Call,Gname,_),
        ( memberchk(ext(Ext),Opts) -> os_ext(Ext,Gname,Milled,Self) ; Milled = Gname )
        ;
        os_stem( Stem, Milled, [from(File)|Opts] )
    ),
    os_ext( Ext, Stem, Milled, Self ),
    os_dir_stem_ext( DMilled, [stem(Stem),ext(Ext)|Opts] ).

os_mill( false, File, Type, _Goal, Milled, Opts ) :-
    exists_milled( Type, Milled ),
    !,
    Using = 'Using existing milled ~w: ~w, apparently from: ~w ',
    debug( os_mill, Using, [Type,Milled,File] ),
    options( on_exists(OnX), Opts ),
    os_mill_exists( OnX, Milled, Opts ).
os_mill( false, File, Type, Goal, Milled, Opts ) :-
    \+ exists_milled( Type, Milled ),
    Create = 'Creating non-existing mill entity: ~w, from: ~w',
    debug( os_mill, Create,  [Milled,File] ),
    os_mill_call( File, Type, Goal, Milled, Opts ).
os_mill( true, File, Type, Goal, Milled, Opts ) :-
    exists_milled( Type, Milled ),
    !,
    Create = 'Recreating mill ~w: ~w, from: ~w',
    debug( os_mill, Create,  [Type,Milled,File] ),
    os_mill_delete_type( Type, Milled ),
    os_mill_call( File, Type, Goal, Milled, Opts ).
os_mill( true, File, Type, Goal, Milled, Opts ) :-
    \+ exists_milled( Type, Milled ),
    Create = 'Creating non-existing mill entity: ~w, from: ~w',
    debug( os_mill, Create,  [Milled,File] ),
    os_mill_call( File, Type, Goal, Milled, Opts ).

os_mill_exists( true, _Milled, _Opts ) :- !.
os_mill_exists( OnX, Milled, Opts ) :-
    os_mill_callable( OnX, Goal, Opts ),
    Calling = 'Calling on_exists option: ~w, on existing millled file ~w',
    debug( os_mill, Calling, [Goal,Milled] ),
    call( Goal, Milled ).

os_mill_delete_type( file, Milled ) :-
    os_rm( Milled ).
os_mill_delete_type( dir, Milled ) :-
    delete_directory_and_contents( Milled ).

% this was old style can remove now that we use os_exists/2
exists_milled( file, Milled ) :-
    os_exists( Milled, type(flink) ),
    !.
exists_milled( dir, Milled ) :-
    os_exists( Milled, dlink ).

os_mill_call( File, Type, Goal, Milled, Opts ) :-
    options( call_options(CoPrv), Opts ),
    ( CoPrv == false -> CoInc = false; CoInc = true ),
    en_list( CoPrv, Copts ),
    os_mill_callable( Goal, Callable, Opts ),
    Callable = CallMod:Pid,
    functor( Pid, Patom, Parity ),
    debug( os_mill, 'Calling os_mill: ~w', [CallMod:Patom/Parity] ),
    options( outputs_to(OutsTo), Opts ),
    options( outputs_as_tty(OutTty), Opts ),
    os_mill_call_outputs( OutsTo, OutTty, CoInc, Callable, File, Type, Milled, Copts ),
    % os_mill_call_opts( CoInc, Callable, File, Milled, Copts ),
    holds( os_lib:os_exists(Milled), Exists ),
    options( not_created(Created), Opts ),
    os_mill_created( Exists, Created, Milled, File, Opts ).

os_mill_call_outputs( user, _OutTty, CoInc, Callable, File, _Type, Milled, Copts ) :-
    !,
    os_mill_call_opts( CoInc, Callable, File, Milled, Copts ).
os_mill_call_outputs( OutsFile, OutTty, CoInc, Callable, FromOs, Type, Milled, Copts ) :-
    ( Type == dir -> 
        make_directory( Milled ),
        os_path( Milled, OutsFile, OutsDst )
        ;
        OutsDst = OutsFile
    ),
    io_streams( In, Ou, Er ),
    setup_call_catcher_cleanup( (   open(OutsDst,write,MessAt),
                                    debug(os_mill,'Opened:~p, at:~w',[OutsDst,MessAt]),
                                    os_mill_set_stream(OutTty,MessAt),
                                    debug(os_mill,'Changing channels to: ~w', io_streams(user_input,MessAt,MessAt)),
                                    io_streams(user_input,MessAt,MessAt)
                                ),
                        os_mill_call_opts( CoInc, Callable, FromOs, Milled, Copts ),
                        Ball,
                               ( 
                                    debug(os_mill, 'Caught: ~w', [Ball]),
                                    debug(os_mill, 'Reverting streams to: ~w', io_streams(In,Ou,Er)),
                                    io_streams( In, Ou, Er ),
                                    debug(os_mill, 'Closing: ~w', [MessAt]),
                                    close(MessAt)
                               )   
                    ),
     debug( os_mill, 'Run output at: ~p', OutsDst ).

os_mill_set_stream( true, MessAt ) :-
    !,
    set_stream( MessAt, tty(true) ).
% defaulty
os_mill_set_stream( _, _MessAt ).

os_mill_callable( Goal, Callable, Opts ) :-
    options( call_module(Mod), Opts ),
    ( Goal = _:_ -> Callable = Goal; Callable = Mod:Goal ).

os_mill_call_opts( true, Callable, File, Milled, Copts ) :-
    debug( os_mill, 'Calling: ~w', [call(Callable,File,Milled,Copts)] ),
    call( Callable, File, Milled, Copts ).
os_mill_call_opts( false, Callable, File, Milled, _Copts ) :-
    debug( os_mill, 'Calling: ~w', [call(Callable,File,Milled)] ),
    call( Callable, File, Milled ).

os_mill_created( true, _Created, _Milled, _File, _Opts ).
os_mill_created( false, Created, Milled, File, _Opts ) :-
    os_mill_created_not( Created, Milled, File ),
    !.
os_mill_created( false, Created, _Milled, _File, _Opts ) :-
    Created \== fail,
    Mismatch = opt_mismatch(created,[false,error,fail,debug]),
    throw( pack_error(os,os_mill/4,Mismatch) ).

os_mill_created_not( true, _Milled, _File ).  % take no action (true = succeed)
os_mill_created_not( error, Milled, File ) :-
    % throw( pack_error(os,os_mill/4,os_created_not(Milled,File)) ).
    throw( os_created_not(Milled,File), [os:os_mill/4] ).
os_mill_created_not( fail, _Milled, _File ) :-
    fail.
os_mill_created_not( debug, Milled, File ) :-
    debug( os_mill, 'Os milled was not created: ~p, (from: ~p)', [Milled,File] ).
