function [bins,edges] = discretize(x,edgesInput,varargin)
%MATLAB Code Generation Library Function

%   Copyright 2018-2020 The MathWorks,Inc.
%#codegen

narginchk(2,inf);
coder.internal.prefer_const(edgesInput,varargin)
ONE = coder.internal.indexInt(1);
TWO = coder.internal.indexInt(2);

% Validate X
validateattributes(x,{'numeric','logical'},{'real'},mfilename,'x',1);
% Validate edges or bins
if coder.internal.isConstTrue(isscalar(edgesInput))
    validateattributes(edgesInput,{'numeric','logical'},{'integer', 'positive', ...
        'finite'}, mfilename, 'edges', 2);
    % Execute if bins are specified
    numbins = double(edgesInput);
    coder.internal.assert(isfloat(x),'MATLAB:discretize:InputNonFloatWithNumBins');
    [nxInLimits,xMinInLimits,xMaxInLimits] = rangeTool(x);
    binWidth = (xMaxInLimits - xMinInLimits)/cast(numbins,'like',xMaxInLimits);
    edges = binPicker(nxInLimits,xMinInLimits,xMaxInLimits,numbins,binWidth);
else
    % Execute if edges are specified
    coder.internal.assert(~isscalar(edgesInput),'MATLAB:discretize:NumbinsNotFixedSize');
    coder.internal.assert(length(edgesInput) >=2,'MATLAB:discretize:EmptyOrScalarEdges');
    validateattributes(edgesInput,{'numeric','logical'},{'vector', 'real', ...
        'nondecreasing'}, mfilename, 'edges', 2);
    edges = edgesInput;
end

% Validate that X can be cast to a numerical value if it is an object
if isobject(x)
    x = castToBuiltinSuperclass(x);
end

% Validate that edges can be cast to numerical values if they are objects
if isobject(edges)
    edges = castToBuiltinSuperclass(edges);
end

edges = full(edges); %If edges are sparse, convert to full storage
nbins = length(edges)-1;

nargs = coder.internal.indexInt(nargin - 2);

right = false;
valuesProvided = false;
categoricalOutput = false;
categoriesProvided = false;


% Parse optional inputs
if nargs >= ONE
    nextArg = ONE;
    
    % Check if first optional input is a vector of bin values
    if coder.internal.isConstTrue(isvector(varargin{1})) && ...
            ~coder.internal.isConstTrue(isempty(varargin{1})) && ...
            ~ischar(varargin{1}) && ~isstring(varargin{1}) && ~isa(varargin{1},'function_handle')
        valuesProvided = true;
        values = varargin{1};
        %Verify that the number of bin values matches the number of bins
        coder.internal.assert(length(values) == nbins,'MATLAB:discretize:ValuesInvalidSize',nbins); 
        nextArg = TWO;
    else
        coder.internal.assert(coder.internal.isConst(varargin{1}), ...
            'Coder:toolbox:OptionStringsMustBeConstant');
    end
    nargsLeft = nargs - nextArg + 1;
    
    % Check if the remaining number of arguments is odd. If so, 'categorical'
    % will be expected as the next argument
    oddNargsLeft = eml_bitand(nargsLeft,ONE) == ONE;
    if nextArg<=nargs
        coder.internal.assert(coder.internal.isConst(varargin{nextArg}), ...
            'Coder:toolbox:OptionStringsMustBeConstant');
        if coder.internal.isTextRow(varargin{nextArg}) &&...
                startsWith("categorical",varargin{nextArg},'IgnoreCase',true)
            categoricalOutput = true;
            if ~oddNargsLeft
                % If 'categorical' appears and there is an even number of
                % arguments, category names are expected
                isValidCategory = coder.internal.isConstTrue(isvector(varargin{nextArg+ONE})) && ...
                    ~coder.internal.isConstTrue(isempty(varargin{nextArg+ONE})) && ...
                    (iscellstr(varargin{nextArg+ONE}) || isstring(varargin{nextArg+ONE}));
                % Validate category names
                coder.internal.assert(isValidCategory,'MATLAB:discretize:InvalidCatnames')
                categoriesProvided = true;
                catNames = varargin{nextArg+ONE};
                coder.internal.assert(length(catNames) == nbins,'MATLAB:discretize:CategoryNamesInvalidSize',nbins);
                nextArg = nextArg + TWO;
            else
                nextArg = nextArg + ONE;
            end
        else
            coder.internal.assert(~oddNargsLeft,'MATLAB:discretize:KeyWithoutValue')
        end
    end
    % Parse remaining arguments
    for k = coder.unroll(nextArg:TWO:nargs-ONE)
        coder.internal.assert(coder.internal.isConst(varargin{k}), ...
            'Coder:toolbox:OptionStringsMustBeConstant');
        name = validatestring(varargin{k},{'IncludedEdge'}, ...
            mfilename,'',k + 2);
        if strcmp(name,'IncludedEdge')
            right = strcmp(validatestring(varargin{k+1},{'left','right'}, ...
                mfilename,'',k + 3),'right');
        end
    end
end

% If category names have not been provided, generate default names
if categoricalOutput && ~categoriesProvided
    catNames =  numericBinEdgesToCategoryNames(edges,right);
end

b = mapElementsToBins(x,edges,right);

if valuesProvided
    % Check that all values in X have been assigned bins
    % Unassigned values are given nan as their bin number
    nanbins = isnan(b(:));
    containsNAN = any(nanbins);
    
    % Throw error if bin values are not numeric and data is not assigned a bin
    coder.internal.assert(isnumeric(values) || ~containsNAN, ...
        'MATLAB:discretize:ValuesClassNoNaN',class(values));
    if containsNAN && isnumeric(values)
        % Verify nan support if bin values are numeric
        NANOK = coder.const(feval('matlab.internal.math.isNaNSupported',coder.internal.scalarEg(values)));
        coder.internal.assert(NANOK,'MATLAB:discretize:ValuesClassNoNaN',class(values));
        % Assign bin values to elements of X
        v1 = [values(:);coder.internal.nan];
        b(nanbins) = length(v1);
        bins_tmp = reshape(v1(b),size(x));
    else
        % Assign bin values to elements of X (handles numeric and non-numeric data)
        bins_tmp = assignValuesToBins(b,values,size(x));
    end
else
    bins_tmp = b;
end

if categoricalOutput
    % For categorical outputs, replace bin values with categories
    bins = categorical(bins_tmp,1:nbins,catNames,'Ordinal',true);
else
    bins = bins_tmp;
end


%--------------------------------------------------------------------------

function y = castToBuiltinSuperclass(x)
% castToBuiltinSuperclass  Internal utility function to cast objects of
% a subclass of a builtin type to their builtin superclass.

if isobject(x)
    if isa(x, 'double')
        y = double(x);
    elseif isa(x, 'single')
        y = single(x);
    elseif isa(x, 'uint8')
        y = uint8(x);
    elseif isa(x, 'int8')
        y = int8(x);
    elseif isa(x, 'uint16')
        y = uint16(x);
    elseif isa(x, 'int16')
        y = int16(x);
    elseif isa(x, 'uint32')
        y = uint32(x);
    elseif isa(x, 'int32')
        y = int32(x);
    elseif isa(x, 'uint64')
        y = uint64(x);
    elseif isa(x, 'int64')
        y = int64(x);
    elseif isa(x, 'logical')
        y = logical(x);
    else
        coder.internal.assert(false,'MATLAB:castToBuiltinSuperclass:UnsupportedType');
    end
else
    y = x;
end

%--------------------------------------------------------------------------

function bins = mapElementsToBins(x,edges,right)

bins = coder.internal.nan(size(x));
nx = coder.internal.indexInt(numel(x));
leftEdge = edges(1);
rightEdge = edges(end);
delta = edges(2) - edges(1);
nedges = numel(edges);
for k = 1:nx
    if x(k) >= leftEdge && x(k) <= rightEdge
        bGuess = ceil((x(k) - leftEdge)/delta);
        if right
            if bGuess >= 1 && bGuess < nedges && ...
                    x(k) > edges(bGuess) && x(k) <= edges(bGuess + 1)
                bins(k) = bGuess;
            else
                b = bsearchR(edges,x(k));
                bins(k) = b;
            end
        else
            if bGuess >= 1 && bGuess < nedges && ...
                    x(k) >= edges(bGuess) && x(k) < edges(bGuess + 1)
                bins(k) = bGuess;
            else
                b = coder.internal.bsearch(edges,x(k));
                bins(k) = b;
            end
        end
    end
end

%--------------------------------------------------------------------------

function n = bsearchR(x,xi)
n = coder.internal.bsearch(x,xi);
while n>1 && xi <= x(n)
    n = n - 1;
end

%--------------------------------------------------------------------------

function bins = assignValuesToBins(b,values,sizeX)
% assignValuesToBins Internal utility function to create a matrix of bin
% values if bin values are not a numeric data type
if iscell(values)
    % cell arrays cannot be resized by assignment so bin values must
    % be assigned to a new array
    ONE = coder.internal.indexInt(1);
    nb = coder.internal.indexInt(numel(b));
    bins = cell(sizeX);
    for k = ONE:nb
        bins{k} = values{b(k)};
    end
else
    bins = reshape(values(b),sizeX);
end

%--------------------------------------------------------------------------

function names = numericBinEdgesToCategoryNames(edges,closedRight)
% numericBinEdgesToCategoryNames Internal utility function to generate
% category names from bin edges

if closedRight
    leftedge = '(';
    rightedge = ']';
else
    leftedge = '[';
    rightedge = ')';
end

nbins = length(edges)-1;
names = cell(1,nbins);
for i = 1:nbins
    names{i} = sprintf([leftedge '%0.5g, %0.5g' rightedge],edges(i),edges(i+1));
end

% Put the correct delimiter at the "opposite" extreme limit.
if closedRight
    names{1}(1) = '[';
else
    names{end}(end) = ']';
end

% If the edges are not distinguishable using 5 digits, error. Do this after
% replacing the extreme delimiter. It can happen that the 1st and 2nd bin, or
% the (N-1)st and Nth bins, would differ subtlely only by a [ vs. ( or ] vs. )
% after that replacement. Allowing this is intentional. Note that this
% will allow up to three repeated edge values at either extreme, but not in
% the middle.

coder.internal.assert(uniqueCategoryNames(names),'MATLAB:discretize:DefaultCategoryNamesNotUnique');

function out = uniqueCategoryNames(in)
out = true;
ONE =  coder.internal.indexInt(1);
for ii = ONE:coder.internal.indexInt(numel(in))-ONE
    for jj = ii+ONE:coder.internal.indexInt(numel(in))
        if strcmp(in{ii},in{jj})
            out = false;
            return
        end
    end
end
