#-------------------------------------------------------------
#
# 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.
#
#-------------------------------------------------------------

# Alternating-Least-Squares (ALS) algorithm using a direct solve method for
# individual least squares problems (reg="L2"). This script computes an 
# approximate factorization of a low-rank matrix V into two matrices L and R.
# Matrices L and R are computed by minimizing a loss function (with regularization).
#
# INPUT:
# -------------------------------------------------------------------------------------------
# X        Location to read the input matrix V to be factorized
# rank     Rank of the factorization
# reg      Regularization parameter, no regularization if 0.0
# maxi     Maximum number of iterations
# check    Check for convergence after every iteration, i.e., updating L and R once
# thr      Assuming check is set to TRUE, the algorithm stops and convergence is declared
#          if the decrease in loss in any two consecutive iterations falls below this threshold;
#          if check is FALSE thr is ignored
# seed     The seed to random parts of the algorithm
# verbose  If the algorithm should run verbosely
# -------------------------------------------------------------------------------------------
#
# OUTPUT:
# -------------------------------------------------------------------------------------------
# U     An m x r matrix where r is the factorization rank
# V     An m x r matrix where r is the factorization rank
# -------------------------------------------------------------------------------------------

m_alsDS = function(Matrix[Double] X, Integer rank = 10, Double reg = 0.000001, 
  Integer maxi = 50, Boolean check = FALSE, Double thr = 0.0001, Integer seed = 321452, Boolean verbose = TRUE)
  return (Matrix[Double] U, Matrix[Double] V)
{
  r = rank;
  max_iter = maxi;

  # check the input matrix V, if some rows or columns contain only zeros remove them from V
  X_nonzero_ind = X != 0;
  row_nonzeros = rowSums (X_nonzero_ind);
  col_nonzeros = t (colSums (X_nonzero_ind));
  orig_nonzero_rows_ind = row_nonzeros != 0;
  orig_nonzero_cols_ind = col_nonzeros != 0;
  num_zero_rows = nrow (X) - sum (orig_nonzero_rows_ind);
  num_zero_cols = ncol (X) - sum (orig_nonzero_cols_ind);
  if (num_zero_rows > 0) {
    if( verbose )
      print ("Matrix X contains empty rows! These rows will be removed.");
    X = removeEmpty (target = X, margin = "rows");
  }
  if (num_zero_cols > 0) {
    if( verbose )
      print ("Matrix X contains empty columns! These columns will be removed.");
    X = removeEmpty (target = X, margin = "cols");
  }
  if (num_zero_rows > 0 | num_zero_cols > 0) {
    if( verbose )
      print ("Recomputing nonzero rows and columns!");
    X_nonzero_ind = X != 0;
    row_nonzeros = rowSums (X_nonzero_ind);
    col_nonzeros = t (colSums (X_nonzero_ind));
  }

  ###### MAIN PART ######
  m = nrow (X);
  n = ncol (X);

  # initializing factor matrices
  U = rand (rows = m, cols = r, min = -0.5, max = 0.5, seed=seed);
  V = rand (rows = n, cols = r, min = -0.5, max = 0.5, seed=seed+1);

  # initializing transformed matrices
  Xt = t(X);

  # check for regularization
  if ( verbose )
    print ("BEGIN ALS SCRIPT WITH NONZERO SQUARED LOSS + L2 WITH REG - " + reg);

  loss_init = 0.0; # only used if check is TRUE
  if (check) {
    loss_init = sum (X_nonzero_ind * (X - (U %*% t(V)))^2) 
      + reg * (sum ((U^2) * row_nonzeros) + sum ((V^2) * col_nonzeros));
    if( verbose )
      print ("----- Initial train loss: " + loss_init + " -----");
  }

  lambda_I = diag (matrix (reg, rows = r, cols = 1));
  it = 0;
  converged = FALSE;
  while ((it < max_iter) & (!converged)) {
    it = it + 1;
    # keep V fixed and update U
    parfor (i in 1:m) {
      V_nonzero_ind = t(X[i,] != 0);
      V_nonzero = removeEmpty (target=V * V_nonzero_ind, margin="rows");
      A1 = (t(V_nonzero) %*% V_nonzero) + (as.scalar(row_nonzeros[i,1]) * lambda_I); # coefficient matrix
      U[i,] = t(solve (A1, t(X[i,] %*% V)));
    }

    # keep U fixed and update V
    parfor (j in 1:n) {
      U_nonzero_ind = t(Xt[j,] != 0)
      U_nonzero = removeEmpty (target=U * U_nonzero_ind, margin="rows");
      A2 = (t(U_nonzero) %*% U_nonzero) + (as.scalar(col_nonzeros[j,1]) * lambda_I); # coefficient matrix
      V[j,] = t(solve (A2, t(Xt[j,] %*% U)));
    }

    # check for convergence
    if (check) {
      loss_cur = sum (X_nonzero_ind * (X - (U %*% t(V)))^2) 
        + reg * (sum ((U^2) * row_nonzeros) + sum ((V^2) * col_nonzeros));
      loss_dec = (loss_init - loss_cur) / loss_init;
      if( verbose )
        print ("Train loss at iteration (X) " + it + ": " + loss_cur + " loss-dec " + loss_dec);
      if (loss_dec >= 0 & loss_dec < thr | loss_init == 0) {
        if( verbose )
          print ("----- ALS converged after " + it + " iterations!");
        converged = TRUE;
      }
      loss_init = loss_cur;
    }
  } # end of while loop

  if(verbose) {
    if(check)
      print ("----- Final train loss: " + loss_init + " -----");
    if(!converged )
      print ("Max iteration achieved but not converged!");
  }

  # inject 0s in U if original X had empty rows
  if (num_zero_rows > 0)
    U = removeEmpty (target = diag (orig_nonzero_rows_ind), margin = "cols") %*% U;
  # inject 0s in V if original X had empty rows
  if (num_zero_cols > 0)
    V = removeEmpty (target = diag (orig_nonzero_cols_ind), margin = "cols") %*% V;
  V = t(V);
}
