function [idx,dist]=knnsearch(obj,Y,varargin)
%KNNSEARCH Find K approximate nearest neighbors using an hnswSearcher object.
%   IDX = KNNSEARCH(NS,Y) finds the approximate nearest neighbor (closest 
%   point) in X=NS.X for each point in Y. Rows of Y correspond to observations 
%   and columns correspond to variables. Y must have the same number of
%   columns as X. IDX is a column vector with NY rows, where NY is the
%   number of rows in Y. Each row in IDX contains the index of the
%   observation in X that has the approximate minimum distance to the 
%   corresponding row in Y. The distance metric used here is NS.Distance,
%   and the additional parameter for the distance metric are specified by
%   NS.DistParameter.
%
%   [IDX, D] = KNNSEARCH(NS,Y) returns a column vector D containing
%   the distance between each row of Y and its approximate closest point 
%   in X. That is, D(I) is the distance between X(IDX(I),:) and Y(I,:).
%
%   [IDX, D]= KNNSEARCH(NS,Y,'NAME1',VALUE1,...,'NAMEN',VALUEN) specifies
%   optional argument name/value pairs.
%
%     Name          Value
%     'K'           A positive integer, K, specifying the number of nearest
%                   neighbors in NS.X for each point in Y, capped at a
%                   maximum value of the number of rows in X. Default is 1. 
%                   IDX and D are NY-by-K matrices. D sorts the distances 
%                   in each row in ascending order. Each row in IDX contains
%                   the indices of K closest neighbors in X corresponding
%                   to the K smallest distances in D.
%                   When the MaxNumLinksPerNode option is too small, the 
%                   internal graph in hnswSearcher can be disconnected. In 
%                   this case, knnsearch can fail to find all requested 
%                   points, and fills in the missing neighbor indices and 
%                   distances with NaN.
%
%     'SearchSetSize'
%                   Size of the candidate list of nearest neighbors for a 
%                   single query point during the search process, a positive 
%                   integer. Larger values lead to a more complete search, 
%                   which increases the likelihood of finding the true 
%                   nearest neighbors, but slows the search. 'SearchSetSize' 
%                   must be at least K, and must not exceed the number of
%                   rows in X. Default is the maximum of 10 and K, capped
%                   at a maximum value of the number of rows in X.
%
% 
%   Example:
%      % Create an hnswSearcher object for data X.
%      X = randn(100,200);
%      Y = randn(25,200);
%      ns = createns(X, 'NSMethod', 'hnsw');
%
%      % Find 5 approximate nearest neighbors in X for each point in Y
%      idx = knnsearch(ns,Y,'k',5);
%
%   See also KNNSEARCH, hnswSearcher, CREATENS, KDTreeSearcher, PDIST2.

%   Copyright 2023 The MathWorks, Inc.

% 1. Sanity check for input data
if ~isempty(find(cellfun(@istall, [{Y},varargin(:)']), 1, 'first'))
    error(message('MATLAB:bigdata:array:FcnNotSupported', upper(mfilename)))
end

% gpuArrays are not supported
if isa(Y,'gpuArray')
    error(message('stats:ApproximateSearcher:hnswSearcher:GpuArrayData'));
end

% Integer/logical/char/anything data will be converted to float. Complex
% floating point data can't be handled.
try
    outClass = superiorfloat(obj.X,Y);
catch
    outClass = class(obj.X);
end

if ~strcmpi(class(Y), outClass)
    warning(message('stats:ApproximateSearcher:hnswSearcher:DataConversion', class(Y), outClass));
    Y = cast(Y,outClass);    
end

% Normalize data for specific distance metrics
if ismember(obj.Distance, {'cosine', 'correlation', 'spearman'})
    Y = hnswSearcher.normalizeData(Y, obj.Distance);
end

% Transpose the data based on the distance metric and cast if necessary.
if ismember(obj.Distance, {'cosine', 'correlation', 'spearman'})
    % For 'cosine', 'correlation', and 'spearman', use normalized data.
    X_transpose = obj.PrecomputedData';
else
    % For other distance metrics, use the original data.
    X_transpose = obj.X';
end

% Perform casting when X is double and Y is single
if ~strcmpi(class(obj.X), outClass)
    X_transpose = cast(X_transpose, outClass);
end

%  Complex floating point data can't be handled
if ~isreal(Y)
    error(message('stats:ApproximateSearcher:hnswSearcher:ComplexData'));
end

%convert sparse data to full, hnswSearcher can't handle
%sparse data directly
if issparse(Y)
    warning(message('stats:ApproximateSearcher:hnswSearcher:DataConversion', 'sparse', 'full'));
    Y = full(Y);
end

[nX,nDims] = size(obj.X);
[nY,nDims2]= size(Y);
if nDims2 ~= nDims
    error(message('stats:ApproximateSearcher:hnswSearcher:SizeMisMatch', nDims));
end

% 2. Parse varargin
[varargin{:}] = convertStringsToChars(varargin{:});
pnames = { 'k'  'SearchSetSize'};
dflts =  { 1    []            };
[numNN, SearchSetSize, setFlag] = statslib.internal.parseArgs(pnames, dflts, varargin{:});
if ~setFlag.SearchSetSize
    SearchSetSize = min( max(10,numNN), nX);
end

validateattributes(numNN,{'numeric'}, ...
    {'scalar','real','integer','positive'},mfilename,'K');
numNN = min(numNN, nX);

% Degenerate case, just return an empty matrix or cell of the proper size.
if (nY == 0 || numNN ==0)
    idx = zeros(nY, numNN);
    dist = zeros(nY, numNN, outClass);
    return;
end

validateattributes(SearchSetSize,{'numeric'}, ...
    {'scalar','real','integer','positive'},mfilename,'SearchSetSize');
if SearchSetSize>nX || SearchSetSize<numNN
    error(message('stats:ApproximateSearcher:hnswSearcher:BadSearchSetSize'));
end

% 3. Perform the search operation
verbose=0;

switch obj.Distance
    case {'seuclidean', 'mahalanobis'}
        % Use precomputed distance parameters stored in 
        % PrecomputedData for 'seuclidean' and 'mahalanobis' 
        % distances.
        [idx,dist]=internal.stats.knnsearchHNSW(X_transpose, Y', numNN, SearchSetSize, obj.PrivDistance(1:3), ...
            obj.SearchStructure, obj.NumLvs, obj.NodeIDEntryPoint, obj.NumNodesPerLv, ...
            verbose, cast(obj.PrecomputedData,outClass), obj.MaxNumLinksPerNode);
    otherwise
        % For other distance metrics, use default settings.
        [idx,dist]=internal.stats.knnsearchHNSW(X_transpose, Y', numNN, SearchSetSize, obj.PrivDistance(1:3), ...
            obj.SearchStructure, obj.NumLvs, obj.NodeIDEntryPoint, obj.NumNodesPerLv, ...
            verbose, cast(obj.PrivDistParameter,outClass), obj.MaxNumLinksPerNode);
end

idx = idx';
dist = dist';
end %method knnsearch
