:- lib( os_sep/2 ).
:- lib( os_name/2 ).

os_postfix_defaults( Args, Defs ) :-
    Defs = [ sep(Sep),
             replace(false),
             ignore_post([]),
             ext(_),
             with_ext(_)
            ],
    os_sep( Sep, Args ).

%% os_postfix( -Postfix, +Posted ).
%% os_postfix( +Postfix, +Fname, -Posted ).
%% os_postfix( +Postfix, -Fname, +Posted ).
%% os_postfix( +Postfix, ?Fname, ?Posted, +Opts ).
%% os_postfix( +Postfix, +Opts, ?Fname, ?Posted ).
%
% Append a Postfix atom (or list of postfix atoms) to a filename without touching its file type extension.
% 
% Also works for removing a postfix.
% If Postfix is compound of arity/1 is taken to be an aliased path, in which case the innermost path is extended.
%
% Second argument is allowed to be the options (recognised as such when input is a list)
% so that it can be used in meta-calls.
%
% Empty atom _''_ is a special postfix where no separator and postfix are added. If you really want the empty postfix, use _['']_ (see examples).
% Opts 
%  * ext(Ext)
%    if Ext is ground is assumed to be the strippable part of Fname. When Ext is a variable
%    the found extension is bound to it. This option is for testing and returning, see with_ext() for setting alternative extensions.
% 
%  * ignore_post(Posts=[])
%    (separated) parts to ignore at end (see ex. below)
% 
%  * with_ext(WithExt)
%    replace extension of Fname with WithExt, if ground (you can pick up the old one using an unbound varible in ext(Ext) above)
% 
%  * postfix(Psfx)
%    alternative way of defining Postfix, only used when Postfix is a variable
% 
%  * replace(Rplc=false)
%    replace relevant part of filename instead of adding new postfix
% 
%  * separator(Sep=Sep)    
%    expansion of sep(Sep) - canonical is sep(Sep)
% 
%  * sep(Sep='_')
%    shortened separator() separator for stem-file parts (see os_sep/2)
%
%==
% ?- os_postfix(abc, library(x.txt), T).
% T = library(x_abc.txt).
% ?- os_postfix(abc, library(x.txt), T, [separator(-)]).
% T = library('x-abc.txt').
% ?- os_postfix(abc, x.txt, T, sep(.)).
% T = x.abc.txt.
% ?- os_postfix(v1, graph_layout.csv, T, [ignore_post(layout)]).
% T = graph_v1_layout.csv.
% ?- os_postfix(v1, graph_lay_out.csv, T, [ignore_post(layout)]).
% T = graph_lay_out_v1.csv.
% ?- os_postfix(v1, graph_lay_out.csv, T, ignore_post([lay,out])).
% T = graph_v1_lay_out.csv.
% ?- os_postfix(v1, graph_lay_out.csv, T, [ignore_post([out]),replace(true)]).
% T = graph_v1_out.csv
% ?- maplist(os_postfix(v1,[sep(-)]),[a.csv,b.csv], AB).
% AB = ['a-v1.csv', 'b-v1.csv'].
% ?- os_postfix(_, library(x.txt), T, postfix(abc)).
% T = library(x_abc.txt).
% ?- os_postfix( _, "x.txt", T, postfix(abc) ).
% T = "x_abc.txt".
% ?- os_postfix(Psf, abc_def.txt).
% Psf = def.
% ?- os_postfix(bit, by.csv, ByBit, with_ext(txt)).
% ByBit = by_bit.txt.
% ?- os_postfix([by,bit], bit.csv, ByBit, with_ext(txt)).
% ByBit = bit_by_bit.txt.
% ?- os_postfix(by_bit, bit.csv, ByBit, with_ext(txt)).
% ByBit = bit_by_bit.txt.
% ?- os_postfix('', bit.csv, Sfxd).
% Sfxd = bit.csv
% ?- os_postfix('', bit.csv, Sfxd).
% Sfxd = bit.csv.
%==
%
% As of v0.5
%==
% ?- os_postfix([''], bit.csv, Sfxd).
% Sfxd = bit_.csv.
%
% ?- os_postfix(abc, A, B).
% ERROR: os_lib:os_postfix/4: Ground argument expected in one of the positions : [2,3], but found: [_29228,_29234]
% 
% ?- set_prolog_flag(pack_errors_arg, false).
% ?- os_postfix(abc, A, B).
% ERROR: os_lib:os_postfix/4: Ground argument expected in one of the positions: [2,3]
% 
% ?- os_postfix('', Stem, bit.csv).
% Stem = bit.csv.
% 
% ?- os_postfix('', Stem, 'bit_.csv').
% Stem = bit.csv.
% 
% ?- os_postfix([''], Stem, 'bit_.csv').
% Stem = bit.csv.
% 
% ?- os_postfix([''], Stem, 'bit.csv').
% false.
%==
%
% @author nicos angelopoulos
% @version  0.2 2014/7/8   changed order of 1&2 make it more suitable to meta calls
% @version  0.3 2014/7/28  added aliased paths and example
% @version  0.4 2014/12/2  added options (separator/1,ignore_post/1,replace/1)
% @version  0.5 2024/12/13 changed fail to ground error, deconstruct '', examples of these + couple on standards
%
os_postfix( Psfx, Posted ) :-
    os_postfix( Psfx, _Fname, Posted, [] ).
os_postfix( Psfx, Fname, Posted ) :-
    os_postfix( Psfx, Fname, Posted, [] ).

os_postfix( Psfx, Args, Fname, Posted ) :-
    ground( Args ),
    is_list(Args),
    !,
    os_postfix_reorder( Psfx, Fname, Posted, Args ).
os_postfix( Psfx, Fname, Posted, Args ) :-
    options_append( os_postfix, Args, Opts ),
    os_postfix_1( Psfx, Fname, Posted, Opts ).

os_postfix_1( Psfx, Fname, Posted, Opts ) :-
    ground( Fname ),
    !,
    os_name( Fname, Ftype ),
    os_postfix( Ftype, Psfx, Fname, Posted, Opts ).
    % os_cast( Fname, +(FnAtm) ),
    % os_postfix_atom( FnAtm, Psfx, PsfFAtm, Opts ),
    % os_cast( Ftype, PsfFAtm, Posted ).
os_postfix_1( Psfx, Fname, Posted, Opts ) :-
    ground( Posted ),
    !,
    os_dir_stem_ext( Dir, PoStem, Ext, Posted ),
    os_sep( Sep, Opts ),
    at_con( Parts, Sep, PoStem ),
    options( ignore_post(Igns), Opts, en_list(true) ),
    append( _Short, Long, Igns ),
    append( Clean, Long, Parts ),      %  two appends always succeed at least once
    os_postfix_parts( Psfx, Sep, PsfxParts ),
    append( Withouts, PsfxParts, Clean ),
    !,
    at_con( Withouts, Sep, Fstem ),
    os_dir_stem_ext( Dir, Fstem, Ext, Fname ).
os_postfix_1( _Psfx, Fname, Posted, _Opts ) :-
     throw( arg_ground_in_one_of([2,3],[Fname,Posted]), os_lib:os_postfix/4 ).

os_postfix( atom, Psfx, File, Posted, Opts ) :-
    os_postfix_atom( File, Psfx, Pile, Opts ),
    os_cast( atom, Pile, Posted ).
os_postfix( string, Psfx, File, Posted, Opts ) :-
    os_postfix_string( File, Psfx, Pile, Opts ),
    os_cast( string, Pile, Posted ).
os_postfix( slash, Psfx, Fname, Posted, Opts ) :-
    Fname = Dir/File,
    os_postfix_atom( File, Psfx, Pile, Opts ),
    os_cast( slash, Dir/Pile, Posted ).
os_postfix( alias, Psfx, Fname, Posted, Opts ) :-
    Fname =.. [Alias,AArg],
    os_postfix_1( Psfx, AArg, PArg, Opts ),
    Pname =.. [Alias,PArg], 
    os_cast( alias, Pname, Posted ).

os_postfix_parts( Psfx, Sep, PsfxParts ) :-
    ground( Psfx ),
    !,
    ( Psfx == '' -> PsfxParts = []
                  ; 
                  ( is_list(Psfx) -> 
                                    Psfx = PsfxParts
                                  ; at_con( PsfxParts, Sep, Psfx )
                  )
    ).    %  postfix might itself contain Sep
os_postfix_parts( Psfx, _Sep, [Psfx] ). % when non-ground

os_postfix_reorder( _Psfx, Fname, _Posted, Opts ) :-
    is_list( Fname ),
    !,
    throw( pack_error(os,os_postfix_lists(Fname,Opts)) ).
os_postfix_reorder( Psfx, Fname, Posted, Args ) :-
    options_append( os_postfix, Args, Opts ),
    os_postfix_1( Psfx, Fname, Posted, Opts ).

os_postfix_atom( File, PostfixPrv, Pile, Opts ) :-
    % fixme: allow all_dots(true) as an option that jumps all extensions ?
    options( ext(Ext), Opts ),
    os_ext( Ext, Stem, File ),
     % file_name_extension( Stem, Ext, File ),
    options( [sep(Sep),replace(Rep)], Opts ),
    ( is_list(PostfixPrv) -> atomic_list_concat(PostfixPrv,Sep,Postfix) ; PostfixPrv = Postfix ),
    os_postfix_stem( Stem, Sep, Rep, Prefix, Suffix, Opts ),
    os_postfix_ground( Postfix, Opts ),
    ( PostfixPrv==[''] -> 
          ( Suffix=='' -> 
               atomic_list_concat( [Prefix,Postfix], Sep, PoStem )
               ;
               atomic_list_concat( [Prefix,Postfix,Suffix], Sep, PoStem )
          )
          ;
          at_con( [Prefix,Postfix,Suffix], Sep, PoStem )
    ),
    file_name_extension( PoStem, Ext, Xile ),
    os_postfix_with_ext( Xile, Pile, Opts ).

os_postfix_string( File, PostfixPrv, Pile, Opts ) :-
    options( ext(Ext), Opts ),
    os_ext( Ext, Stem, File ),
    options( [sep(Sep),replace(Rep)], Opts ),
    ( is_list(PostfixPrv) -> atomic_list_concat(PostfixPrv,Sep,Postfix) ; PostfixPrv = Postfix ),
    os_postfix_stem( Stem, Sep, Rep, Prefix, Suffix, Opts ),
    os_postfix_ground( Postfix, Opts ),
    at_con( [Prefix,Postfix,Suffix], Sep, PoStem ),
    os_ext( Ext, PoStem, Pile ).

os_postfix_with_ext( Xile, Pile, Opts ) :-
    options( with_ext(WithExt), Opts ),
    holds( ground(WithExt), WithExtB ),
    os_postfix_atom_ext( WithExtB, WithExt, Xile, Pile ).

os_postfix_atom_ext( true, WithExt, Xile, Pile ) :-
    os_ext( _, WithExt, Xile, Pile ).
os_postfix_atom_ext( false, _WithExt, Pile, Pile ).

os_postfix_stem( Stem, Sep, Rep, Prefix, Suffix, Opts ) :-
    atomic_list_concat( StemParts, Sep, Stem ),
    options( ignore_post(IgnS), Opts ),
    en_list( IgnS, Igns ),
    os_postfix_stem_ignore( StemParts, Sep, Rep, Igns, Prefix, Suffix ).

os_postfix_stem_ignore( Parts, Sep, Rep, Igns, Prefix, Suffix ) :-
    Igns \== [],
    append( PrefixParts, Igns, Parts ),
    !,
    os_postfix_stem_replace( Rep, PrefixParts, RemainingParts ),
    at_con( RemainingParts, Sep, Prefix ),
    at_con( Igns, Sep, Suffix ).
os_postfix_stem_ignore( PrefixParts, Sep, Rep, _Igns, Prefix, '' ) :-
    os_postfix_stem_replace( Rep, PrefixParts, RemainingParts ),
    at_con( RemainingParts, Sep, Prefix ).

os_postfix_stem_replace( true, PrefixParts, RemainingParts ) :-
    append( RemainingParts, [_Last], PrefixParts ),
    !.
os_postfix_stem_replace( false, Parts, Parts ).

os_postfix_ground( Postfix, _Opts ) :-
    \+ var( Postfix ),
    !.
os_postfix_ground( Postfix, Opts ) :-
    options( postfix(Postfix), Opts ). % fixme: message will be a bit imprecise if missing
