function [ogroup,gnum,gdata,gcount] = mgrp2idx(group,rows,inclnan,inclempty)
%MATLAB Code Generation Library Function

%MGRP2IDX Convert multiple grouping variables to index vector
%
%   [OGROUP,GDATA,GCOUNT] = MGRP2IDX(GROUP,ROWS,INCLNAN,INCLEMPTY) 
%   creates an index vector from the grouping variables in GROUP.  
%
%   Inputs: 
%       GROUP is a grouping variable (categorical variable, numeric vector,
%       numeric matrix, datetime vector, datetime matrix, duration vector, 
%       duration matrix, string matrix, or cell array of strings) or a cell
%       array of grouping variables. If GROUP is a cell array, all of the 
%       grouping variables that it contains must have the same number of 
%       rows. 
%
%       ROWS is used only to create a grouping var (all ones) in the 
%       special case when GROUP is a 1x0 cell array containing no grouping 
%       variables (it should be set to the length of the data variable).  
%       It is not used to check lengths of grouping variables against the 
%       data variable; the caller should do that.
%
%       INCLNAN is a true/false flag about whether to include NaNs present 
%       in the group.
%
%       INCLEMPTY is a true/false flag about whether to include groups
%       whose count is 0.
%
%   Outputs:
%       OGROUP is a vector of group indices.  
%
%       GNUM is the number of groups 
%
%       GDATA is a cell array containing one column vector per grouping 
%       variable. The column vectors contain one row for each distinct 
%       combination of grouping variable values.
%
%       GCOUNT is a vector containing the group counts for the data
%
%   Limitations:
%       The in-memory version can sometimes return gcount with the same
%       type as rows, but most of the time returns it as a double. This 
%       version will always return grount with the same type as rows.

%   Copyright 2021 The MathWorks, Inc.
%#codegen

coder.internal.prefer_const(rows,inclnan,inclempty);
ONE = coder.internal.indexInt(1);

% Compute number of grouping variables
ngrps = size(group,2);

% if no grouping vars, create one group containing all observations
if ngrps == 0
    ogroup = ones(rows,1);
    gdata = {};
    if ~inclempty && isequal(rows,0)
        gcount = zeros(0,1,'like',rows);
        gnum = 0;
    else
        gcount = rows;
        gnum = 1;
    end
    return;
end

% Special case a single grouping variable
if ngrps == 1
    % Depending on nargout set the output correctly
    if nargout > 2
        [ogroup,gnum,gdataTmp] = grp2idx(group{1,1},inclnan,inclempty);
        gdata = {gdataTmp};
    else
        [ogroup,gnum] = grp2idx(group{1,1},inclnan,inclempty);
    end
    
    % if required compute group count
    if nargout > 3
        % Avoiding grpmat(isnan(grpmat)) = [] to reduce overhead
        grpmat = removeNaNs(ogroup);
        if isempty(grpmat)
            if ~inclempty
                gcount = zeros(0,1,'like',rows);
            else
                gcount = zeros(gnum,1,'like',rows);
            end
        else
            gcount = accumarray(grpmat,1,[gnum,1]);
        end
    end
    
    return;
end

% preallocate to avoid warnings
gd = cell(1,ngrps);
gdata = cell(1,ngrps);
countgrp = zeros(1,ngrps);

% Compute size of group variable to compare vs other group inputs
es = size(group{1,1});

% If all grouping variables are numeric, then we can call unique once
firstCellType = class(group{1});
allNumeric = true;
for ii = ONE:numel(group)
    g = group{ii};
    allNumeric = allNumeric && isnumeric(g) && ~issparse(g) && ~isobject(g) ...
        && isequal(class(g),firstCellType);
end
% Get integer codes and data/names for each grouping variable
[g,~] = grp2idx(group{1,1},inclnan,inclempty);
rows = size(g,1);
grpmat = coder.nullcopy(zeros(rows,ngrps,'like',rows));

for j=1:ngrps
    % Only call with the output arguments required
    if nargout > 2
        [g,countgrp(j),gd{1,j}] = grp2idx(group{1,j},inclnan,inclempty);
    else
        [g,countgrp(j)] = grp2idx(group{1,j},inclnan,inclempty);
    end

    % Checking input size is correct (needed in findgroups)
    currentSize = size(group{1,j});
    coder.internal.assert(numel(currentSize) == numel(es), 'MATLAB:findgroups:InputSizeMismatch');
    for ii = ONE:numel(currentSize)
        coder.internal.assert(es(ii) == currentSize(ii), 'MATLAB:findgroups:InputSizeMismatch');
    end

    % Assign output group
    grpmat(:,j) = g;
end

% First remove any NaN categories from grpmat (included missing will have
% number not NaNs)
[grpmatWithoutNaNs,wasnan] = removeNaNRows(grpmat);

% If including empties need to create all groups possible first
if inclempty
    
    % Compute the total number of groups
    prodgrp = countgrp(1);
    % Avoiding prod to reduce overhead
    for ii = coder.internal.indexInt(2):ngrps
        prodgrp = prodgrp*countgrp(ii);
    end
    gnum = prodgrp;
    
    % Create linear groupnumber from grpmatWithoutNaNs
    cumprodCountgrp = 1;
    grplin = coder.nullcopy(zeros(size(grpmatWithoutNaNs,1),1));
    for ii = ONE:size(grpmatWithoutNaNs,1)
        cumprodCountgrp = cumprodCountgrp*countgrp(ii);
        grplin(ii) = grpmatWithoutNaNs(ii,ONE) - 1;
        for jj = coder.internal.indexInt(2):size(grpmatWithoutNaNs,2)
            grplin(ii) = grplin(ii) + grpmatWithoutNaNs(ii,jj) - 1;
        end
        grplin(ii) = grplin(ii) * prodgrp/cumprodCountgrp + 1;
    end

    % For each grouping variable create group data if requested
    if nargout > 2
        coder.unroll();
        for j=1:ngrps
            % catch the case where one grouping variable is all missing and include missing is false
            if prodgrp == 0 
                gnamerow = zeros(0,1);
            else
                % Compute number of repetitions for the data replications
                %TODO Get rid of prod here to reduce overhead
                step = prodgrp/(prod(countgrp(1:j-1))*countgrp(j));
                left = prodgrp/(step*countgrp(j));
            
                % Compute the data numbers for group j for all groups
                gnametag = 1:countgrp(j);
                gnamerow = repmat(repelem(gnametag',step,1),left,1);
            end
            % Index with the group data numbers into gd to compute gdata
            gdata{1,j} = gd{1,j}(gnamerow);
        end
    end
    
    % Adding back missing groups that were ignored
    ogroup = NaN(size(wasnan));
    ogroup(~wasnan) = grplin;
    
    % If we requested groupcounts compute them
    if nargout > 3
        gcount = accumarray(grplin,1,[prodgrp,1]);
    end
% If excluding empties things are simpler
else
    % Group according to each distinct combination of grouping variables, use
    % unique rows to determine combinations
    % Inputs for uniquehelper are (data,['rows' 'first' 'last' 'sorted' 'stable'],equalNaNs,includeNaNs)
    [urows,~,uj] = uniquehelper(grpmat,[true true false true false],true,false);
    
    % Adding back missing groups that were ignored
    ogroup = NaN(size(wasnan));
    ogroup(~wasnan) = uj;
    
    if isempty(uj)
        gnum = 0;
    else
        gnum = max(uj);
    end
    
    % If want groupdata need to use uniquerows and original gd from grp2idx
    % to create the output combinations in gdata
    if nargout > 2
        if coder.internal.isConst(ngrps)
            coder.unroll();
        end
        for j=1:ngrps
            gdata{1,j} = gd{1,j}(urows(:,j));
        end
    end
    
    % If we want group counts also compute them (Ex: groupsummary)
    if nargout > 3
        % Compute group count
        gcount = accumarray(uj,1);
    end
end
end

function out = removeNaNs(in)
    out = removeNaNRows(in(:));
end

function [out, hasNaNs] = removeNaNRows(in)
ONE = coder.internal.indexInt(1);
numNonNaNRows = coder.internal.indexInt(size(in,1));
hasNaNs = zeros(size(in,1), 1,'like',true);
for jj = ONE:size(in,2)
    for ii = ONE:size(in,1)
        if ~hasNaNs(ii) && isnan(in(ii,jj))
            hasNaNs(ii) = true;
            numNonNaNRows = numNonNaNRows - ONE;
        else
            hasNaNs(ii) = false;
        end
    end
end

out = coder.nullcopy(zeros(numNonNaNRows,size(in,2),'like',in));
count = ONE;
for ii = ONE:numNonNaNRows
    while hasNaNs(count)
        count = count + ONE;
    end
    out(ii,:) = in(count,:);
    count = count + ONE;
end
end
