PP_Optimizer = function(data, class = NA, findex = "HOLES", dimproj = 2, sphere = TRUE, 
                     optmethod = "GTSA", weight = TRUE, lambda = 0.1,  r = 1, 
                     cooling = 0.9, eps = 1e-3, maxiter = 3000, half = 30) {

  # Funcao de otimizacao dos indices da projection pursuit, desenvolvida
  # por Paulo Cesar Ossani in 2017/04/06.
  
  # Entrada:
  # data    - Conjunto de dados numericos sem informacao de classes.
  # class   - Vetor com os nomes das classes dos dados.
  # findex  - Funcao indice de projecao a ser usada:
  #           "lda" - Indice LDA,
  #           "pda" - Indice PDA,
  #           "lr" - Indice Lr,
  #           "holes" - Indice holes (default),
  #           "cm" - Indice massa central, 
  #           "pca" - Indice PCA,
  #           "friedmantukey" - Indice Friedman Tukey,
  #           "entropy" - Indice entropia,
  #           "legendre" - Indice Legendre,
  #           "laguerrefourier" - Indice Laguerre Fourier,
  #           "hermite" - Indice Hermite,
  #           "naturalhermite" - Indice Hermite natural,
  #           "kurtosismax" - Indice curtose maxima,
  #           "kurtosismin" - Indice curtose minima,
  #           "moment" - Indice momento, 
  #           "chi" - Indice qui-quadrado,
  #           "mf"  - Indice MF.
  # dimproj - Dimensao para a projecao dos dados (default = 2).
  # sphere  - Dados esfericos (default = TRUE).
  # optmethod - Metodo de otmizacao GTSA - Grand Tour Simulated Annealing 
  #             ou SA - Simulated Annealing (default = "GTSA").
  # weight  - Usado nos indice LDA, PDA e Lr, para ponderar os calculos
  #           pelo numero de elementos em cada classe (default = TRUE). 
  # lambda  - Usado no indice PDA (default = 0.1).
  # r       - Usado no indice Lr(default = 1).
  # cooling - Taxa de arrefecimento (default = 0.9).
  # eps     - Precisao de aproximacao para cooling (default = 1e-3).
  # maxiter - Numero maximo de iteracoes do algoritimo (default = 3000).
  # half    - Numero de etapas sem incremetar o indice, para em seguida
  #           diminuir o valor do cooling (default = 30).
  
  # Retorna:
  # num.class   - Numero de classes.
  # class.names - Nomes das classes.
  # proj.data   - Dados projetados.
  # vector.opt  - Vetores de projecao encontrados.
  # index       - Vetor com os indices de projecao encontrados no processo.
  # findex      - Funcao indice de projecao usada.
  
  if (!is.data.frame(data) && !is.matrix(data))
     stop("'data' input is incorrect, it should be of type data frame or matrix. Verify!")

  if (!is.na(class[1])) {
    
    class <- as.vector(class)
    
    if (nrow(data) != length(class))
       stop("'class' or 'data' input is incorrect, they should contain the same number of lines. Verify!")
  }

  if (findex %in% c("LDA", "PDA", "LR") && is.na(class[1]))
     stop("For the 'LDA', 'PDA' and 'LR' indices, needs input in 'class'. Verify!")
  
  findex <- toupper(findex) # transforma em maiusculo

  if (!(findex %in% c("LDA", "PDA", "LR", "HOLES", "CM", "PCA", "FRIEDMANTUKEY", "ENTROPY",
                      "LEGENDRE",  "LAGUERREFOURIER", "HERMITE", "NATURALHERMITE",
                      "KURTOSISMAX", "KURTOSISMIN", "MOMENT", "CHI", "MF")))
     # stop(paste("index function:",findex, "not registered. Verify!"))
     stop("'findex' input is incorrect, it should be: 'lda', 'pda', 'lr', 
          'holes', 'cm', 'pca', 'friedmantukey', 'entropy', 'legendre', 
          'laguerrefourier', 'hermite', 'naturalhermite', 'kurtosismax', 
          'kurtosismin', 'moment', 'chi' or 'mf'. Verify!")

  if ((findex %in% c("PCA","KURTOSISMAX", "KURTOSISMIN"))  && dimproj != 1)
     stop("For the index 'PCA', 'KURTOSISMAX' and 'KURTOSISMIN', 'dimproj' should be 1 (one). Verify!")

  if ((findex %in% c("MOMENT", "CHI", "FRIEDMANTUKEY", "ENTROPY", "LEGENDRE",
                     "LAGUERREFOURIER", "HERMITE", "NATURALHERMITE")) && dimproj != 2)
     stop("For the index 'MOMENT', 'CHI', 'FRIEDMANTUKEY', 'ENTROPY', 'LEGENDRE', 
          'LAGUERREFOURIER', 'HERMITE' and 'NATURALHERMITE', 'dimproj' should be 2 (two). Verify!")

  if (dimproj >= ncol(data))
     stop("dimproj greater, or equal to the number of columns in data. Verify!")

  if (!is.logical(sphere))
     stop("'sphere' input is incorrect, it should be TRUE or FALSE. Verify!")

  if (!is.logical(weight))
     stop("'weight' input is incorrect, it should be TRUE or FALSE. Verify!")
  
  if (!is.numeric(lambda) || lambda < 0 || lambda >= 1 )
     stop("'lambda' input is incorrect, it should be a numeric value between [0,1). Verify!")
  
  if (!is.numeric(r) || r <= 0 )
     stop("'r' input is incorrect, it should be a numeric value greater than zero. Verify!")
  
  optmethod <- toupper(optmethod) # transforma em maiusculo

  if (!(optmethod %in% c("GTSA", "SA")))
     stop("'optmethod' input is incorrect, it should be of type character, being 'GTSA' or 'SA'. Verify!")

  if (nrow(data) < ncol(data) && sphere)
     stop("For spherical data the number of observations should be greater, or equal to the number of variables in data. Verify!")

  if (!is.numeric(cooling) || cooling <= 0 || cooling >= 1)
     stop("'cooling' input is incorrect, it should be a numeric value between (0,1). Verify!")

  if (!is.numeric(eps) || eps <= 0 || eps >= 1)
     stop("'eps' input is incorrect, it should be a numeric value between (0,1). Verify!")

  if (!is.numeric(maxiter) || maxiter < 1 || (floor(maxiter)-maxiter) != 0)
     stop("'maxiter' input is incorrect, it should be a numeric integer greater than zero. Verify!")

  if (!is.numeric(half) || half < 1 || (floor(half)-half) != 0)
     stop("'half' input is incorrect, it should be a numeric integer greater than zero. Verify!")

  set.seed(7) # semente para o processo de aleatoriedade
  
  #### START - Useful Functions ####
  spheredata <- function(data) {
    # Esfera uma matriz (ou estrutura de dados), ou seja,
    # projecao das variaveis com componentes principais.
    # E um conjunto com a vista guiada, uma vez que remove padroes
    # mais simples que podem ocultar conclusoes mais interessantes.

    apply(predict(prcomp(data)), 2, scale)

    ## Tem o mesmo resultado do anterior
    # Dat <- scale(data, center = T, scale = F) # colunas centralizadas nas respctivas medias
    # a <- var(Dat)
    # t(diag(1/sqrt(eigen(a)$values))%*%t(eigen(a)$vectors)%*%t(Dat)) # projecao dos dados

  }

  
  Base <- function(NumLin, d) {
    # Esta funcao ajuda a encontrar uma base ortonormal usando as componentes principais

    dataBase <- matrix(rnorm(NumLin * d), ncol = d)

    return(dataBase)
  }


  if (optmethod == "GTSA") { # somente para o metodo de otimizacao grand tour simulated annealing

    Interpolation <- function(Aa, Az) {
      # This function performs matrix Aa interpolation in Az

      # Input:
      # Aa - Initial projection
      # Az - Projection target

      # Return:
      # A - Matrix of interpolated projection of Aa in Az

      if (!is_orthonormal(Aa)) Aa <- Orthonormalise(Aa)

      if (!is_orthonormal(Az)) Az <- Orthonormalise(Az)

      sv <- svd(t(Aa) %*% Az) # decomposicao de valor singular

      # Componentes das decomposicao de varlor singular
      NumCol <- ncol(Aa)
      lambda <- sv$d[NumCol:1] # do menor para o maior lambda para encontrar os planos mais proximos
      Va     <- sv$u[, NumCol:1]
      Vz     <- sv$v[, NumCol:1]

      # Planos para a projecao
      Ba <- Aa %*% Va
      Bz <- Az %*% Vz

      # Ortonormaliza os planos
      Ba <- Orthonormalise(Ba)
      Bz <- Orthonormalise(Bz)
      Bz <- Orthonormalise_by(Bz, Ba)

      # Calcula os angulos principais
      Tau <- suppressWarnings(acos(lambda)) # Gera uma mensagem de aviso que corresponde ao seu argumento
      Tau.NaN <- is.nan(Tau) | Tau < eps
      Tau[Tau.NaN] <- 0

      Bz[, Tau.NaN] <- Ba[, Tau.NaN]

      k <- 1
      # for (k in 1:length(Tau))
      while (k <= length(Tau)) {
        Bz[,k] <- Ba[,k] * cos(Tau[k]) + Bz[,k] * sin(Tau[k])
        k <- k + 1
      }

      A = Bz %*% Va

      return(A)
    }


    Normalise <- function(Base) {
      # Esta funcao normaliza uma Base
      sweep(Base, 2, sqrt(colSums(Base^2,na.rm = TRUE)), FUN = "/")
    }


    Orthonormalise_by <- function(MatX, MatY) {
      # Esta funcao Orthonormalize uma matriz por outra.Isso
      # garante que cada coluna em MatX seja ortogonal a coluna
      # correspondente em por MatY, usando o processo de Gram-Shimidt

      # verifica se as matrizes possuem o mesmo tamanho
      stopifnot(ncol(MatX) == ncol(MatY))
      stopifnot(nrow(MatX) == nrow(MatY))

      MatX <- Normalise(MatX)
      j <- 1
      while(j <= ncol(MatX)) { # processo de ortognalizacao de Gram-Schmidt
        MatX[, j] <- MatX[, j] - c(crossprod(MatX[, j], MatY[, j]) / sum(MatY[, j]^2)) * MatY[, j]
        # MatX[, j] <- MatX[, j] - crossprod(MatX[, j], MatY[, j]) * MatY[, j]
        j <- j + 1
      }

      Normalise(MatX)
    }


    Orthonormalise <- function(Base) {
      # Esta funcao encontra uma base ortogonal ou ortonormal
      # para os vetores em Base usando o processo de Gram-Shimidt

      Base <- Normalise(Base) # to be conservative

      if (ncol(Base) > 1) {
        j <- 1
        while(j <= ncol(Base)) { # processo de ortognalizacao de Gram-Schmidt
          i <- 1
          while(i <= (j - 1)) {
            Base[, j] <- Base[, j] - c(crossprod(Base[, j], Base[, i]) / sum(Base[, i]^2)) * Base[, i]
            #Base[, j] <- Base[, j] - crossprod(Base[, j], Base[, i]) * Base[, i]
            i <- i + 1
          }
          j <- j + 1
        }
      }

      Normalise(Base)
    }


    is_orthonormal <- function(data) {
      # Esta funcao verifica se data e ortonormal

      stopifnot(is.matrix(data))

      Limit <- 0.001

      j <- 1
      while(j <= ncol(data)) {
        if (sqrt(sum(data[, j] ^ 2)) < 1 - Limit) return(FALSE)
        j <- j + 1
      }

      if (ncol(data) > 1) {
        j <- 2
        while(j <= ncol(data)) {
          i <- 1
          while(i <= (ncol(data) - 1)) {
            if (abs(sum(data[, j] * data[, i])) > Limit) return(FALSE)
            i <- i + 1
          }
          j <- j + 1
        }
      }

      TRUE
    }
  }
  
  
  if (findex == "CHI") { # funcao usada no indice Qui-quadrado
    # Encontrar a probabilidade de normalizacao bivariada normal sobre cada caixa radial
    fnr <- function(x) { x * exp(-0.5 * x^2) } # veja que aqui a funcao normal padrao bivariada esta em Coordenadas Polares
    ck  <- rep(1,40)
    ck[1:8]   <- integrate(fnr, 0, sqrt(2*log(6))/5)$value/8
    ck[9:16]  <- integrate(fnr, sqrt(2*log(6))/5  , 2*sqrt(2*log(6))/5)$value/8
    ck[17:24] <- integrate(fnr, 2*sqrt(2*log(6))/5, 3*sqrt(2*log(6))/5)$value/8
    ck[25:32] <- integrate(fnr, 3*sqrt(2*log(6))/5, 4*sqrt(2*log(6))/5)$value/8
    ck[33:40] <- integrate(fnr, 4*sqrt(2*log(6))/5, 5*sqrt(2*log(6))/5)$value/8
  } else ck = NA
  
  #### END - Useful Functions ####

  NumCol <- ncol(data)

  if (sphere) {
     Dat <- spheredata(as.matrix(data)) # projeta dos dados usando as componentes principais
  } else Dat <- as.matrix(data)

  Aa <- diag(1, nrow = NumCol, ncol = dimproj) # Matrix of orthogonal initialization

  if (!(findex %in% c("LDA", "PDA", "LR", "CHI", "MF"))) {

     Proj <- Dat %*% Aa # initial projection

  } else Proj <- Dat
  
  Proj <- as.matrix(Proj)

  proj.data <- Proj
 
  indexMax <- PP_Index(data = Proj, class = class, vector.proj = Aa, findex = findex,
                       dimproj = dimproj, weight = weight, lambda = lambda, r = r, ck = ck)$index # Encontra o indice de acordo com findex

  index <- as.matrix(indexMax) # index of intical projection

  mi <- 1
  h  <- 0	# number of iterations without increase in index
  while (mi <= maxiter && cooling > eps) {

    Ai <- Base(NumCol, dimproj) # initial base
    
    Az <- Aa + cooling * Ai # projecao alvo

    if (optmethod == "GTSA") # somente para o metodo de otimizacao grand tour simulated annealing
       Az <-  Interpolation(Aa, Az) # Matriz de projecao atraves da interpolacao

    if (!(findex %in% c("LDA", "PDA", "CHI"))) {

       Proj <- Dat %*% Az # initial projection

    } else Proj <- Dat

    indexC <- PP_Index(data = Proj, class = class, vector.proj = Az, findex = findex,
                       dimproj = dimproj, weight = weight, lambda = lambda, r = r, ck = ck)  # Encontra o indice de acordo com findex

    indexCurent <- indexC$index

    print(paste ("Iteration <-", round(mi,1),"   index <-", round(indexMax,10), "   cooling <-", round(cooling,9)))#,"   i<- ", round(i,1)))

    if (findex == "LDA"   && indexCurent > indexMax ||
        findex == "PDA"   && indexCurent > indexMax ||
        findex == "LR"    && indexCurent > indexMax ||
        findex == "HOLES" && indexCurent > indexMax ||
        findex == "PCA"   && indexCurent > indexMax ||
        findex == "HERMITE"  && indexCurent > indexMax ||
        findex == "ENTROPY"  && indexCurent > indexMax ||
        findex == "LEGENDRE" && indexCurent > indexMax ||
        findex == "LAGUERREFOURIER" && indexCurent > indexMax ||
        findex == "FRIEDMANTUKEY"   && indexCurent > indexMax ||
        findex == "NATURALHERMITE"  && indexCurent > indexMax ||
        findex == "KURTOSISMAX" && indexCurent > indexMax || # favorece detectcao outliers
        findex == "KURTOSISMIN" && indexCurent < indexMax || # favorece detectcao agrupamento
        findex == "MOMENT" && indexCurent < indexMax ||
        findex == "CHI"    && indexCurent < indexMax ||
        findex == "MF"     && indexCurent < indexMax || # menor resultado foi melhor
        findex == "CM"     && indexCurent < indexMax) {
      
      if ((findex %in% c("LDA", "PDA", "CHI")))
         Proj <- Dat %*% Az # initial projection
      
      Aa        <- Az
      indexMax  <- indexCurent
      proj.data <- Proj
      index     <- rbind(index, indexCurent)

    } else h <- h + 1

    mi <- mi + 1

    if (h == half) {
       cooling <- cooling * 0.9
       h <- 0
    }

  }

  rownames(index) <- NULL

  rownames(proj.data)  <- rownames(data)

  rownames(Aa) <- colnames(data)

  if (!is.na(class[1])) {

    proj.data <- cbind(as.data.frame(proj.data), class)
    colnames(proj.data) <- c(paste("Projection", 1:(ncol(proj.data) - 1)),"class")

  } else colnames(proj.data) <- c(paste("Projection", 1:(ncol(proj.data))))

  colnames(Aa) <- c(paste("Axis", 1:(ncol(Aa))))
 
  if (length(index) > 1) colnames(index) <- "index"

  if (!is.na(class[1])) {
     class.Table <- table(class)        # cria tabela com as quantidade dos elementos das classes
     class.names <- names(class.Table)  # nomes das classses
     num.class   <- length(class.Table) # numero de classes
  } else {
     class.names <- NA # nomes das classses
     num.class   <- NA # numero de classes
  }
  
  Lista <- list(num.class = num.class, class.names = class.names,
                proj.data = proj.data, vector.opt = Aa, 
                index = index, findex = findex)
  
  return(Lista)
  
}  
