#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------

# Imputes missing values, indicated by NaNs, using KNN-based methods
# (k-nearest neighbors by euclidean distance). In order to avoid NaNs in
# distance computation and meaningful nearest neighbor search, we initialize
# the missing values by column means. Currently, only the column with the most
# missing values is actually imputed.
#
# INPUT:
# ------------------------------------------------------------------------------
# X             Matrix with missing values, which are represented as NaNs
# method        Method used for imputing missing values with different performance and accuracy tradeoffs:\n
#               - 'dist' (default): Compute all-pairs distances and impute the missing values by closest. O(N^2 * #features)
#               - 'dist_missing': Compute distances between data and records with missing values. O(N*M * #features), assuming that the number of records with MV is M<<N.
#               - 'dist_sample': Compute distances between sample of data and records with missing values. O(S*M * #features) with M<<N and S<<N, but suboptimal imputation.
#
# seed          Root seed value for random/sample calls for deterministic behavior. -1 for true randomization
# sample_frac   Sample fraction for 'dist_sample' (value between 0 and 1)
# ------------------------------------------------------------------------------
#
# OUTPUT:
# ------------------------------------------------------------------------------
# result     Imputed dataset
# ------------------------------------------------------------------------------

m_imputeByKNN = function(Matrix[Double] X, String method="dist", Int seed=-1, Double sample_frac=0.1)
  return(Matrix[Double] result)
{
  #KNN-Imputation Script

  imputedValue = X

  #Impute NaN value with temporary mean value of the column
  filled_matrix = imputeByMean(X, matrix(0, cols = ncol(X), rows = 1))

  if(method == "dist") {
    #Calculate the distance using dist method after imputation with mean
    distance_matrix = dist(filled_matrix)

    #Change 0 value so rowIndexMin will ignore that diagonal value
    distance_matrix = replace(target=distance_matrix, pattern=0, replacement=999)

    #Get the minimum distance row-wise computation
    minimum_index = rowIndexMin(distance_matrix)

    #Create aligned matrix from minimum index
    aligned = table(minimum_index, seq(1, nrow(X)), odim1=nrow(X), odim2=nrow(X))

    #Get the X records that need to be imputed
    imputedValue = t(filled_matrix) %*% aligned
    imputedValue = t(imputedValue)
  }
  else if(method == "dist_missing") {
    #assuming small missing values
    imputedValue = compute_missing_values(X, filled_matrix, seed, 1.0)
  }
  else if(method == "dist_sample"){
    #assuming large missing values
    imputedValue = compute_missing_values(X, filled_matrix, seed, sample_frac)
  }
  else {
    stop("Method is unknown or not yet implemented")
  }

  #Impute the value
  result = replace(target=X, pattern=NaN, replacement=0)
  result = result + (imputedValue * is.nan(X))
}

compute_missing_values = function (Matrix[Double] X, Matrix[Double] filled_matrix, Int seed, Double sample_frac)
    return (Matrix[Double] imputedValue)
{
    #Split the matrix into containing NaN values (missing records) and not containing NaN values (M2 records)
    maskNAN = is.nan(X)
    I = rowSums(maskNAN) != 0
    missing = removeEmpty(target=filled_matrix, margin="rows", select=I)

    Y = (rowSums(maskNAN)==0)
    M2 = removeEmpty(target=X, margin = "rows", select = Y)

    if (sample_frac != 1.0) {
        #Create permutation matrix for sampling sample_frac*nrow(X) rows
        I = rand(rows=nrow(M2), cols=1, seed=seed) <= sample_frac;
        M2 = removeEmpty(target=M2, margin="rows", select=I);
    }

    #Calculate the euclidean distance between fully records and missing records, and then find the min value row wise
    dotM2 = rowSums(M2 * M2) %*% matrix(1, rows = 1, cols = nrow(missing))
    dotMissing = t(rowSums(missing * missing) %*% matrix(1, rows = 1, cols = nrow(M2)))
    D = sqrt(dotM2 + dotMissing - 2 * (M2 %*% t(missing)))
    minD = rowIndexMin(t(D))

    #Get the index location of the missing value
    pos = rowMaxs(maskNAN)
    missing_indices = seq(1, nrow(pos)) * pos

    #Put the replacement value in the missing indices
    I2 = removeEmpty(target=missing_indices, margin="rows")
    R = table(I2, 1, minD, odim1=nrow(X), odim2=1)

    #Replace the 0 to avoid error in table()
    R = replace(target=R, pattern=0, replacement=nrow(X)+1)

    #Create aligned matrix from minimum index
    aligned = table(R, seq(1, nrow(X)), odim1=nrow(X), odim2=nrow(X))

    #Reshape the subset
    reshaped = rbind(M2, matrix(0, rows=nrow(X) - nrow(M2), cols=ncol(X)))

    #Get the subset records that need to be imputed
    imputedValue = t(reshaped) %*% aligned
    imputedValue = t(imputedValue)
}