# ==============================================================================
# File: algo_adv.R
# Description: Internal functions for advanced probabilistic and machine
# learning methods (PPCA with EM Algorithm and Generalized Hebbian Algorithm).
# ==============================================================================

# ------------------------------------------------------------------------------
# 8. Probabilistic PCA (PPCA) with EM Algorithm
# ------------------------------------------------------------------------------
.apca_ppca <- function(X, n_components, max_iter = 1000, tol = 1e-9) {
  N <- nrow(X)
  D <- ncol(X)

  # Initialize Loadings (W) and noise variance (sigma2)
  W <- matrix(rnorm(D * n_components), nrow = D, ncol = n_components)
  sigma2 <- var(as.vector(X))

  for (iter in 1:max_iter) {
    # Expectation Step (E-Step)
    M <- t(W) %*% W + diag(sigma2, n_components)
    M_inv <- solve(M)
    Ez <- X %*% W %*% M_inv
    Ezz <- N * sigma2 * M_inv + t(Ez) %*% Ez

    # Maximization Step (M-Step)
    W_new <- t(X) %*% Ez %*% solve(Ezz)

    # Update sigma2 (Noise variance)
    sum_x2 <- sum(X^2)
    term2 <- 2 * sum(diag(t(Ez) %*% X %*% W_new))
    term3 <- sum(diag(Ezz %*% t(W_new) %*% W_new))
    sigma2_new <- (sum_x2 - term2 + term3) / (N * D)

    # Convergence check based on Loadings
    if (max(abs(W_new - W)) < tol) {
      W <- W_new
      sigma2 <- sigma2_new
      break
    }
    W <- W_new
    sigma2 <- sigma2_new
  }

  # Orthogonalize W using SVD to align with standard PCA principal axes
  svd_W <- svd(W)
  loadings <- svd_W$u[, 1:n_components, drop = FALSE]

  scores <- X %*% loadings
  eigenvalues <- apply(scores, 2, var)
  total_var <- sum(apply(X, 2, var))
  var_explained <- eigenvalues / total_var

  colnames(loadings) <- paste0("PC", 1:n_components)
  colnames(scores) <- paste0("PC", 1:n_components)

  list(
    scores = scores,
    loadings = loadings,
    eigenvalues = eigenvalues,
    variance_explained = var_explained,
    sigma_squared = sigma2
  )
}

# ------------------------------------------------------------------------------
# 9. Generalized Hebbian Algorithm (GHA) / Sanger's Rule
# ------------------------------------------------------------------------------
.apca_gha <- function(X, n_components, learning_rate = 0.01, epochs = 50) {
  N <- nrow(X)
  D <- ncol(X)

  # Initialize synaptic weights with small random numbers
  W <- matrix(rnorm(D * n_components, mean = 0, sd = 0.1), nrow = D, ncol = n_components)

  # Epoch-based iteration for online learning
  for (epoch in 1:epochs) {
    # Learning rate decay to ensure eventual convergence
    lr <- learning_rate / (1 + 0.01 * epoch)

    for (i in 1:N) {
      x_i <- X[i, ]
      y_i <- as.numeric(x_i %*% W) # Neural response

      # Sanger's Rule Update (Sequential partial reconstruction)
      for (j in 1:n_components) {
        if (j == 1) {
          reconstruction <- y_i[1] * W[, 1]
        } else {
          reconstruction <- W[, 1:j] %*% y_i[1:j]
        }

        delta_w <- lr * y_i[j] * (x_i - as.numeric(reconstruction))
        W[, j] <- W[, j] + delta_w
      }
    }
  }

  # Normalize column vectors to unit length
  W <- apply(W, 2, function(x) x / sqrt(sum(x^2)))

  # Edge-case handling: Ensure W remains a matrix if n_components = 1
  if (!is.matrix(W)) {
    W <- matrix(W, nrow = D, ncol = n_components)
  }

  scores <- X %*% W
  eigenvalues <- apply(scores, 2, var)
  total_var <- sum(apply(X, 2, var))
  var_explained <- eigenvalues / total_var

  colnames(W) <- paste0("PC", 1:n_components)
  colnames(scores) <- paste0("PC", 1:n_components)

  list(
    scores = scores,
    loadings = W,
    eigenvalues = eigenvalues,
    variance_explained = var_explained
  )
}
