function [Y, I] = minmaxOp(fcnMinMax,varargin)
%minmaxOp    template for max and min

%   Copyright 2006-2023 The MathWorks, Inc.

narginchk(2, 8);

distributedutil.CodistParser.verifyNonCodistributedInputs(varargin);
fcnStr = lower(func2str(fcnMinMax));

% Arguments beyond the second should never be distributed.
if numel(varargin)>2
    varargin(3:end) = distributedutil.CodistParser.gatherElements(varargin(3:end));
end

% Before going any further, use a local call to sanity check the syntax
iLocalSanityCheck(fcnMinMax,nargout,varargin)

% Split off any trailing string arguments (e.g. 'omitnan'). Note that the
% first two arguments are *always* treated as data.
[dataArgs, trailingArgs] = iGetTrailingArgs(varargin);
numDataArgs = numel(dataArgs);

% Check whether linear indices were requested. We have to do a bit more
% work for those.
linearI = any(cellfun(@(x) iIsFlag(x, "linear"), trailingArgs));
% If two outputs and dim is "all" but "linear" not specified, add it.
if nargout>1 && ~linearI && ...
        numel(dataArgs)>=3 && iIsFlag(dataArgs{3}, "all")
    linearI = true;
    trailingArgs{1,end+1} = "linear";
end

% Bind the trailing arguments into the function call, making sure they are
% not distributed.
if ~isempty(trailingArgs)
    fcnMinMax = @(varargin) fcnMinMax(varargin{:}, trailingArgs{:});
end

try
    if numDataArgs == 1 || numDataArgs == 3
        % Handle fMinMax(X) and fMinMax(X, [], dim).
        X = iChar2Double(dataArgs{1});
        if istabular(X) && nargout > 1
            % Limitation: index output for tabular inputs not supported for
            % reductions. In-memory min/max does not support it for
            % comparison.
            error(message("MATLAB:bigdata:array:TabularMinMaxIndexUnsupported", "distributed"));
        end
        if numDataArgs == 1
            dim = distributedutil.Sizes.firstNonSingletonDimension(size(X));
        else
            if ~isempty(dataArgs{2})
                % This is the same error message as with builtin min and max.
                % For example, min(3, 4, 2).
                error(message(['MATLAB:', fcnStr, ':caseNotSupported']));
            end
            dim =  distributedutil.CodistParser.gatherIfCodistributed(dataArgs{3});
            if ~isa(X, 'codistributed')
                if nargout > 1
                    [Y, I] = fcnMinMax(X, [], dim);
                else
                    Y = fcnMinMax(X, [], dim);
                end
                return;
            end
        end
        if nargout > 1
            [Y, I] = iMinMaxAlongDim(fcnMinMax, X, dim, linearI);
        else
            Y = iMinMaxAlongDim(fcnMinMax, X, dim, linearI);
        end
    elseif numDataArgs == 2
        % Handle fMinMax(X, Y), i.e. elementwise comparison between X and Y.
        if nargout > 1
            error(message(['MATLAB:', fcnStr, ':TwoInTwoOutCaseNotSupported']));
        end
        strongTypes = ["categorical", "datetime", "duration"];
        if ismember(underlyingType(dataArgs{1}), strongTypes) ...
                || ismember(underlyingType(dataArgs{2}), strongTypes)
            % With strong types, convert any char input to string.
            X = iChar2String(dataArgs{1});
            Z = iChar2String(dataArgs{2});
        else
            % With numeric/logical inputs, convert any char input to
            % double.
            X = iChar2Double(dataArgs{1});
            Z = iChar2Double(dataArgs{2});
        end
        Y = codistributed.pElementwiseOp(fcnMinMax, X, Z);
    else
        % Incorrect syntax. This could be a few different things, so let
        % the function decide using the local parts.
        args = iGetLocalParts(dataArgs);
        [~] = fcnMinMax(args{:});
        % Just in case it didn't throw, use a generic maxrhs error
        error(message('MATLAB:maxrhs'));
    end
catch E
    % Error stack should only show min or max, not minmaxOp.
    throwAsCaller(E);
end

end % End of minmaxOp.

function [Y, I] = iMinMaxAlongDim(fcnMinMax, X, dim, linearI)

I = [];
wantI = nargout>1;
if ~matlab.bigdata.internal.util.isValidReductionDimension(dim)
    error(message('MATLAB:getdimarg:invalidDim'));
end
if iIsFlag(dim, "all")
    % Replace "all" with numeric vector, and always return linear indices
    dim = 1:ndims(X);
end

% Tabular maths in dimension 2 combines the table variables. That's
% extremely difficult to do if the distribution dimension is also 2 so
% redistribute in that case.
codistr = getCodistributor(X);
if istabular(X) && any(dim==2) ...
        && isa(codistr, 'codistributor1d') && codistr.Dimension==2
    X = redistribute(X, codistributor1d(1));
end

xSz = size(X);
if all(ismember(xSz(dim(dim<=numel(xSz))), [0, 1]))
    % min/max on all singleton or empty dimensions drops complexity but
    % does not perform any other operation. This is completely different
    % from any/all/sum/prod. However, we must still check that the args are
    % valid. We do this by calling the function on a scalar of the same
    % type as the local part (fcnMinMax has the trailing args bound-in and
    % this will throw if they are invalid).
    if isnumeric(X) || ischar(X) || islogical(X)
        if wantI
            [~,~] = fcnMinMax(cast(1, 'like', getLocalPart(X)), [], dim);
        else
            [~] = fcnMinMax(cast(1, 'like', getLocalPart(X)), [], dim);
        end
    end
    % All Ok. Just copy the input, unless X is not real but all imaginary
    % parts are zero, where we have to drop the complexity.
    if isnumeric(X) && ~isreal(X) && isempty(find(imag(X), 1))
        Y = real(X);
    else
        Y = X;
    end
    if wantI
        if linearI && ~isempty(X)
            % Indices just go from 1:N but need to be the same shape and
            % distribution as the input.
            I = redistribute(reshape(codistributed.colon(1, numel(X)), size(X)), getCodistributor(X));
        else
            I = codistributed.ones(size(X), getCodistributor(X), 'noCommunication');
        end
    end
    return;
end

% Remove all trivial dimensions.
dim(dim>numel(xSz)) = [];
dim(xSz(dim)<=1) = [];

% Defer to codistributor-specific implementation for non-trivial dimensions.
codistr = getCodistributor(X);
LP = getLocalPart(X);
[LPY, LPI, codistr] = codistr.hMinMaxImpl(fcnMinMax, LP, dim, wantI, linearI);

% Reconstruct result arrays
Y = codistributed.pDoBuildFromLocalPart(LPY, codistr);
if wantI
    I = codistributed.pDoBuildFromLocalPart(LPI, codistr);
end

end % End of iMinMaxAlongDim.

function args = iGetLocalParts(args)
% Helper to get the local parts of any codistributed inputs. Other inputs
% are left alone.
for ii=1:numel(args)
    if iscodistributed(args{ii})
        args{ii} = getLocalPart(args{ii});
    end
end
end

function arg = iChar2Double(arg)
% Convert char inputs to double. Other types are left as-is.
if ischar(arg)
    arg = double(arg);
end
end

function [dataArgs, trailingArgs] = iGetTrailingArgs(argList)
% Helper to split data and dimension args from trailing arguments. Note that
% for MIN and MAX the first two arguments are *always* treated as data.
trailingArgs = {};
if numel(argList)<=2
    dataArgs = argList;
else
    % First, strip supported option/flags from the end.
    while numel(argList)>2 ...
            && isScalarString(argList{end}) ...
            && ~strncmpi(argList{end}, "all", strlength(argList{end}))
        trailingArgs(1,end+1) = argList(end); %#ok<AGROW>
        argList(end) = [];
    end
    % Revert back trailing args, we need the correct order for name-value
    % pair.
    trailingArgs = flip(trailingArgs);
    dataArgs = argList;

    % For min(x,[],'omitnan') and similar, ignore the []
    if ~isempty(trailingArgs) && numel(dataArgs)==2 && ismatrix(dataArgs{2}) && all(size(dataArgs{2}) == 0)
        dataArgs(end) = [];
    end
end
end

function iLocalSanityCheck(fcnMinMax,nout,args)
% Use a local call with empty/scalar data to check the syntax
% Array MIN/MAX
for ii=1:numel(args)
    if iscodistributed(args{ii})
        lp = getLocalPart(args{ii});
        if (ischar(args{ii}) && isrow(args{ii}))
            args{ii} = lp;
        elseif isequal(args{ii}, [])
            % Probably placeholder used in MAX(X,[],DIM) so leave as []
            args{ii} = [];
        elseif istabular(args{ii})
            args{ii} = getTableTemplate(args{ii});
        else
            % Use the allocator to get a scalar of the right type
            args{ii} = distributedutil.Allocator.create([1 1], lp);
        end
    end
end
out = cell(1,nout);
try
    [out{:}] = fcnMinMax(args{:}); %#ok<NASGU>
catch err
    throwAsCaller(err);
end
end

function tf = iIsFlag(arg, flagName)
% Returns true if partial case-insensitive match for flag, false otherwise.
tf = matlab.internal.datatypes.isScalarText(arg) ...
    && strlength(arg)>0 ...
    && startsWith(flagName, arg, "IgnoreCase", true);
end

function arg = iChar2String(arg)
% Convert char inputs to strings. Other types are left as-is.
if ischar(arg)
    arg = string(arg);
end
end