module PCAClass
   use IOClass
   use MathClass
   use ArrayClass
   implicit none

   type :: PCA_
      logical :: columns_are_feature_names = .true.
      real(real64), allocatable :: DataFrame(:, :)
      real(real64), allocatable :: CovMatrix(:, :)
      real(real64), allocatable :: EigenVector(:, :)
      real(real64), allocatable :: EigenValue(:)
   contains
      procedure :: load => loadPCA
      procedure :: standarize => standarizePCA
      procedure :: run => runPCA
      procedure :: principalComponent => principalComponentPCA
   end type
contains

! #############################################################
   subroutine loadPCA(obj, filename, DataFrame, columns_are_feature_names, num_feature)
      class(PCA_), intent(inout) :: obj
      real(real64), optional, intent(in) :: DataFrame(:, :)
      character(*), optional, intent(in) :: filename
      logical, optional, intent(in) :: columns_are_feature_names
      integer(int32), optional, intent(in) :: num_feature
      type(IO_) ::f
      character(:), allocatable :: line
      integer(int32) :: n

      if (present(filename)) then
         if (.not. present(num_feature)) then
            print *, "ERROR :: LoadPCA >> Argument: num_feature should be set."
            stop
         end if
         n = f%numLine(filename)
         obj%DataFrame = zeros(n, num_feature)
         call f%open(filename, "r")
         n = 1
         do while (n <= 10)
            line = f%readline()
            print *, line
            read (line, *) obj%DataFrame(n, 1:num_feature)
            n = n + 1
         end do
         call f%close()
         call print(obj%DataFrame)
      else
         if (present(columns_are_feature_names)) then
            obj%columns_are_feature_names = columns_are_feature_names
         end if
         obj%DataFrame = DataFrame
      end if

   end subroutine
! #############################################################

! #############################################################
   subroutine standarizePCA(obj)
      class(PCA_), intent(inout) :: obj
      real(real64) :: sdval, aveval
      integer(int32) :: n

      ! get covarianceMatrix
      if (.not. obj%columns_are_feature_names) then
         n = size(obj%DataFrame, 1)
         obj%DataFrame = transpose(obj%DataFrame)
      end if

      do n = 1, size(obj%DataFrame, 2)
         sdval = standardDeviation(obj%DataFrame(:, n))
         aveval = average(obj%DataFrame(:, n))
         obj%DataFrame(:, n) = obj%DataFrame(:, n) - aveval
         obj%DataFrame(:, n) = obj%DataFrame(:, n)/sdval
      end do

   end subroutine
! #############################################################

! #############################################################
   subroutine runPCA(obj)
      class(PCA_), intent(inout) :: obj
      integer(int32) :: i, j, n

      ! get covarianceMatrix
      if (obj%columns_are_feature_names) then
         n = size(obj%DataFrame, 2)
         obj%CovMatrix = covarianceMatrix(obj%DataFrame, obj%DataFrame, n)
      else
         n = size(obj%DataFrame, 1)
         obj%DataFrame = transpose(obj%DataFrame)
         obj%columns_are_feature_names = .true.
         obj%CovMatrix = covarianceMatrix(obj%DataFrame, &
                                          obj%DataFrame, n)
      end if

      call eigenValueAndVector(obj%CovMatrix, lambda=obj%EigenValue, x=obj%eigenVector)

      print *, "Eigen value::"
      call print(obj%EigenValue)
      do i = 1, size(obj%EigenValue)
         print *, "Eigen Vector #"//str(i)
         print *, obj%eigenVector(:, i)
      end do

   end subroutine
! #############################################################

! #############################################################
   function principalComponentPCA(obj) result(ret)
      class(PCA_), intent(in) :: obj
      real(real64), allocatable :: ret(:, :), Wmat(:, :)
      integer(int32) :: numdata, numfeature

      numdata = size(obj%DataFrame, 1)
      numfeature = size(obj%DataFrame, 2)
      ret = zeros(numdata, 2)
      Wmat = zeros(numfeature, 2)
      Wmat(:, 1) = obj%eigenVector(1, :)
      Wmat(:, 2) = obj%eigenVector(2, :)

      ! First Principal Component
      ret(:, 1:2) = matmul(obj%DataFrame, Wmat)

   end function
! #############################################################

end module PCAClass
