function A = onehotdecode(B,classes,featdim,typename)
% ONEHOTDECODE Decode probability vectors into class labels.
%
%   A = ONEHOTDECODE(B,CLASSES,FEATDIM) reduces an array of probability
%   vectors into the most probable class label for each observation. Each
%   vector in B along the dimension specified by FEATDIM is replaced by the
%   element of CLASSES in the same position as the highest value along the
%   vector. Input B is an n-dimensional numeric array containing values
%   between 0 and 1. CLASSES must be a cell array of char vectors, a string
%   or numeric vector, or a 2-dimensional char array that specifies the
%   classes used for decoding. FEATDIM is a positive integer such that
%   size(B,featdim) is equal to length(CLASSES). If a probability vector in
%   B contains NaN values, then that observation is decoded to the class
%   that has the largest probility that is not NaN. If an observation
%   contains only NaN values, then that observation is decoded to the first
%   class label.
% 
%   A = ONEHOTDECODE(...,TYPENAME) specifies the data type of the decoded
%   labels. Valid options are 'categorical', 'string', and numeric types
%   such as 'double'. If you specify a numeric type, CLASSES must be a
%   numeric array, or a string array or a cellstr array containing strings
%   or char vectors of numbers. The default value is 'categorical'.
%
%   Example 1: One-hot decode a set of probabilities 
% 
%       nObservations = 10;
%       classes = ["dog","cat","tiger"];
%       B = rand(nObservations,length(classes));
%       A = onehotdecode(B,classes,2,'string');
% 
%   Example 2: One-hot decode a one-hot encoded matrix
% 
%       % Create a categorical matrix
%       colors = categorical({'r' 'b' 'g'; 'g' 'r' 'b'; 'b' 'r' 'g'}, ...
%           {'r' 'g' 'b'},{'red' 'green' 'blue'});
%       encodedColors = onehotencode(colors,3);
%       decodedColors = onehotdecode(encodedColors,categories(colors),3);
% 
%   See also ONEHOTENCODE, CATEGORICAL, CATEGORIES.

%   Copyright 2020 The MathWorks, Inc.

narginchk(3,4);

if nargin==4
    typename=iAssertAndReturnValidTypename(typename);
else
    typename='categorical';
end

try
    classes=iValidateInputArguments(B,classes,featdim,typename);
catch e
    throw(e);
end

clsIdx=mlearnlib.internal.data.OneHotConverter.decodeVectorsToIntegers(B,featdim);
A=mlearnlib.internal.data.LabelIntConverter.decodeIntegersToLabels(clsIdx,classes,typename);
end

function classes=iValidateInputArguments(B,classes,featdim,typename)

validateattributes(B,{'numeric'},{},'onehotdecode','B');
validateattributes(B(~isnan(B)),{'numeric'},{'nonnegative', '<=', 1},'onehotdecode','B');

validateattributes(featdim,{'numeric'},{'integer', 'scalar', 'positive'},'onehotdecode','featdim');
classes=mlearnlib.internal.data.LabelIntConverter.assertAndReturnValidClasses(classes);

if size(B,featdim)~=length(classes)
    % The size of the specified feature dimension must match the number of
    % classes.
    error(message('mlearnlib:onehot:ohDecodeFeatureDimension',featdim));
end

if ~strcmp(typename,'categorical')
    % Verify compatibility between classes and typename, if typename is not
    % the default value.
    classes=iValidateClassesAndTypenameAreCompatible(classes,typename);
end
end

function typename=iAssertAndReturnValidTypename(typename)
supportedTypes=["categorical", "string", "double", "single", ...
    "int8" "int16" "int32" "int64" "uint8" "uint16" "uint32" "uint64"];

typename=validatestring(typename, supportedTypes, 'onehotdecode', 'typename');
end

function classes=iValidateClassesAndTypenameAreCompatible(classes,typename)
% Validate that classes is of type compatible with typename, i.e. it is of
% type string if typename is string and it is of type double if typename is
% a numeric type. If typename is an integer type, classes must be a vector
% of integer values.

typenameString=typename == "string";
typenameCategorical=typename == "categorical";
typenameInteger=contains(typename, 'int');
isClassesCellstr=iscellstr(classes);
isClassesNumeric = isnumeric(classes);

if typenameString && isClassesNumeric
    error(message('mlearnlib:onehot:ohDecodeStringOutputNonStringClasses'));
elseif ~typenameString && ~typenameCategorical && (isstring(classes)||isClassesCellstr)
    error(message('mlearnlib:onehot:ohDecodeNumericOutputNonNumberStringClasses'));
elseif typenameInteger
    classesHasAllIntegerValues = all(floor(abs(classes)) == abs(classes));
    if ~classesHasAllIntegerValues
        error(message('mlearnlib:onehot:ohDecodeIntOutputNotIntegerClasses',typename));
    end
end
end