##  Copyright (C) 2015  Daniel Kraft <d@domob.eu>
##  GNU Octave level-set package.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  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, see <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn  {Function File} {@var{data} =} so_save_descent (@var{file}, @var{header}, @var{data})
##
## Update handlers to save the descent run to a file.  The existing handlers
## in @var{data} will be kept, but additional logic will be added to them
## that saves all data computed during the descent run to a file.  This
## file can then be used with @code{so_replay_descent}.
##
## @var{file} can either be a file ID (integer) or file name (string).
## In the latter case, the file will be created in @qcode{"wb"} mode
## and closed after the descent run.  @var{header} should be some data
## (possibly a struct) that will be written as the ``file header''.
## It can contain data that identifies the problem solved and all
## parameter values.  It must be possible to reproduce a problem's @var{data}
## structure from this header when replaying the descent.
##
## The state struct can be optionally ``compressed'' before it is written,
## in case it contains excessive amounts of data that is not necessary
## for the replay (for instance, because it is cheap to re-compute).
## If this feature should be used, a function must be defined that translates
## the state struct accordingly.  It should be stored in
## @code{@var{data}.compress.save.state}, and will be called
## with arguments @code{(@var{s}, @var{data})}.  It should return
## the state struct in the form to save it.
##
## This routine requires @code{fsave} from the @code{parallel} package
## to be available.  Note that the files are not guaranteed to be interoperable
## between machines or package versions.
##
## @seealso{so_run_descent, so_replay_descent, so_explore_descent, fsave}
## @end deftypefn

% Format of the descent log (all variables saved with 'fsave'):
%
%   header
%   initial state struct
%   for each step:
%     speed field f
%     dJ for f
%     step length taken
%     new state struct
%
% This format, in particular that the new state is the very last
% item and the initial state struct the last part of the "header",
% ensures that each actual step data is preceded by the "old" state.
% Thus, if we start reading actually there, we get information for each state
% that includes both the old and the new state.

function data = so_save_descent (file, header, data)
  if (nargin () != 3)
    print_usage ();
  endif
  if (!isstruct (data))
    error ("DATA should be a struct");
  endif

  openFile = ischar (file);
  if (!openFile && (!isnumeric (file) || !isscalar (file)))
    error ("FILE must be a string or file ID");
  endif

  hasFsave = exist ("fsave");
  if (hasFsave != 2 && hasFsave != 3)
    error ("'fsave' is not available");
  endif

  % Retain old handlers (if present).
  if (!isfield (data, "handler"))
    data.handler = struct ();
  endif
  oldHandlers = data.handler;

  % Set defaults for old handlers.
  if (!isfield (oldHandlers, "initialised"))
    oldHandlers.initialised = @(data) data.log;
  endif
  if (!isfield (oldHandlers, "direction"))
    oldHandlers.direction = @(k, f, dJ, data) data.log;
  endif
  if (!isfield (oldHandlers, "after_step"))
    oldHandlers.after_step = @(k, t, s, data) data.log;
  endif
  if (!isfield (oldHandlers, "finished"))
    oldHandlers.finished = @(data) data.log;
  endif

  % Set new handlers.
  args = struct ("openFile", openFile, "file", file, ...
                 "header", header, "oldHandlers", oldHandlers);
  data.handler.initialised = @(data) initialised (args, data);
  data.handler.direction = @(k, f, dJ, data) direction (args, k, f, dJ, data);
  data.handler.after_step = @(k, t, s, data) afterStep (args, k, t, s, data);
  data.handler.finished = @(data) finished (args, data);
endfunction

% Save the given state struct, compressing it before if necessary.
function saveStateStruct (fd, s, data)
  if (isfield (data, "compress") && isfield (data.compress, "save")
        && isfield (data.compress.save, "state"))
    s = data.compress.save.state (s, data);
  endif

  fsave (fd, s);
endfunction

% Initialisation handler.  This is used to open the file (if required)
% and to write the header.  It also saves the initial state struct.
function log = initialised (args, data)
  if (args.openFile)
    fd = fopen (args.file, "wb");
    if (fd == -1)
      error ("failed to open file '%s' for saving the descent log", args.file);
    endif
  else
    fd = args.file;
  endif

  fsave (fd, args.header);
  saveStateStruct (fd, data.s, data);
  data.log._saveDescentFd = fd;

  log = args.oldHandlers.initialised (data);
endfunction

% Save direction information when known.
function log = direction (args, k, f, dJ, data)
  fd = data.log._saveDescentFd;
  fsave (fd, f);
  fsave (fd, dJ);
  log = args.oldHandlers.direction (k, f, dJ, data);
endfunction

% Save new state and step length.  The new state should be the very
% last information saved for a step.
function log = afterStep (args, k, t, s, data)
  fd = data.log._saveDescentFd;
  fsave (fd, t);
  saveStateStruct (fd, s, data);
  log = args.oldHandlers.after_step (k, t, s, data);
endfunction

% Finished handler, closes the file (if applicable).
function log = finished (args, data)
  if (args.openFile)
    fclose (data.log._saveDescentFd);
  endif
  log = args.oldHandlers.finished (data);
endfunction

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Tests.

% Test for errors.
%!error <Invalid call to>
%!  so_save_descent (1, 2);
%!error <Invalid call to>
%!  so_save_descent (1, 2, 3, 4);
%!error <DATA should be a struct>
%!  so_save_descent (1, 2, 3);
%!error <FILE must be a string or file ID>
%!  so_save_descent (struct (), 2, struct ());
%!error <FILE must be a string or file ID>
%!  so_save_descent ([2, 3], 2, struct ());
%!error <'fsave' is not available>
%!  pkg unload parallel;
%!  so_save_descent ("foo", 2, struct ());

% Working calls.
%!test
%!  pkg load parallel;
%!  so_save_descent ("foo", 2, struct ());
%!  so_save_descent (5, 2, struct ());

% Funtional tests and a demo are in so_replay_descent.
