module GAClass
   use RandomClass
   use ArrayClass
   use MathClass
   implicit none

   type :: GA_annotaton
      character(:), allocatable :: annotation
   end type

   type :: GA_Individual_
      real(real64), allocatable  :: realParameter(:)
      integer(int32), allocatable:: intParameter(:)
      type(GA_annotaton), allocatable :: realAnnotaton(:)
      type(GA_annotaton), allocatable :: intAnnotaton(:)
      logical, allocatable :: realRegistered(:)
      logical, allocatable :: intRegistered(:)
   contains
      procedure, public :: init => initGA_Individual
   end type

   type :: GA_
      type(GA_Individual_), allocatable :: plants(:)
      real(real64), allocatable :: score(:)
      integer(int32), allocatable :: selected(:)
      real(real64), allocatable :: selectedScore(:)
      integer(int32) :: num_individual
      logical :: initialized = .false.
      logical :: registered = .false.
      logical :: realRegistered = .false.
      logical :: intRegistered = .false.
   contains
      procedure, public :: init => initGA
      procedure, public :: setup => setupGA
      procedure, public :: show => showGA
      procedure, public :: parse => parseGA
      procedure, public :: select => selectGA
      procedure, public :: cross => crossGA
      procedure, public :: mutate => mutateGA
   end type
contains

! #################################################################
   subroutine initGA_Individual(obj, num_real, num_int)
      class(GA_Individual_), intent(inout) :: obj
      integer(int32), intent(in) :: num_real, num_int

      if (num_real /= 0) then
         obj%realParameter = zeros(num_real)
         allocate (obj%realAnnotaton(num_real))
         allocate (obj%realRegistered(num_real))
         obj%realRegistered(:) = .false.
      end if

      if (num_int /= 0) then
         obj%intParameter = zeros(num_int)
         allocate (obj%intAnnotaton(num_int))
         allocate (obj%intRegistered(num_int))
         obj%intRegistered(:) = .false.
      end if

      !if(num_real==0) then
      !    obj%realRegistered = .true.
      !endif

      !if(num_int==0) then
      !    obj%intRegistered = .true.
      !endif
   end subroutine
! #################################################################
! #################################################################
! #################################################################
! #################################################################

! #################################################################
   subroutine initGA(obj, num_individual, num_real, num_int)
      class(GA_), intent(inout) :: obj
      integer(int32), intent(in) :: num_individual
      integer(int32), intent(in) :: num_real, num_int
      integer(int32) :: i

      if (allocated(obj%plants)) deallocate (obj%plants)

      if (num_real < 0 .or. num_int < 0) then
         print *, "ERROR :: initGA >> invalid num_real/num_int >> both should be >= 0"
         stop
      end if
      ! generate individuals
      allocate (obj%plants(num_individual))
      obj%score = zeros(num_individual)
      obj%num_individual = num_individual

      ! fill zero to initialize
      do i = 1, num_individual
         call obj%plants(i)%init(num_real=num_real, num_int=num_int)
      end do

      obj%initialized = .true.

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

! #####################################################################
   subroutine setupGA(obj, DataType, DataID, DataRange, DataAnnotation)
      class(GA_), intent(inout) :: obj
      integer(int32), intent(in) :: DataType, DataID
      real(real32), intent(in) :: DataRange(2)
      character(*), intent(in) :: DataAnnotation

      real(real32) :: DRange(2)
      real(real64) :: theta
      type(Random_) :: random
      integer(int32):: i

      DRange(1) = minval(DataRange)
      DRange(2) = maxval(DataRange)

      if (.not. obj%initialized) then
         print *, "ERROR :: setupGA >> not initialized. please call %init()"
         stop
      end if

      if (DataType == real64) then
         do i = 1, size(obj%plants)
            ! set random data
            ! by DRange
            theta = random%random()
            obj%plants(i)%realParameter(DataID) = (1.0d0 - theta)*Drange(1) + theta*Drange(2)
            obj%plants(i)%realAnnotaton(DataID)%annotation = DataAnnotation
            obj%plants(i)%realRegistered(DataID) = .true.
         end do

      elseif (DataType == int32) then
         do i = 1, size(obj%plants)
            ! set random data
            ! by DRange
            theta = random%random()
            obj%plants(i)%intParameter(DataID) = int((1.0d0 - theta)*Drange(1) + theta*Drange(2))
            obj%plants(i)%intAnnotaton(DataID)%annotation = DataAnnotation
            obj%plants(i)%intRegistered(DataID) = .true.
         end do

      else
         print *, "ERROR :: setupGA >> unknown datatype. please input real64 or int32"
         stop
      end if

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

! #####################################################################
   subroutine showGA(obj, KeyWord)
      class(GA_), intent(in) :: obj
      character(*), intent(in) :: KeyWord
      logical :: found = .false.
      integer(int32) :: i, j

      if (.not. obj%initialized) then
         print *, "[CAUTION] >> showGA >>  not initialized"
         return
      end if

      if (allocated(obj%plants(1)%realParameter)) then
         do i = 1, size(obj%plants(1)%realAnnotaton)
            if (index(obj%plants(1)%realAnnotaton(i)%annotation, KeyWord) /= 0) then
               print *, "Data :: "//obj%plants(1)%realAnnotaton(i)%annotation//" DataType :: Real64"
               do j = 1, size(obj%plants)
                  print *, "IndvID: ", j, " Value:", obj%plants(j)%realParameter(i)
               end do
               found = .true.
            end if
         end do
      end if

      if (allocated(obj%plants(1)%intParameter)) then
         do i = 1, size(obj%plants(1)%intParameter)
            if (index(obj%plants(1)%intAnnotaton(i)%annotation, KeyWord) /= 0) then
               print *, "Data :: "//obj%plants(1)%intAnnotaton(i)%annotation//" DataType :: int32"
               do j = 1, size(obj%plants)
                  print *, "IndvID: ", j, " Value:", obj%plants(j)%intParameter(i)
               end do
               found = .true.
            end if
         end do
      end if

      if (.not. found) then
         print *, "Not Found."
      end if

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

! #####################################################################
   function parseGA(obj, KeyWord) result(ret)
      class(GA_), intent(in) :: obj
      character(*), intent(in) :: KeyWord
      real(real64), allocatable :: ret(:)
      logical :: found = .false.
      integer(int32) :: i, j

      ret = zeros(obj%num_individual)

      if (.not. obj%initialized) then
         print *, "[CAUTION] >> showGA >>  not initialized"
         return
      end if

      if (allocated(obj%plants(1)%realParameter)) then
         do i = 1, size(obj%plants(1)%realAnnotaton)
            if (index(obj%plants(1)%realAnnotaton(i)%annotation, KeyWord) /= 0) then
               do j = 1, size(obj%plants)
                  ret(j) = obj%plants(j)%realParameter(i)
               end do
               found = .true.
            end if
         end do
         return
      end if

      if (allocated(obj%plants(1)%intParameter)) then
         do i = 1, size(obj%plants(1)%intParameter)
            if (index(obj%plants(1)%intAnnotaton(i)%annotation, KeyWord) /= 0) then
               do j = 1, size(obj%plants)
                  ret(j) = dble(obj%plants(j)%intParameter(i))
               end do
               found = .true.
            end if
         end do
         return
      end if

      if (.not. found) then
         print *, "Not Found."
      end if

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

! #####################################################################
   subroutine selectGA(obj, score, SurvivalRate)
      class(GA_), intent(inout) :: obj
      real(real64), intent(in) :: score(:), SurvivalRate
      integer(int32) :: num_selection, i, n
      real(real64), allocatable :: id(:), score_val(:)

      num_selection = int(SurvivalRate*obj%num_individual)
      if (num_selection == 0) then
         num_selection = 1
      end if
      id = zeros(obj%num_individual)
      do i = 1, size(id)
         id(i) = i
      end do
      score_val = score
      n = obj%num_individual
      call heapsort(array=score_val, val=id, n=obj%num_individual)
      obj%selected = id(n - num_selection + 1:n)
      obj%selectedScore = score_val(n - num_selection + 1:n)

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

! #####################################################################
   subroutine crossGA(obj)
      class(GA_), intent(inout) :: obj
      type(GA_) :: copy
      type(Random_) :: random
      integer(int32) :: i, j, num_select, itr, parent1, parent2
      real(real64), allocatable :: realbuf(:)
      real(real64) :: theta

      copy = obj
      num_select = size(obj%selected)
      realbuf = zeros(num_select)

      if (allocated(obj%plants(1)%realParameter)) then
         ! real parameter exists
         ! for all new individuals, update info.
         do i = 1, obj%num_individual
            ! determine parents
            parent1 = obj%selected(random%randint(from=1, to=num_select))
            parent2 = obj%selected(random%randint(from=1, to=num_select))
            ! create new data
            ! weighted averaging
            do j = 1, size(obj%plants(i)%realParameter)
               theta = random%random() ! 0 < theta < 1
               obj%plants(i)%realParameter(j) = &
                  (1.0d0 - theta)*copy%plants(parent1)%realParameter(j) + &
                  theta*copy%plants(parent2)%realParameter(j)
            end do
         end do
      end if

      if (allocated(obj%plants(1)%intParameter)) then
         ! int parameter exists
         ! for all new individuals, update info.
         do i = 1, obj%num_individual
            ! determine parents
            parent1 = obj%selected(random%randint(from=1, to=num_select))
            parent2 = obj%selected(random%randint(from=1, to=num_select))
            ! create new data
            ! random selection
            do j = 1, size(obj%plants(i)%intParameter)
               theta = random%random()
               if (theta >= 0.50d0) then
                  obj%plants(i)%intParameter(j) = copy%plants(parent1)%intParameter(j)
               else
                  obj%plants(i)%intParameter(j) = copy%plants(parent2)%intParameter(j)
               end if
            end do
         end do
      end if

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

! #####################################################################
   subroutine mutateGA(obj, KeyWord, sigma)
      class(GA_), intent(inout) :: obj
      character(*), intent(in) :: KeyWord
      real(real64), intent(in) :: sigma
      type(Random_) :: random
      logical :: found = .false.
      integer(int32) :: i, j

      if (allocated(obj%plants(1)%realParameter)) then
         do i = 1, size(obj%plants(1)%realAnnotaton)
            if (index(obj%plants(1)%realAnnotaton(i)%annotation, KeyWord) /= 0) then
               do j = 1, size(obj%plants)
                  obj%plants(j)%realParameter(i) = &
                     obj%plants(j)%realParameter(i) + random%gauss(mu=0.0d0, sigma=sigma)
               end do
               found = .true.
            end if
         end do
         return
      end if

      if (allocated(obj%plants(1)%intParameter)) then
         do i = 1, size(obj%plants(1)%intParameter)
            if (index(obj%plants(1)%intAnnotaton(i)%annotation, KeyWord) /= 0) then
               do j = 1, size(obj%plants)
                  obj%plants(j)%intParameter(i) = &
                     int(dble(obj%plants(j)%intParameter(i)) + random%gauss(mu=0.0d0, sigma=sigma))
               end do
               found = .true.
            end if
         end do
         return
      end if

      if (.not. found) then
         print *, "Not Found."
      end if

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

end module
