########################################################################
##
## Copyright (C) 2021 The Octave Project Developers
##
## See the file COPYRIGHT.md in the top-level directory of this
## distribution or <https://octave.org/copyright/>.
##
## This file is part of Octave.
##
## Octave 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.
##
## Octave 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 Octave; see the file COPYING.  If not, see
## <https://www.gnu.org/licenses/>.
##
########################################################################

classdef MException

  ## -*- texinfo -*-
  ## @deftypefn {} {@var{ME} =} MException (@var{id}, @var{template}, @dots{})
  ## Create an object of the MException class that stores error message information.
  ##
  ## @seealso{error}
  ## @end deftypefn

  properties (GetAccess = public, SetAccess = private)
    identifier = '';
    message = '';
    stack = struct([]);
    cause = {};
    Correction = [];
  endproperties

  properties (Access = private, Hidden = true)
    hasBeenCaught = false;
  endproperties

  methods (Access = public)

    function this = MException (id, template, varargin)
      if (nargin == 1 && isstruct (id))
        # Matlab does not have this constructor. Used internally by last().
        this.identifier = id.identifier;
        this.message = id.message;
        this.stack = id.stack;
      else
        if (nargin < 2 || nargout > 1)
          print_usage ();
        endif
        this.identifier = id;
        this.message = sprintf (template, varargin{:});
      endif
      if (! ischar (this.identifier) || (! isempty (this.identifier) && ! isrow (this.identifier)))
        error ("MException: Identifier must be a row string.");
      endif
      if (! ischar (this.message) || (! isempty (this.message) && ! isrow (this.message)))
        error ("MException: Message must be a row string.");
      endif
      if (! isempty(this.identifier) && isempty (regexp(this.identifier,'^([a-zA-Z]\w*)(:[a-zA-Z]\w*)+$')))
        error ("MException: Invalid identifier.");
      endif
    endfunction

    function throw (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} throw (@var{ME})
      ##
      ## @end deftypefn

      rethrow (as_struct (this));
    endfunction

    function rethrow (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} rethrow (@var{ME})
      ##
      ## @end deftypefn

      if (this.hasBeenCaught)
        throw (this);
      else
        error ("MException: Can only rethrow a caught exception.");
      endif
    endfunction

    function throwAsCaller (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} throwAsCaller (@var{ME})
      ##
      ## @end deftypefn

      struct_err = as_struct (this);
      if (numel (struct_err.stack) > 0)
        struct_err.stack = struct_err.stack(end);
      endif
      rethrow (struct_err);
    endfunction

    function obj = addCause (this, ME)

      ## -*- texinfo -*-
      ## @deftypefn {} {@var{ME_C} =} addCause (@var{ME_A}, @var{ME_B})
      ##
      ## @end deftypefn

      obj = this;
      if (! isa (ME, 'MException') || ! isscalar (ME))
        error ("MException: Cause must be a MException scalar object.");
      endif
      obj.cause{end+1} = ME;
    endfunction

    function obj = addCorrection (this, C)

      ## -*- texinfo -*-
      ## @deftypefn {} {@var{ME_C} =} addCorrection (@var{ME}, @var{C})
      ##
      ## @end deftypefn

      obj = this;
      if (! isa (C, 'matlab.lang.correction.Correction') || ! isscalar (C))
        error ("MException: Correction must be a Correction scalar object.");
      endif
      obj.Correction(end+1) = C;
    endfunction

    function report = getReport (this, type, varargin)

      ## -*- texinfo -*-
      ## @deftypefn {} {@var{report} =} getReport (@var{ME}, @var{type}, @dots{})
      ##
      ## @end deftypefn

      hyperlinks = "default";
      if (nargin < 2)
        type = "extended";
      endif
      if (numel (varargin) > 0)
        if (! mod( numel (varargin), 2))
          error ("MException: Parameter missing value.");
        endif
        for (i=1:2:numel(varargin))
          if (! ischar (varargin{i}))
            error ("MException: Expecting pairs of parameters/values.");
          endif
          switch (lower (varargin{i}))
            case "hyperlinks"
              hyperlinks = lower (varargin{i+1});
              if (! ismember (hyperlinks, {"on", "off", "default"}))
                error ("MException: hyperlinks parameter must be one of 'on', 'off', 'default'.");
              endif
            otherwise
              error ("MException: Unknown parameter.");
          endswitch
        endfor
      endif
      switch (type)
        case "basic"
          report = this.message;
        case "extended"
          # need to capture and return the output of throw()
          report = this.message;
        otherwise
          error ("MException: Report type must be 'basic' or 'extended'.");
      endswitch
    endfunction

    function newobj = horzcat (varargin)
      newobj = varargin{1};
      sz = cumsum (cellfun (@(x) size (x, 2), varargin));
      for i=2:numel (varargin)
        newobj(:,sz(i-1)+1:sz(i)) = varargin{i};
      endfor
    endfunction

    function newobj = vertcat (varargin)
      newobj = varargin{1};
      sz = cumsum (cellfun (@(x) size (x, 1), varargin));
      for i=2:numel (varargin)
        newobj(sz(i-1)+1:sz(i),:) = varargin{i};
      endfor
    endfunction

  endmethods

  methods (Access=private)
    function struct_err = as_struct (this)
      if (! isscalar (this))
        error ("MException: Expect a scalar input.");
      endif
      if (isstruct (this))
        struct_err = this;
        return;
      endif
      struct_err = struct (...
        "identifier", this.identifier, ...
        "message", this.message, ...
        "stack", this.stack);
    endfunction
  endmethods

  methods (Static)

    function ME = last (reset)

      ## -*- texinfo -*-
      ## @deftypefn {} {@var{ME} =} MException.last ()
      ## @deftypefnx {} {} MException.last (@qcode{"reset"})
      ## Query or reset the last error message information.
      ##
      ## When called without arguments, return a MException object containing
      ## the last error message and other information related to this error.
      ##
      ## If called with the argument "reset", all fields are set to their
      ## default values.
      ## @end deftypefn

      if (nargin)
        if (ischar (reset) && strcmpi (reset, "reset"))
          if (nargout > 0)
            error ("MException: Too many output arguments.");
          endif
          lasterror ("reset"); # need to reset cause and Correction
        else
          error ("MException: RESET is the only allowed input.");
        endif
      else
        ME = MException (lasterror ()); # use MException(struct_err) constructor
      endif
    endfunction

  endmethods

endclassdef

%!test
%! ME = MException ("octave:error", "error message");
%! identifier = ME.identifier;
%! message = ME.message;
%! stack = ME.stack;
%! cause = ME.cause;
%! Correction = ME.Correction;

## Test MException.last ()
%!test
%! ME = MException.last ();
%! M = ME.last ();
%! MException.last ("reset");

## Test input validation
%!error MException;
%!error MException ("octave:error");
%!error MException ("octave", "error message");
%!error MException ("octave:err!", "error message");
%!error MException ("gnu-octave:error", "error message");
%!error MException ("octave:error-msg", "error message");
%!error MException ("octave:1st", "error message");
%!error MException ("1st:error", "error message");
%!error MException (1, "error message");
%!error MException ("octave:error", NaN);
%!error MException ("octave:error", strvcat ("error", "message"));
%!error MException.last ("random");
%!error ME = MException.last ("reset");
%!error
%! ME = MException ("octave:error", "error message");
%! ME.identifier = "octave:error";
%!error
%! ME = MException ("octave:error", "error message");
%! ME.message = "error message";
%!error
%! ME = MException ("octave:error", "error message");
%! ME.stack = struct();
%!error
%! ME = MException ("octave:error", "error message");
%! ME.cause = [];
%!error
%! ME = MException ("octave:error", "error message");
%! ME.Correction = [];

