function [lbls,files] = filenames2labels(src,NVArgs)
%filenames2labels Get list of labels from filenames
%   LBLS = filenames2labels(LOC,...) creates a list of labels in
%   categorical vector LBLS based on the filenames specified by the file
%   location LOC.
%
%   The location input, LOC, can be:
%      - A character vector or string scalar specifying a folder name
%      - A cell array or a string array of folder names
%      - A remote location specified using an internationalized
%        resource identifier (IRI). For more information on accessing remote
%        data, see "Read Remote Data" in the documentation.
%
%   LOC can contain a relative path (HDFS requires a full path) or a
%   wildcard (*) character.
%
%   When a file location is specified, filenames2labels uses a
%   matlab.io.datastore.FileSet object to obtain a file list and parses the
%   filenames to obtain labels. Subfolders are included by default.
%
%   LBLS = filenames2labels(LOC,...,'FileExtensions',EXT) specifies the
%   file extensions, EXT, of the files to be included in the file list from
%   which labels will be parsed. EXT can be a string scalar or vector, a
%   character vector, or a cell array of character vectors. If EXT is not
%   specified, all filenames of files found in the specified location are
%   included in the list from which labels are parsed.
%
%   LBLS = filenames2labels(LOC,...,'IncludeSubfolders',TF) specifies
%   whether files in subfolders are included in the file list. Specify TF
%   as a logical scalar. The default is TRUE.
%
%   LBLS = filenames2labels(DS,...) creates a list of labels from the files
%   in DS. DS can be a datastore, matlab.io.datastore.FileSet object, or
%   matlab.io.datastore.BlockedFileSet object. If DS is a datastore, it
%   must contain a 'Files' property from which label names are parsed. If
%   DS is a matlab.io.datastore.FileSet object, label names are obtained
%   from the filenames listed in its 'FileInfo' property. If DS is a
%   matlab.io.datastore.BlockedFileSet object, label names are obtained
%   from the filenames listed in its 'BlockInfo' property.
%
%   By default, filenames2labels outputs the filenames as labels. When the
%   label names are contained in filename substrings, you can specify how
%   to extract the names using one of these mutually exclusive name-value
%   arguments:
%
%   LBLS = filenames2labels(...,'ExtractBefore',S) when S is a string
%   scalar or a pattern object extracts labels from each filename as the
%   substring that begins with the first character of the filename and ends
%   before the first occurrence of S. When S is a positive integer, the
%   function extracts labels from each filename as the substring that
%   begins with the first character of the filename and ends before the
%   position specified by index S. If the string or pattern object S is not
%   found in a filename, or if index S is 1 or larger than
%   length(char(filename)) + 1, then the label for that filename is set to
%   <undefined>.
%
%   LBLS = filenames2labels(...,'ExtractAfter',S) when S is a string scalar
%   or a pattern object extracts labels from each filename as the substring
%   that begins after the first occurrence of S and ends with the last
%   character of the filename. When S is a nonnegative integer, the
%   function extracts labels from each filename as the substring that
%   begins after the position specified by index S and ends with the last
%   character of the filename. If the string or pattern object S is not
%   found in a filename, or if the index S is larger than or equal to the
%   length of the filename, the label for that filename is set to
%   <undefined>.
%
%   LBLS = filenames2labels(...,'ExtractBetween',[P S]) when [P S] is a
%   two-element string vector or cell array of characters, or a two-element
%   vector of pattern objects extracts labels from each filename as the
%   substring that begins after P and ends before S. When P and S are
%   different, the function searches for the first occurrence of P and the
%   first occurrence of S in each filename and extracts a label from the
%   characters in between. When P and S are equal, the function searches
%   for the first and second occurrences of S in each filename and extracts
%   a label from the characters in between. When [P S] is a two-element
%   vector of positive integers, the function extracts labels from each
%   filename as the characters indexed by P:S. S must be larger than or
%   equal to P. If there are no characters between strings or pattern
%   objects P and S in a filename, or if indices P or S are larger than the
%   length of the filename, the label for that filename is set to
%   <undefined>.
%
%   LBLS = filenames2labels(...,'Extract',P) extracts labels from each
%   filename as the substrings that match pattern object P. If no match is
%   found in a filename, the label for that filename is set to <undefined>.
%   If more than one pattern is found per filename, LBLS is a matrix. All
%   filenames must have the same number of pattern matches. If not the
%   function throws an error.
%
%   [LBLS,FILES] = filenames2labels(...) returns a list of files in string
%   vector FILES. The i-th element in LBLS corresponds to a label for the
%   i-th file in FILES.
%
%   See also folders2labels, countlabels, splitlabels, pattern.

%   Copyright 2022 The MathWorks, Inc.

arguments
    src {mustBeNonempty}
    NVArgs.FileExtensions {mustBeText}
    NVArgs.IncludeSubfolders {mustBeNumericOrLogical,mustBeNonempty,mustBeScalarOrEmpty}
    NVArgs.ExtractBefore
    NVArgs.ExtractAfter
    NVArgs.ExtractBetween
    NVArgs.Extract 
end


files = labelutils.internal.validation.validateSrcForLabelExtraction(src,NVArgs);
[option, limits] = validateInputs(NVArgs);
lbls = getLabels(files, option, limits);
end

%--------------------------------------------------------------------------
function lbls = getLabels(files, option, limits)
% Extract folder name from file name

[~,filenames] = fileparts(files);
N = numel(filenames);

% For ExtractBefore, ExtractAfter, and ExtractBetween cases, and when
% limits are indices, remove filenames that are of a size that would cause
% functions to error out or to extract empty substrings. Set the labels for
% those filenames to missing at the end. 

% For the Extract case, use try-catch to rethrow errors when filenames do
% not have a consistent number of pattern matches in their names.

switch option
    case "ExtractBefore"
        missingIdxs = false(N,1);
        if isnumeric(limits)
            fnLengths = arrayfun(@(x) length(char(x)),filenames);
            missingIdxs = limits > fnLengths + 1;
            filenames = filenames(~missingIdxs);
        end
        lbls = extractBefore(filenames,limits);
    case  "ExtractAfter"
        missingIdxs = false(N,1);
        if isnumeric(limits)
            fnLengths = arrayfun(@(x) length(char(x)),filenames);
            missingIdxs = limits >= fnLengths;
            filenames = filenames(~missingIdxs);
        end
        lbls = extractAfter(filenames,limits);
    case "ExtractBetween"
        missingIdxs = false(N,1);
        if isnumeric(limits)
            fnLengths = arrayfun(@(x) length(char(x)),filenames);
            missingIdxs = limits(1) > fnLengths | limits(2) > fnLengths;
            filenames = filenames(~missingIdxs);
        end
        lbls = extractBetween(filenames,limits(1),limits(2));
    case "Extract"
        try
            lbls = extract(filenames,limits);
            lbls = categorical(lbls);
        catch E
            throwAsCaller(E);
        end
        return;
    case "FullName"
        lbls = categorical(filenames);
        return;
end

allLbls = repmat(string(missing),N,1);
allLbls(~missingIdxs) = lbls;
lbls = categorical(allLbls);
end

%--------------------------------------------------------------------------
function [option, limits] = validateInputs(NVArgs)

% Mutually exclusive inputs and at least one input
s = sum(isfield(NVArgs,["ExtractBefore","ExtractAfter","ExtractBetween","Extract"]));
if s > 1
    error(message("shared_labelutils:filenames2labels:MutuallyExclusiveArgs"))
elseif s == 0
    option = "FullName";
    limits = [];
    return;
end

if isfield(NVArgs,"ExtractBefore")
    [validFlag, val] = validateStringPatternOrIntegerScalar(NVArgs.ExtractBefore);
    if ~validFlag || (isnumeric(val) && val <= 0)
        error(message("shared_labelutils:filenames2labels:InvalidExtractBefore"));
    end
    option = "ExtractBefore";
    limits = val;
elseif isfield(NVArgs,"ExtractAfter")
    [validFlag, val] = validateStringPatternOrIntegerScalar(NVArgs.ExtractAfter);
    if ~validFlag || (isnumeric(val) && val < 0)
        error(message("shared_labelutils:filenames2labels:InvalidExtractAfter"));
    end
    option = "ExtractAfter";
    limits = val;
elseif isfield(NVArgs,"ExtractBetween")
    % ExtractBetween must be a 2 element string vector or a 2 element integer
    % vector
    [validateFlag, val] = validateStringPatternOrIntegerTwoElementVector( NVArgs.ExtractBetween);
    if ~validateFlag
        error(message("shared_labelutils:filenames2labels:InvalidExtractBetween"))
    end
    option = "ExtractBetween";
    limits = val;
else % Extract
    if ~isa(NVArgs.Extract,'pattern') || ~isscalar(NVArgs.Extract)
        error(message("shared_labelutils:filenames2labels:InvalidExtract"))
    end
    option = "Extract";
    limits = NVArgs.Extract;
end
end

%--------------------------------------------------------------------------
function [validFlag, val] = validateStringPatternOrIntegerScalar(val)
if isa(val,'pattern') 
    validFlag = isscalar(val);
elseif ischar(val) || isstring(val)
    % Must be char or string not empty
    val = string(val);
    validFlag = isscalar(val) && (val ~= "");
elseif isnumeric(val)
    % if numeric, must be integer
    validFlag = isscalar(val) && (round(val) == val);
else
    validFlag = false;
end
end

%--------------------------------------------------------------------------
function [validateFlag, val] = validateStringPatternOrIntegerTwoElementVector(val)
if isa(val, 'pattern')
    validateFlag = (isvector(val) && numel(val) == 2);
elseif iscellstr(val) || isstring(val)
    % Must be two-element string, cell string, and must not have "" values
    val = string(val);
    validateFlag = (isvector(val) && numel(val) == 2) &&  ~any(val == "");
elseif isnumeric(val)
    % If numeric it must be a two-element vector of integers with
    % second element >= first element
    validateFlag = (isvector(val) && numel(val) == 2) && ...
        (val(2) >= val(1)) && all(round(val) == val) && all(val > 0);
else
    validateFlag = false;
end
end




