##  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} {} so_explore_descent (@var{file}, @var{init})
##
## Interactively explore a descent file saved by @code{so_save_descent}.
## This opens a shell where the user can navigate through the replay.
##
## The arguments are the same as for @code{so_replay_descent}.  In order
## to allow backwards navigation, @var{file} must either be a file name
## or a file descriptor that allows seeking.
##
## On the initial call, the @code{initialised} handler is called.
## Then, @code{before_step}, @code{direction} and @code{after_step}
## will be called for the currently active data at each navigation
## of the user.  Note that the descent steps on which they are called may be
## out-of-order and may skip intermediate iterations.  The @code{finished}
## handler will be called with the data that is active when the user closes
## the interactive session.
##
## Note that @code{@var{data}.log.steps} and @code{@var{data}.log.costs} will
## always contain @emph{all} steps and costs that have been loaded during
## the replay so far, which may not correspond to the currently active step.
## These fields will be present in all calls to handlers except
## @code{initialised}, not just for the call to @code{finished}.
##
## In addition to the default handlers in @code{handler} of the
## @var{data} struct, also extra handlers can be defined in
## @code{@var{data}.onFirst}.  They will be called @emph{in order} and
## @emph{on each} iteration when it is first loaded from the descent log.
## The @code{finished} handler will be called on the last iteration data
## that has been loaded before the session is terminated.  This may not
## be the active data at that point in time, if the user has navigated
## backwards.  The @code{onFirst} handlers (if present) will be called before
## the corresponding @code{handler} functions.
##
## No on-the-fly computation of extra steps (as it is done by
## @code{so_replay_descent} when not enough steps are saved in the log) is
## supported by this routine.  Instead, it is simply not possible to step
## beyond EOF of the log file.
##
## This routine requires @code{fload} from the @code{parallel} package
## to be available.
##
## @seealso{so_replay_descent, so_run_descent, so_save_descent, fload}
## @end deftypefn

function so_explore_descent (file, init)
  if (nargin () != 2)
    print_usage ();
  endif

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

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

  % Open file and read header.
  if (openFile)
    fd = fopen (file, "rb");
    if (fd == -1)
      error ("failed to open file '%s' for reading the descent log", file);
    endif
  else
    fd = file;
  endif
  header = fload (fd);

  % Call init method on the header.
  data = init (header);

  % Add some (possibly) missing stuff with defaults.
  if (!isfield (data, "log"))
    data.log = struct ();
  endif
  if (!isfield (data, "handler"))
    data.handler = struct ();
  endif
  if (!isfield (data, "onFirst"))
    data.onFirst = struct ();
  endif

  % Load initial state.  During the entire run, we keep track of fseek
  % positions for the state *before* each step's begin.  This is used
  % to later quickly jump to it.  For the first step, this is now.
  filePos = ftell (fd);
  data.s = so_load_compressed (fd, "state", data);

  % Call "initialised" handler and save log stuff.
  data.log.s0 = data.s;
  if (isfield (data.onFirst, "initialised"))
    data.log = data.onFirst.initialised (data);
  endif
  if (isfield (data.handler, "initialised"))
    data.log = data.handler.initialised (data);
  endif
  data.log.costs = [data.s.cost];
  data.log.steps = 0;

  % Start the shell loop.
  k = 0;
  target = 1;
  lastN = 1;
  s = data.s;
  while (true)
    if (ferror (fd))
      error ("error reading descent log");
    endif

    % Read in sequentially until we reach the target.  Seeking to the
    % correct previous position as well as reading the previous state
    % has already been performed -- either above for the initial
    % step or at the end of the loop after a navigation command.
    assert (k < target);
    while (k < target)
      try
        f = fload (fd);
      catch
        if (feof (fd))
          break;
        endif
        error ("error reading descent log");
      end_try_catch

      % Now update data.s.  This ensures that data.s is always
      % set to the correct value (i. e., the old state) when calling
      % any of the onFirst or handler handlers.  The update should
      % be done after the EOF check above and before overwriting s with
      % the newly loaded state below.
      data.s = s;

      dJ = fload (fd);
      step = fload (fd);
      pos = ftell (fd);
      s = so_load_compressed (fd, "state", data);
      if (ferror (fd))
        error ("error reading descent log");
      endif

      ++k;
      assert (k <= length (filePos));
      if (k == length (filePos))
        assert (data.log.steps == k - 1);
        data.log.costs(k + 1) = s.cost;
        data.log.steps = k;
        assert (data.log.steps == length (filePos));
        assert (data.log.steps + 1 == length (data.log.costs));
        filePos(k + 1) = pos;

        if (isfield (data.onFirst, "before_step"))
          data.log = data.onFirst.before_step (k, data);
        endif
        if (isfield (data.onFirst, "direction"))
          data.log = data.onFirst.direction (k, f, dJ, data);
        endif
        if (isfield (data.onFirst, "after_step"))
          data.log = data.onFirst.after_step (k, step, s, data);
        endif
      else
        assert (filePos(k + 1) == pos);
      endif
    endwhile

    % Handle EOF if we have it.
    assert (k <= target);
    if (k < target)
      assert (feof (fd));
      if (k == 0)
        error ("no data in descent log");
      else
        fprintf (stderr (), "no more steps present in the descent log\n");
      endif
    endif

    % Call the actual handlers.
    if (isfield (data.handler, "before_step"))
      data.log = data.handler.before_step (k, data);
    endif
    if (isfield (data.handler, "direction"))
      data.log = data.handler.direction (k, f, dJ, data);
    endif
    if (isfield (data.handler, "after_step"))
      data.log = data.handler.after_step (k, step, s, data);
    endif

    % Prompt for user input.
    fullExit = false;
    do
      try
        cmd = input (sprintf ("@ step %d> ", k), "s");
      catch
        cmd = "exit";
      end_try_catch
      if (strcmp (cmd, "exit") || strcmp (cmd, "quit"))
        fullExit = true;
        break;
      endif

      haveValidN = true;
      relative = true;
      if (strcmp (cmd, ""))
        n = lastN;
      elseif (strcmp (cmd, "start"))
        n = -k;
      elseif (strcmp (cmd, "end"))
        n = Inf;
      elseif (strcmp (cmd, "help"))
        fprintf (stderr (), "\nCommands for descent log explorer:\n\n");
        fprintf (stderr (), "exit    Close the explorer session.\n");
        fprintf (stderr (), "quit    Close the explorer session.\n");
        fprintf (stderr (), "help    Display this help message.\n");
        fprintf (stderr (), "kb      Open Octave shell with 'data' set.\n");
        fprintf (stderr (), "start   Go to the very first iteration.\n");
        fprintf (stderr (), "end     Go to the last iteration.\n");
        fprintf (stderr (), "go N    Go to iteration N.\n");
        fprintf (stderr (), "N       Go N steps forward.\n");
        fprintf (stderr (), "-N      Go N steps backward.\n");
        fprintf (stderr (), "\nIf no command is given, the last navigation");
        fprintf (stderr (), " command is repeated.\n");
        haveValidN = false;
      elseif (strcmp (cmd, "kb"))
        openKbShell (data);
        haveValidN = false;
      else
        if (length (cmd) >= 4 && strcmp (substr (cmd, 1, 3), "go "))
          relative = false;
          cmd = substr (cmd, 4);
        endif

        n = str2double (cmd);
        if (isnan (n) || n != fix (n))
          fprintf (stderr (), "invalid command given\n");
          haveValidN = false;
        elseif (relative)
          lastN = n;
        endif
      endif
    until (haveValidN);
    if (fullExit)
      break;
    endif

    % Find target step number and fix out-of-bounds backward steps.
    assert (haveValidN);
    if (relative)
      target = k + n;
    else
      target = n;
    endif
    if (target < 1)
      fprintf (stderr (), "stepping to beginning\n");
      target = 1;
    endif

    % If we have to go back, seek to the correct position.
    if (target <= k)
      assert (length (filePos) >= target);
      fseek (fd, filePos(target));
      k = target - 1;
      s = fload (fd);
    endif
  endwhile

  % Close the file now.
  if (openFile)
    fclose (fd);
  endif

  % Call the finished handlers.
  if (isfield (data.onFirst, "finished"))
    data.log = data.onFirst.finished (data);
  endif
  if (isfield (data.handler, "finished"))
    data.log = data.handler.finished (data);
  endif
endfunction

% Helper function for the "kb" command.  It creates a new scope
% where the user can safely play around with data without
% messing up anything else.
function openKbShell (data)
  fprintf (stderr (), "You can now interactively explore the current data.\n");
  fprintf (stderr (), "Use the variable 'data' to access all information.\n\n");
  fprintf (stderr (), "Type 'return' to go back to the explore prompt.\n\n");
  keyboard ("kb> ");
endfunction

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

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

% No functional tests, since they require user interaction.
% Use the demo instead for basic testing.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Demo.

%!demo
%!  pkg load parallel;
%!
%!  data = struct ();
%!  data.p = so_init_params (false);
%!  data.p.vol = 10;
%!  data.p.weight = 50;
%!
%!  data.p.nSteps = 10;
%!  data.figs = struct ();
%!  data.figs.speed = figure ();
%!  data.figs.exploreCosts = figure ();
%!
%!  n = 100;
%!  x = linspace (-10, 10, n);
%!  h = x(2) - x(1);
%!  data.g = struct ("x", x, "h", h);
%!
%!  data = so_example_problem (data);
%!  phi0 = ls_genbasic (data.g.x, "box", -3, 7);
%!
%!  printf ("Computing descent...\n");
%!  f = tmpfile ();
%!  d = data;
%!  d.handler = struct ();
%!  d = so_save_descent (f, struct (), d);
%!  s = so_run_descent (data.p.nSteps, phi0, d);
%!  printf ("Final cost: %.6d\n", s.cost);
%!
%!  printf ("\nNow replaying...\n");
%!  init = @() data;
%!  frewind (f);
%!  so_explore_descent (f, init);
%!  fclose (f);
