function [parms, params_unspecified ] = dsCheckOptions(options, options_schema, strict)
%CHECKOPTIONS - organize key/value pairs in structure with default or user-supplied values according to a schema
%
% Usage:
%   options = dsCheckOptions(keyvals, options_schema, [strict])
%
% Inputs:
%   - keyvals: list of key/value pairs ('option1',value1,'option2',value2,...)
%   - options_schema: cell array containing 3 values per known 'option':
%     - option name
%     - default value
%     - allowed values:
%         - vector of true/false values
%         - vector of min/max values
%         - vector of allowed values (more than 2 elements)
%         - cell array of allowed values
%         - empty to specify no limitations.
%   - strict (default: true): whether to fail if options not specified in the
%       options_schema are found.
%
% Note: this function was adapted from one developed "in-house" years ago...
%
% Outputs:
%   - options: structure with options (using default values if not supplied)
%
% See also: dsOptions2Keyval, dsCheckSpecification, dsCheckModel, dsCheckData
% 
% Author: Jason Sherfey, PhD <jssherfey@gmail.com>
% Copyright (C) 2016 Jason Sherfey, Boston University, USA


% Convert cell argument to struct if contains struct (Leave as is if already a struct)
if length(options) == 1 && ~isstruct(options) && isstruct(options{1})
  options = options{1};
end

if ~isstruct(options) && (0 ~= mod(length(options),2))     %   Validate that the # of args is even
  error('List of arguments must be even (must have name/value pair)');
end
if (0 ~= mod(length(options_schema),3))    %   Validate the options_schema info is right
  error('Programming error: list of default arguments must be even (must have name/value pair)');
end
if (~exist('strict', 'var'))
  strict = true;
end

parms = [];

%   Rip all cell arguments into non-cell arguments?
%     NOTE: currently just converts empty cells into empty arrays
if ~isstruct(options)
  for ind = 1:length(options)
    if iscell(options{ind})
      if isempty(options{ind})
        options{ind}=[];
      elseif ~iscell(options{ind}{1})
        %options{index} = { options{index} };
      end
    end
  end
else
  flds = fieldnames(options);
  for ind = 1:length(flds)
    fld = flds{ind};
    if iscell(options.(fld))
      if isempty(options.(fld))
        options.(fld)=[];
      elseif ~iscell(options.(fld){1})
        %options{index} = { options{index} };
      end
    end
  end
end

if ~isstruct(options) % if arguments given as list
  input_fields  = options(1:2:end);
else
  input_fields  = fieldnames(options);
end
valid_fields    = options_schema(1:3:end);
unknown_fields  = setdiff(input_fields, valid_fields);

%   Validate that there are no extraneous params sent in
if (strict && ~isempty(unknown_fields))
  error('The following unrecogized options were passed in: %s', sprintf('%s ',unknown_fields{:}));
end

if ~isstruct(options) %convert to struct if arguments given as list
  if (~isempty( options ))
    for f=1:length(options)/2
      parms.(options{2*f-1})=options{2*f};
    end
    %parms=struct(options{:});
  end
else
  parms = options;
end

%   This allows 'pass-through' of parameters;
%   remove any fields that are unknown
%   unless no schema is defined.

params_unspecified = struct;
if (~strict && ~isempty(parms) && ~isempty(options_schema))
  for i = 1:length(unknown_fields)
      params_unspecified.(unknown_fields{i}) = parms.(unknown_fields{i});
  end
  parms = rmfield(parms,unknown_fields);
end

%   Check arg values and set defaults
for f=1:3:length(options_schema)
  %   The value has been set explicitly by the caller;
  %   Validate the input parameters by the 'range' field
  if  (isfield(parms,options_schema{f}))
    param_name		= options_schema{f};
    param_value		= getfield(parms,param_name);
    param_range   = options_schema{f+2};

    % no value was specified,
    if isempty(param_value)
      parms = setfield(parms, options_schema{f}, options_schema{f+1});
    % no range was specified,
    elseif isempty(param_range)
      continue;
     %	param range is a cell array of strings; make sure the current value is within that range.
    elseif iscell(param_range)
      num_flag = 0;
      char_flag = 0;
      
      for i=1:length(param_range)
        if isnumeric(param_range{i}), num_flag=1; end
        if ischar(param_range{i}), char_flag=1; end
      end
      
      if num_flag && char_flag
        error('type of parameter range (cell array of numbers and strings) specified for parameter ''%s'' is currently unsupported', options_schema{f});
      elseif char_flag
        if iscell(param_value)
          for i=1:length(param_value)
            if ~ischar(param_value{i})
              error('parameter ''%s'' must be string or cell array of strings', options_schema{f});
            end
          end
        elseif ~ischar(param_value)
          error('parameter ''%s'' must be string or cell array of strings', options_schema{f});
        else
          param_value = {param_value};
        end
        
        if length(find(ismember(param_value,param_range))) ~= length(param_value)
          error('parameter ''%s'' value must be one of the following: { %s}', ...
            param_name, sprintf('''%s'' ',param_range{:}));
        end
      elseif num_flag
        param_range = cell2mat(param_range);
        
        if ~isnumeric(param_value)
          error('parameter ''%s'' must be numeric', options_schema{f});
        end
        
        if length(find(ismember(param_value,param_range))) ~= length(param_value)
          error('parameter ''%s'' value must be one of the following: { %s}', ...
            param_name,sprintf('%d ',param_range));
        end
      else
        error('type of parameter range specified for parameter ''%s'' is currently unsupported', options_schema{f});
      end
    %  param range is logical and has two elements (i.e. true/false)
    elseif islogical(param_range) && length(param_range)==2
      if ~ismember(param_value,param_range)
        error('parameter %s value must be true (1) or false (0)', options_schema{f});
      end
    %  param range is numeric and has two elements (i.e. min and max)
    elseif isnumeric(param_range) && length(param_range)==2
      %  param range is numeric or logical, and within a specified range,
      if ~isempty(find(param_value < param_range(1))) || ...
         ~isempty(find(param_value > param_range(2)))
        if int64(param_range(1))==param_range(1) && int64(param_range(2))==param_range(2)
          error('parameter %s value must be between %d and %d',...
            options_schema{f},param_range(1),param_range(2));
        else
          error('parameter %s value must be between %0.4f and %0.4f',...
            options_schema{f},param_range(1),param_range(2));
        end
      end
    %  param range is numeric and has more than two elements (allowed values)
    elseif isnumeric(param_range)
      if ~isnumeric(param_value)
        error('parameter ''%s'' must be numeric', options_schema{f});
      end
      
      if length(find(ismember(param_value,param_range))) ~= length(param_value)
        error('parameter ''%s'' value must be one of the following: [ %s]', ...
          param_name,sprintf('%d ',param_range));
      end
    %  param range is of a type we currently don't support.
    else
      error('type of parameter range specified for parameter ''%s'' is currently unsupported', options_schema{f});
    end
  % field not found, so set the default value.
  else
    %parms = setfield(parms, options_schema{f}, options_schema{f+1});
    parms.(options_schema{f})=options_schema{f+1};
  end
end