/*
  persistence.yap - make assertions and retracts persistent

  Copyright (C) 2006, Christian Thaeter <chth@gmx.net>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2 as
  published by the Free Software Foundation.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, contact me.

*/

:- module(persistence,
	[
		persistent_open/3,
		persistent_close/1,
		persistent_assert/1,
		persistent_retract/1
	]).

:- use_module(library(system),[]).

:- dynamic(persistent_desc/2).

/*
  persistent_open(PredDesc, File, Opts).
  
  declare Module:Functor/Arity (Functor/Arity) to be persistent
  stored in File's (*.db *.log *log.$PID *.lock *.bak)
  
  Opts are:
    db     - use dbfile (flat file containing all persistent predicates)
    log    - use logfile (logfile with either +(Term) for asserts and -(Term) for retracts)
    bak    - make backupfiles when regenerating the dbfile
    sync   - flush data always
    ro     - readonly, can load locked files, never changes data on disk
    wo     - (planned) writeonly, implies [log], data is only written to the log and not
             asserted into prolog, the database will not be loaded at persistent_open.
    conc   - (planned) concurrency, extends the locking for multiple readers/single writer locks
    trans  - (planned) support for transactions (begin/commit/abort)

  Guides:
  - if the data mutates a lot, use [db,log].
  - if you mostly append data [log] suffices.
  - if the data is not important (can be regenerated) and mostly readonly then [db] is ok.
  - when using only [db] you must not forget to persistent_close!
  - for extra security against failures add [bak,sync].
  - don't use [bak] if you need to conserve disk space and the database is huge.
  - don't use [sync] if you need very fast writes.
  - turning all on [db,log,bak,sync] is probably the best, if you are undecided.
  - [ro,db] loads only the last saved db file.
  - [ro,log] loads the last saved db file if it exists and replays the log.
  - note that [ro] will fail if the db is not intact (.bak file present).

  (planned features)
  - [wo] is very limited and only useful if you want to log data to a file
  - [wo,db] will replay the log at close
  - [conc] is useful for shareing data between prolog processes, but this is not a
    high performance solution.
  - [trans] can improve performance of concurrent access somewhat
*/
persistent_open(PredDesc, File, Opts) :-
	module_goal(PredDesc, Module:Functor/Arity),
	atom(Functor), integer(Arity), atom(File),
	\+ persistent_desc(Module:Functor/Arity,_),

	atom_concat(File,'.db',DBfile),
	assertz(persistent_desc(Module:Functor/Arity,dbfile(DBfile))),

	atom_concat(File,'.bak',Backupfile),
	assertz(persistent_desc(Module:Functor/Arity,backupfile(Backupfile))),

        atom_concat(File,'.log',Logfile),
	assertz(persistent_desc(Module:Functor/Arity,logfile(Logfile))),

        system:pid(Pid),
	assertz(persistent_desc(Module:Functor/Arity,pid(Pid))),

        number_atom(Pid,P),
        atom_concat(Logfile,P,Mylogfile),
	assertz(persistent_desc(Module:Functor/Arity,mylogfile(Mylogfile))),

        atom_concat(File,'.lock',Lockfile),
	assertz(persistent_desc(Module:Functor/Arity,lockfile(Lockfile))),

        persistent_opts_store(Module:Functor/Arity,Opts),
	persistent_load(Module:Functor/Arity),

        (       \+ persistent_desc(Module:Functor/Arity, ro), persistent_desc(Module:Functor/Arity, log)
        ->      open(Logfile, append, Log),
                assertz(persistent_desc(Module:Functor/Arity,logstream(Log)))
        ;       true
        ).

/*
  closes the database associated with PredDesc ([Module:]Functor/Arity)
*/
persistent_close(PredDesc0) :-
	module_goal(PredDesc0,PredDesc),
        (       persistent_desc(PredDesc, logstream(Log))
        ->      close(Log)
        ;       true
        ),
        persistent_save(PredDesc),
        persistent_desc(PredDesc, backupfile(Backupfile)),
        (system:delete_file(Backupfile,[ignore]); true),
        persistent_lock_release(PredDesc),
        retractall(persistent_desc(PredDesc,_)).

/*
  assert data to the database, this is always an assertz, if you need some ordering,
  then store some kind of key within your data.
  rules can be asserted too
*/
persistent_assert(Term) :-
        Term = (Head0 :- Body),
	module_goal(Head0, Module:Head),
        functor(Head, Functor, Arity),
	once(persistent_desc(Module:Functor/Arity,_)),!,
        (       persistent_desc(Module:Functor/Arity, logstream(Log))
        ->      writeq(Log,+(((Module:Head):-Body))), write(Log,'.\n'),
                (       persistent_desc(Module:Functor/Arity, sync)
                ->      flush_output(Log)
                ;       true
                )
        ;       true
        ),
        assertz((Module:Head:-Body)).
persistent_assert(Term0) :-
	module_goal(Term0, Module:Term),
        functor(Term,Functor,Arity),
	once(persistent_desc(Module:Functor/Arity,_)),!,
        (       persistent_desc(Module:Functor/Arity,logstream(Log))
        ->      writeq(Log,+(Module:Term)), write(Log,'.\n'),
                (       persistent_desc(Module:Functor/Arity, sync)
                ->      flush_output(Log)
                ;       true
                )
        ;       true
        ),
        assertz(Module:Term).

/*
  retract a persistent Term
*/
persistent_retract(Term0) :-
	module_goal(Term0, Module:Term),
        functor(Term,Functor,Arity),
        once(persistent_desc(Module:Functor/Arity,_)),!,
        retract(Module:Term),
        (       persistent_desc(Module:Functor/Arity, logstream(Log))
        ->      writeq(Log,-(Module:Term)), write(Log,'.\n'),
                (       persistent_desc(Module:Functor/Arity, sync)
                ->      flush_output(Log)
                ;       true
                )
        ;       true
        ).

% transaction support for future
persistent_begin.
persistent_commit.
persistent_abort.


/*

  PRIVATE PREDICATES, DONT USE THESE

*/

% save all data to a .db file
persistent_save(PredDesc) :-
        \+  persistent_desc(PredDesc,ro),
	(       persistent_desc(PredDesc,db)
	->	persistent_desc(PredDesc,dbfile(DBfile)),
		(
                        persistent_desc(PredDesc,bak)
                ->      persistent_desc(PredDesc,backupfile(Backupfile)),
                        (       system:file_exists(DBfile)
                        ->      system:rename_file(DBfile,Backupfile)
                        ;       true
                        )
                ;       true
                ),
                open(DBfile, write, S),
                persistent_writeall(PredDesc,S),
                close(S),
                persistent_desc(PredDesc,logfile(Logfile)),
                (system:delete_file(Logfile,[ignore]); true)
        ;       true
        ).

% write all predicates matching Functor/Arity to stream S
persistent_writeall(PredDesc, S) :-
	module_goal(PredDesc, Module:Functor/Arity),
        functor(Clause, Functor, Arity),
        clause(Module:Clause, Body),
        (       Body = true
        ->      writeq(S,Module:Clause)
        ;       writeq(S,(Module:Clause:-Body))
        ),
        write(S,'.\n'),
        fail.
persistent_writeall(_,_).

% load a database, recover logfile, recreate .db
persistent_load(PredDesc) :-
	persistent_desc(PredDesc,dbfile(DBfile)),
	persistent_desc(PredDesc,backupfile(Backupfile)),
	persistent_desc(PredDesc,logfile(Logfile)),

        (       persistent_desc(PredDesc,ro)
        ->      \+ system:file_exists(Backupfile),
                (       system:file_exists(DBfile)
                ->      persistent_load_file(DBfile)
                ;       true
                ),
                (       persistent_desc(PredDesc,log), system:file_exists(Logfile)
                ->      persistent_load_file(Logfile)
                ;       true
                )
        ;
                persistent_lock_exclusive(PredDesc),
                (       system:file_exists(Backupfile)
                ->      system:rename_file(Backupfile, DBfile)
                ;       true
                ),
                (       system:file_exists(DBfile)
                ->      persistent_load_file(DBfile)
                ;       true
                ),
                (       system:file_exists(Logfile)
                ->      persistent_load_file(Logfile),
                        (       persistent_desc(PredDesc, db)
                        ->      persistent_save(PredDesc)
                        ;       true
                        )
                ;       true
                )
        ).

% load a .db file or replay a .log file
persistent_load_file(File) :-
        open(File, read, S),
        repeat,
        read(S, TermIn),
        (
                TermIn == end_of_file,
                close(S),
                !
        ;
                (
                        TermIn = +(Term),
                        assertz(Term)
                ;
                        TermIn = -(Term),
                        retract(Term)
                ;
                        assertz(TermIn)
                ),
                fail
        ).

%lock handling, so far only exclusive locks
persistent_lock_exclusive(PredDesc) :-
	persistent_desc(PredDesc,lockfile(Lockfile)),
	persistent_desc(PredDesc,pid(Pid)),
        open(Lockfile, append, Lockappend),
        write(Lockappend,lock(write,Pid)),write(Lockappend,'.\n'),
        close(Lockappend),
        open(Lockfile, read, Lockread),
        read(Lockread,LPid),
        close(Lockread),
        LPid = lock(_,Pid).

% recover lock
persistent_lock_exclusive(PredDesc) :-
	persistent_desc(PredDesc, lockfile(Lockfile)),
        open(Lockfile, read, Lockread),
        read(Lockread,lock(_,LPid)),
        close(Lockread),
        \+ catch(kill(LPid,0),_,fail),
        (system:delete_file(Lockfile,[ignore]); true),
        %system:sleep(1),
        persistent_lock_exclusive(PredDesc).

persistent_lock_release(PredDesc) :-
	persistent_lock_exclusive(PredDesc),
	persistent_desc(PredDesc,lockfile(Lockfile)),
        (system:delete_file(Lockfile,[ignore]); true).


persistent_opts_store(_,[]).
persistent_opts_store(PredDesc,[H|T]) :-
	assertz(persistent_desc(PredDesc,H)),
	persistent_opts_store(PredDesc,T).

module_goal(Module:Goal,Module:Goal) :-
	callable(Goal), nonvar(Module),!.
module_goal(Goal,Module:Goal) :-
	callable(Goal), prolog_flag(typein_module,Module).