%GETPARAMS   Automated parameter parsing for DIPimage functions
%   VARARGOUT = GETPARAMS(DEFS,VARARGIN)
%    Retrieves the parameters in VARARGIN according to the expected
%    values in DEFS. Puts a variable PARAMERROR into the caller's
%    workspace, which will contain a diagnostic string in case this
%    function willingly generates an error.
%
%    If varargin contains the string '?', an interactive parameter entry
%    is started. This feature is DEPRECATED, and will be removed in a
%    future version.
%
%   MSG = GETPARAMS('test',DEFS)
%    Tests the DEFS structure for correctness. Returns a string in MSG
%    if there is an error, otherwise MSG is empty.
%
% --DEFINITION OF THE DEFS STRUCTURE -----------------------------------------
%
% DEFS is a struct array. It contains the following fields:
%   menu           String with name of menu to place function in
%   display        String with display name of function in menu
%   inparams       Structure array with input parameters
%   outparams      Structure array with output parameters
%   output_select  Boolean value, adds an input argument that selects
%                  which of the output arguments to generate.
%
% defs.inparams contains the following fields for each parameter:
%   name           String containing the variable's name
%   description    String containing a description to show the user
%   type           String containing the data type
%   dim_check      Points to the argument that determines the dimensionality
%   range_check    Array with range limits
%   required       Boolean value
%   default        Default value used if none specified
%
% defs.outparams contains the following fields for each parameter:
%   name           String containing the variable's name
%   description    String containing a description to show the user
%   type           String containing the data type
%   suppress       (optional) Boolean value
%
% NAME:        Not used for now.
%
% DESCRIPTION: This is a clue for the user as to what to enter.
%
% TYPE:        One of: 'image','dataset','measurement','array','anytypearray','measureid',
%              'option','optionarray','infile','outfile','indir','handle','string',
%              'boolean','cellarray'. The difference between 'infile' and 'outfile' is
%              in the dialog box displayed (UIGETFILE or UIPUTFILE). 'indir' displayes
%              a dialog box to select a directory (UIGETDIR).
%
% DIM_CHECK:   - If TYPE is 'array', this specifies the dimensions of the array in
%              one of two ways: 1) it points to an image parameter whose dimensionality
%              dictates the length of this vector, or 2) it directly gives the expected
%              SIZE, with -1 values for the dimensions where there is no size
%              requirements. Furthermore, if DIM_CHECK is 0, a scalar is expected, if it
%              is -1, any length vector is allowed, including the empty array, and if it
%              is [], an empty array is expected. If various options are possible, put
%              them into a cell array: {[],[1,3],[3,3]}. The options are checked in turn,
%              the first fit is used. If no fit is found, the error for the last option
%              is returned.
%              - If TYPE is 'image', DIM_CHECK defines a range for the image dimensionality:
%              [MIN,MAX]. [] and 0 map to [0,Inf], and N maps to [N,N]. For example, to allow
%              only 2D images, specify either 2 or [2,2].
%              - If TYPE is 'measureid', it points to the measurement parameter from which
%              a measurement ID should be selected.
%              - If TYPE is 'optionarray', a value of 0 indicates an empty cell array is
%              allowed; a value of 1 indicates at least 1 option is expected.
%              - For other TYPEs, this value is meaningless.
%
% RANGE_CHECK: - If TYPE is 'array', [min,max] is the range for the parameter. An empty
%              array indicates "anything goes". Shortcut strings:
%                Integer types: 'N+' = [1 Inf]. 'N-' = [-Inf -1]. 'N' = [0 Inf], 'Z' = [].
%                Real types: 'R' = []. 'R+' = [0 Inf]. 'R-' = [-Inf 0].
%              - If TYPE is 'image', RANGE_CHECK is a string or a cell array with
%              strings that defines the allowed data types and whether the image is
%              expected to be scalar, color, etc. Valid is any combination of dip_image
%              data types plus these aliases: 'float', 'signed', 'sint', 'unsigned',
%              'uint', 'integer','int', 'real', 'noncomplex', 'complex', 'any'.
%              The parameter can specify at most one of these: 'scalar', 'tensor',
%              'vector', 'color' or 'array'. If none is specified, 'tensor' is assumed.
%              If this value is set to [], {'all','tensor'} is assumed.
%              - If TYPE is 'option' or 'optionarray', RANGE_CHECK is a cell array
%              with possible options: {0,1},{'rectangular','elliptic','parabolic'},
%              etc. OR it is a struct with possible options together with
%              a definition: s(1).name = 'name' ; s(1).description = 'description'.
%              - If TYPE is 'infile' or 'outfile', RANGE_CHECK is a string
%              containing the mask for the filename.
%              - If TYPE is 'handle', RANGE_CHECK is a cell array with strings that
%              specify the type of figure window required. See PRIVATE/HANDLELIST for
%              information on the format.
%
% REQUIRED:    If this value is non-zero (TRUE), then the parameter cannot be
%              missing from varargin. DEFAULT is then meaningless. 'option',
%              'optionarray' and 'measureid' should *always* have zero here.
%
% DEFAULT:     This is the value of the default taken. If TYPE is 'image',
%              'dataset' or 'measurement', it is the name of a variable in the
%              base workspace.
%              - If TYPE is 'measureid', the default is the first measurement id
%              in the dip_measurement variable; this value is not used.
%              - If TYPE is 'option', it is one of the options in the array.
%              - If TYPE is 'optionarray', it is either one of the options in the
%              array, or a cell array with some of the options in the array, or
%              an empty cell array.
%              - If TYPE is 'handle', GCF is the default; this value is not used.
%              - If TYPE is 'boolean', can be either 'yes', 'no', 1 or 0.
%
% SUPPRESS:    Set to 1 to avoid displaying the output when the command is executed
%              from the GUI. Most functions do not have this value in their DEFS
%              struct. It defaults to 0.
%
% ----------------------------------------------------------------------------
%
% For more information, read the DIPimage user guide.

% (C) Copyright 1999-2009               Pattern Recognition Group
%     All rights reserved               Faculty of Applied Physics
%                                       Delft University of Technology
%                                       Lorentzweg 1
%                                       2628 CJ Delft
%                                       The Netherlands
%
% Cris Luengo, April-June 2000.
% 9 August 2000:     Added support for DATASET in the toolbox.
% 2 September 2000:  Corrected bug that didn't allow default vector 'array'.
% 19 September 2000: Added support for MEASUREMENT in the toolbox.
% 5 May 2001:        Added the 'handle' data type.
% 8 August 2001:     'array' with DIM_CHECK == 0 wasn't checked in the second syntax.
% 15 August 2001:    Added the 'string' and 'boolean' data types.
% 26 August 2001:    'handle' parsing now done by GETFIGH.
% 30 September 2001: Added RANGE_CHECK for 'handle'.
% 18 December 2001:  Integer ARRAY can be Inf.
% 6 March 2002:      'option' and 'optionarray' now support a description list as well.
% 15 November 2002:  New style GETPARAMS. Not much of the actual code has changed.
%                    Merged TESTDEFS and TESTPARAM into this file. Minimized the
%                    number of dependent private functions. Changed the DEFS struct
%                    in TESTDEFS to avoid more parsing during other tasks.
% 12 September 2003: 'optionarray' now uses 'dim_check' to allow for empty cell arrays.
% 20 July 2004:      Added TYPE == 'measureid'.
% 12 Oct 2004:       Added TYPE == 'indir' (BR)
% 11 July 2005:      Added TYPE == 'cellarray' (BR & CL)
% 31 August 2007:    Removed the GETPARAMS('test',DEFS.INPARAMS(I),VALUE) syntax.
% 16 September 2007: Deprecated the '?' input argument to all DIPimage functions.
%                    Split out code from this function into PARAMTYPE and sub-functions.
% 18 September 2007: Removed the GETPARAMS('test') syntax with two output arguments.
% 24 July 2009:      Added optional DEFS.OUTPARAMS.SUPPRESS value.
% 30 July 2009:      Changed help to reflect changes in PARAMTYPE_IMAGE.

function varargout = getparams(defs,varargin)
varargout = {};
if nargin>1 & ischar(defs) & strcmpi(defs,'test')
   if nargin~=2
      error('Too many input arguments.')
   end
   if nargout>1
      error('Too many output arguments.')
   end
   varargout{1} = testdefs(varargin{1});
   return
elseif nargin<1 | ~isstruct(defs)
   error('Expecting a DEFS structure as input.')
end
assignin('caller','paramerror','')
msg = testdefs(defs);
if ~isempty(msg)
   error(['DEFS structure incorrect (',msg,').'])
end
M = nargin-1;
if isfield(defs,'inparams')
   params = defs.inparams;
else
   params = [];
end
if isfield(defs,'output_select')
   selectout = defs.output_select;
else
   selectout = 0;
end
N = length(params);

if selectout
   if M > N+1
      assignin('caller','paramerror','Too many input variables')
      error('Too many input variables.')
   end
   if N+1 ~= nargout
      error('Incorrect number of output variables.')
   end
else
   if M > N
      assignin('caller','paramerror','Too many input variables')
      error('Too many input variables.')
   end
   if N ~= nargout
      error('Incorrect number of output variables.')
   end
end
question_mode = 0;   % This value is set to non-zero when the interactive
                     % mode is entered. This is accomplished by a question
                     % mark on the command line.
% Iterate over the requested parameters:
for ii=1:N
   if ii > M
      % Input values exhausted. Use defaults
      if question_mode
         varargout{ii} = ask(params(ii),'?',varargout);
      else
         if params(ii).required
            assignin('caller','paramerror','Required parameter missing')
            error('Required parameter missing.')
         end
         try
            varargout{ii} = paramtype('default_value',params(ii));
         catch
            assignin('caller','paramerror','Default value evaluation failed.')
            error('Default value evaluation failed.')
         end
      end
   else
      % Use input values.
      if ischar(varargin{ii}) & strcmp(varargin{ii},'?')
         question_mode = 1;
         warning('The question-mark argument functionality is deprecated. Please use the DIPimage GUI or type HELP <command> to learn how to use this function.')
         varargout{ii} = ask(params(ii),'?',varargout);
      else
         if question_mode
            % In interactive mode, the input value can be used as the default.
            varargout{ii} = ask(params(ii),varargin{ii},varargout);
         else
            varargout{ii} = varargin{ii};
         end
      end
   end
   % Check the parameter & adjust if necessary...
   [msg,varargout{ii}] = paramtype('value_test',params(ii),varargout{ii},varargout);
   if ~isempty(msg)
      assignin('caller','paramerror',['Argument # ',num2str(ii),': ',msg])
      error('Error in parameter.')
   end
end
if selectout
   if M==N+1 & ~question_mode
      try
         varargout{nargout} = check_output_values(varargin{M},{defs.outparams.name});
      catch
         error(lasterr)
      end
   else
      varargout{nargout} = {defs.outparams.name};
   end
end


%
% This function takes a single structure, and asks the user for a value.
% The optional DEFAULT value overrules the one in PARAMS.
%
function retval = ask(params,default,varargout)
if ischar(default) & strcmp(default,'?')
   default = params.default;
end
switch lower(params.type)
   case 'image'
      if isempty(default)
         default = 'ans';
      end
      done = 0;
      while ~done
         retval = input([params.description,' [',printval(default),']? '],'s');
         if isempty(retval)
            retval = default;
         end
         done = 1;
         if ischar(retval)
            try
               retval = evalin('base',retval);
               retval = dip_image(retval);
            catch
               done = 0;
               disp(['Error: ',lasterr]);
            end
         end
      end
   case 'dataset'
      if isempty(default)
         default = 'ans';
      end
      done = 0;
      while ~done
         retval = input([params.description,' [',printval(default),']? '],'s');
         if isempty(retval)
            retval = default;
         end
         done = 1;
         if ischar(retval)
            try
               retval = evalin('base',retval);
               if ~isa(retval,'dataset')
                  retval = dataset(retval);
               end
            catch
               done = 0;
               disp(['Error: ',lasterr]);
            end
         end
      end
   case 'measurement'
      if isempty(default)
         default = 'ans';
      end
      done = 0;
      while ~done
         retval = input([params.description,' [',printval(default),']? '],'s');
         if isempty(retval)
            retval = default;
         end
         done = 1;
         if ischar(retval)
            try
               retval = evalin('base',retval);
               if ~isa(retval,'dip_measurement')
                  retval = dip_measurement(retval);
               end
            catch
               done = 0;
               disp(['Error: ',lasterr]);
            end
         end
      end
   case 'array'
      done = 0;
      while ~done
         if isempty(params.range_check)
            retval = input([params.description,' [',enumerate(default),']? '],'s');
         else
            retval = input([params.description,' [',enumerate(params.range_check),...
                           '] [',enumerate(default),']? '],'s');
         end
         if isempty(retval)
            retval = default;
         end
         done = 1;
         if ischar(retval)
            try
               retval = evalin('base',retval);
            catch
               done = 0;
               disp(['Error: ',lasterr]);
            end
         end
         if done
            msg = paramtype('value_test',params,retval);
            if ~isempty(msg)
               done = 0;
               disp(['Error: ',msg]);
            end
         end
      end
   case 'measureid'
      done = 0;
      options = fieldnames(varargout{params.dim_check});
      default = params.range_check{1};
      params.type = 'option';          % for the call to testparam, this is actually just an option list.
      params.range_check = options;
      params.default = default;
      while ~done
         retval = input([params.description,' {',enumerate(options),...
                         '} [',printval(default),']? '],'s');
         if isempty(retval)
            retval = default;
         end
         done = 1;
         msg = paramtype('value_test',params,retval);
         if ~isempty(msg)
            done = 0;
            disp(['Error: ',msg]);
         end
      end
   case {'option','optionarray'}
      done = 0;
      while ~done
         if isstruct(params.range_check)
            options = {params.range_check.name};
         else
            options = params.range_check;
         end
         if ischar(options{1})
            retval = input([params.description,' {',enumerate(options),...
                            '} [',printval(default),']? '],'s');
            if isempty(retval)
               retval = default;
            end
            done = 1;
         else
            while ~done
               retval = input([params.description,' {',enumerate(options),...
                               '} [',printval(default),']? '],'s');
               if isempty(retval)
                  retval = default;
               end
               done = 1;
               if ischar(retval)
                  try
                     retval = evalin('base',retval);
                  catch
                     done = 0;
                     disp(['Error: ',lasterr]);
                  end
               end
            end
         end
         if done
            msg = paramtype('value_test',params,retval);
            if ~isempty(msg)
               done = 0;
               disp(['Error: ',msg]);
            end
         end
      end
   case {'infile','outfile','indir'}
      done = 0;
      while ~done
         retval = input([params.description,' [',printval(default),']? '],'s');
         if isempty(retval)
            retval = default;
         end
         msg = paramtype('value_test',params,retval);
         done = 1;
         if ~isempty(msg)
            done = 0;
            disp(['Error: ',msg]);
         end
      end
   case 'handle'
      done = 0;
      while ~done
         retval = input([params.description,' [gcf]? '],'s');
         if isempty(retval)
            retval = gcf;
            done = 1;
         elseif ischar(retval)
            try
               retval = evalin('base',retval);
               done = 1;
            catch
               disp(['Error: ',lasterr]);
            end
         end
         if done
            msg = paramtype('value_test',params,retval);
            if ~isempty(msg)
               done = 0;
               disp(['Error: ',msg]);
            end
         end
      end
   case {'string','cellarray'}
      done = 0;
      while ~done
         retval = input([params.description,' [',printval(default),']? '],'s');
         if isempty(retval)
            retval = default;
         end
         msg = paramtype('value_test',params,retval);
         done = 1;
         if ~isempty(msg)
            done = 0;
            disp(['Error: ',msg]);
         end
      end
   case 'boolean'
      try
         if evalbool(default)
            default = 'y';
         else
            default = 'n';
         end
      catch
         default = 'n';
      end
      done = 0;
      while ~done
         retval = input([params.description,' {y/n} [',printval(default),']? '],'s');
         if isempty(retval)
            retval = default;
         end
         msg = paramtype('value_test',params,retval);
         done = 1;
         if ~isempty(msg)
            done = 0;
            disp(['Error: ',msg]);
         end
      end
   otherwise
      error('CANNOT HAPPEN')
end


%
% This function pretty-prints an array. It never produces errors.
%
function string = enumerate(array)
dims = sum(size(array)>1);
%#function isa
if builtin('isa',array,'dip_image')
   string = '(image)';
elseif isa(array,'dataset')
   string = '(dataset)';
elseif dims >= 2
   %??? How to display an array with data?
   string = '(data)';
elseif dims == 1
   if iscell(array)
      string = printval(array{1});
      for ii=2:length(array)
         string = [string,',',printval(array{ii})];
      end
   elseif ischar(array)
      string = array;
   else
      string = num2str(array(1));
      for ii=2:length(array)
         string = [string,',',num2str(array(ii))];
      end
   end
else
   % 0-D or empty array
   if isempty(array)
      string = '';
   else
      string = printval(array);
   end
end


%
% This function pretty-prints a single value. It never produces errors.
%
function string = printval(value)
dims = sum(size(value)>1);
%#function isa
if builtin('isa',value,'dip_image')
   string = '(image)';
elseif isa(value,'dataset')
   string = '(dataset)';
elseif isa(value,'measurement')
   string = '(measurement)';
elseif iscell(value)
   if dims ~= 0
      string = '(values)';
   elseif isempty(value)
      string = '{}';
   else
      string = printval(value{1});
   end
elseif ischar(value)
   if dims > 1
      string = '(character values)';
   else
   string = value;
   end
else
   if dims ~= 0
      string = '(numerical values)';
   else
      string = num2str(value);
   end
end


%
% This function gets 2 cell arrays of strings. All names in the
% first one must be in the second one, and no names must be
% repeated.
%
function params = check_output_values(params,list)
if ~iscell(params)
   error('Expected cell array.')
end
if length(unique(params))~=length(params)
   error('Repeated values in cell array.')
end
if any(~ismember(params,list))
   error('Illegal values in cell array.')
end


%
% Tests the DEFS structure for correctness. Returns a string in MSG
% if there is an error. MSG is empty if its OK. The returned DEFS has
% some elements changed to avoid ambiguities (easier further parsing).
%
function msg = testdefs(defs)
msg = [];
if isfield(defs,'inparams')
   params = defs.inparams;
   if ~isfield(params,'name') | ~isfield(params,'description') | ~isfield(params,'type') | ...
      ~isfield(params,'dim_check') | ~isfield(params,'range_check') | ...
      ~isfield(params,'required') | ~isfield(params,'default')
      msg = 'structure elements missing in INPARAMS';
      return
   end
   N = length(params);
   for ii=1:N
      if ~ischar(params(ii).name) | ~ischar(params(ii).description) | ~ischar(params(ii).type)
         msg = ['NAME, DESCRIPTION and TYPE must be strings (input parameter #',num2str(ii),')'];
         return
      end
      if ~isnumeric(params(ii).required) | prod(size(params(ii).required)) ~= 1 |...
         (params(ii).required ~= 0 & params(ii).required ~= 1)
         msg = ['REQUIRED must be a scalar (1 or 0) (input parameter #',num2str(ii),')'];
         return
      end
      msg = paramtype('definition_test',params(ii),params);
      if ~isempty(msg)
         msg = [msg,' (input parameter #',num2str(ii),')'];
         return;
      end
   end
end
if isfield(defs,'outparams')
   params = defs.outparams;
   if ~isfield(params,'name') | ~isfield(params,'description') | ~isfield(params,'type')
      msg = 'structure elements missing in OUTPARAMS';
      return
   end
   N = length(params);
   for ii=1:N
      if ~ischar(params(ii).name) | ~ischar(params(ii).description) | ~ischar(params(ii).type)
         msg = ['NAME, DESCRIPTION and TYPE must be strings (output parameter #',num2str(ii),')'];
         return
      end
   end
   if isfield(params,'suppress')
      for ii=1:N
         if ~isnumeric(params(ii).suppress) | prod(size(params(ii).suppress)) ~= 1 |...
            (params(ii).suppress ~= 0 & params(ii).suppress ~= 1)
            msg = ['SUPPRESS must be a scalar (1 or 0) (output parameter #',num2str(ii),')'];
            return
         end
      end
   end
end


%
% Parse a boolean value
%
function bool = evalbool(string)
if ischar(string)
   switch lower(string)
      case {'y','yes','t','true'}
         bool = 1;
      case {'n','no','f','false'}
         bool  = 0;
      otherwise
         error('Boolean value expected.')
   end
elseif isnumeric(string) & prod(size(string))==1
   if string
      bool = 1;
   else
      bool = 0;
   end
else
   error('Boolean value expected.')
end
