
module SceneClass
   use FEMDomainClass
   use SoybeanClass
   implicit none

   type :: Scene_
      type(FEMDomainp_), allocatable :: femdomain(:)
      type(Soybeanp_), allocatable :: soybean(:)
   contains
      procedure, pass :: add_FEMDomain_to_Scene
      procedure, pass :: add_soybean_to_Scene
      generic :: add => add_FEMDomain_to_Scene, add_soybean_to_Scene
      procedure, public :: vtk => vtk_FEMDomain_from_Scene

      procedure, public :: nn => nn_Scene
      procedure, public :: ne => ne_Scene

      procedure, public :: x => x_Scene
      procedure, public :: y => y_Scene
      procedure, public :: z => z_Scene
      procedure, public :: xyz => xyz_Scene

      ! info getter
      procedure, public :: xmin => xmin_Scene
      procedure, public :: xmax => xmax_Scene
      procedure, public :: x_min => xmin_Scene
      procedure, public :: x_max => xmax_Scene

      procedure, public :: ymin => ymin_Scene
      procedure, public :: ymax => ymax_Scene
      procedure, public :: y_min => ymin_Scene
      procedure, public :: y_max => ymax_Scene

      procedure, public :: zmin => zmin_Scene
      procedure, public :: zmax => zmax_Scene
      procedure, public :: z_min => zmin_Scene
      procedure, public :: z_max => zmax_Scene
      ! editor
      procedure, public :: resize => resize_scene
      procedure, public :: rotate => rotate_scene
      procedure, public :: move => move_scene

      procedure, public :: overset => overset_scene
      procedure, public :: getOverlapList => getOverlapList_Scene
      procedure, public :: select_point_ID => select_point_ID_Scene

      procedure, pass   :: full_function_Scene
      procedure, pass   :: full_select_node_in_range_Scene
      generic :: full => full_function_Scene, full_select_node_in_range_Scene

   end type

   interface operator(.contacts.)
      module procedure femdomain_contacts_with_femdomain
   end interface

contains

   subroutine add_FEMDomain_to_Scene(this, femdomain)
      class(Scene_), intent(inout) :: this
      type(FEMDomain_), target, intent(in) :: femdomain(:)
      type(FEMDomainp_), allocatable ::  buf(:)
      integer(int32) :: i, n, old_n

      n = size(femdomain)
      old_n = size(this%femdomain)
      if (.not. allocated(this%femdomain)) then
         allocate (this%femdomain(n))
         do i = 1, n
            this%femdomain(i)%femdomainp => femdomain(i)
         end do
      else
         buf = this%femdomain
         deallocate (this%femdomain)
         allocate (this%femdomain(old_n + n))
         this%femdomain(1:old_n) = buf(1:old_n)
         do i = 1, n
            this%femdomain(i + old_n)%femdomainp => femdomain(i)
         end do
      end if

   end subroutine
! #######################################################
   function nn_Scene(this) result(ret)
      class(Scene_), intent(in) :: this
      integer(int32) :: offset, DomainID, ret

      ret = 0
      do DomainID = 1, size(this%femdomain)
         ret = ret + this%femdomain(DomainID)%femdomainp%nn()
      end do

   end function
! #######################################################
   function ne_Scene(this) result(ret)
      class(Scene_), intent(in) :: this
      integer(int32) :: offset, DomainID, ret

      ret = 0
      do DomainID = 1, size(this%femdomain)
         ret = ret + this%femdomain(DomainID)%femdomainp%ne()
      end do

   end function
! #######################################################
   function x_Scene(this, node_id) result(ret)
      class(Scene_), intent(in) :: this
      real(real64), allocatable :: ret(:)
      integer(int32), optional, intent(in) :: node_id
      integer(int32) :: offset, DomainID

      ret = zeros(this%nn())
      offset = 0.0d0
      do domainID = 1, size(this%femdomain)
         ret(offset + 1:offset + this%femdomain(DomainID)%femdomainp%nn()) &
            = this%femdomain(DomainID)%femdomainp%mesh%nodcoord(:, 1)
         offset = offset + this%femdomain(DomainID)%femdomainp%nn()
      end do

      if (present(node_id)) then
         ret = ret(node_id)
      end if

   end function
! #######################################################
   recursive function y_Scene(this, node_id) result(ret)
      class(Scene_), intent(in) :: this
      real(real64), allocatable :: ret(:)
      integer(int32), optional, intent(in) :: node_id
      integer(int32) :: offset, DomainID

      offset = 0.0d0
      ret = zeros(this%nn())
      do domainID = 1, size(this%femdomain)
         ret(offset + 1:offset + this%femdomain(DomainID)%femdomainp%nn()) &
            = this%femdomain(DomainID)%femdomainp%mesh%nodcoord(:, 2)
         offset = offset + this%femdomain(DomainID)%femdomainp%nn()
      end do
      if (present(node_id)) then
         ret = ret(node_id)
      end if

   end function
! #######################################################
   recursive function z_Scene(this, node_id) result(ret)
      class(Scene_), intent(in) :: this
      real(real64), allocatable :: ret(:)
      integer(int32), optional, intent(in) :: node_id
      integer(int32) :: offset, DomainID
      offset = 0.0d0
      ret = zeros(this%nn())
      do domainID = 1, size(this%femdomain)
         ret(offset + 1:offset + this%femdomain(DomainID)%femdomainp%nn()) &
            = this%femdomain(DomainID)%femdomainp%mesh%nodcoord(:, 3)
         offset = offset + this%femdomain(DomainID)%femdomainp%nn()
      end do
      if (present(node_id)) then
         ret = ret(node_id)
      end if

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

! #######################################################
   function xyz_Scene(this) result(ret)
      class(Scene_), intent(in) :: this
      real(real64), allocatable :: ret(:, :)
      integer(int32) :: offset, DomainID

      ret = this%x() .h.this%y() .h.this%z()

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

   subroutine add_soybean_to_Scene(this, soybean)
      class(Scene_), intent(inout) :: this
      type(soybean_), target, intent(in) :: soybean(:)
      type(soybeanp_), allocatable ::  buf(:)
      integer(int32) :: i, n, old_n

      n = size(soybean)
      old_n = size(this%soybean)
      if (.not. allocated(this%soybean)) then
         allocate (this%soybean(n))
         do i = 1, n
            this%soybean(i)%soybeanp => soybean(i)
         end do
      else
         buf = this%soybean
         deallocate (this%soybean)
         allocate (this%soybean(old_n + n))
         this%soybean(1:old_n) = buf(1:old_n)
         do i = 1, n
            this%soybean(i + old_n)%soybeanp => soybean(i)
         end do
      end if

   end subroutine

! #######################################################

   subroutine vtk_FEMDomain_from_Scene(this, name, scalar)
      class(Scene_), intent(inout) :: this
      character(*), intent(in) :: name
      integer(int32) :: i, from, to
      real(real64), optional, intent(in) :: scalar(:)

      from = 0
      to = 0

      if (present(scalar)) then
         if (allocated(this%femdomain)) then
            do i = 1, size(this%femdomain)
               from = to + 1
               to = from + this%femdomain(i)%femdomainp%nn() - 1
               call this%femdomain(i)%femdomainp%vtk(name + "/femdomain_"+zfill(i, 6), scalar=scalar(from:to))
            end do
         end if
      else
         if (allocated(this%femdomain)) then
            do i = 1, size(this%femdomain)
               call this%femdomain(i)%femdomainp%vtk(name + "/femdomain_"+zfill(i, 6))
            end do
         end if
      end if

      if (allocated(this%soybean)) then
         do i = 1, size(this%soybean)
            call this%soybean(i)%soybeanp%vtk(name + "/soybean_"+zfill(i, 6), single_file=.true.)
         end do
      end if

   end subroutine

! #####################################################
   subroutine resize_scene(this, object, x, y, z, x_rate, y_rate, z_rate)
      class(Scene_), intent(inout) :: this
      integer(int32), intent(in) :: object(:)
      real(real64), optional, intent(in) :: x, y, z, x_rate, y_rate, z_rate
      integer(int32) :: domainID, itr
      ! resize femdomain object
      do itr = 1, size(object)
         domainID = object(itr)
         call this%femdomain(DomainID)%femdomainp%resize(x_len=x, y_len=y, z_len=z, &
                                                         x_rate=x_rate, y_rate=y_rate, z_rate=z_rate)
      end do
   end subroutine
! #####################################################

! #####################################################
   subroutine move_scene(this, object, x, y, z)
      class(Scene_), intent(inout) :: this
      integer(int32), intent(in) :: object(:)
      real(real64), optional, intent(in) :: x, y, z
      integer(int32) :: domainID, itr
      ! move femdomain object
      do itr = 1, size(object)
         domainID = object(itr)
         call this%femdomain(DomainID)%femdomainp%move(x=x, y=y, z=z)
      end do
   end subroutine
! #####################################################

! #####################################################
   subroutine rotate_scene(this, object, x, y, z)
      class(Scene_), intent(inout) :: this
      integer(int32), intent(in) :: object(:)
      real(real64), optional, intent(in) :: x, y, z
      integer(int32) :: domainID, itr

      ! rotate femdomain object
      do itr = 1, size(object)
         domainID = object(itr)
         call this%femdomain(DomainID)%femdomainp%rotate(x=x, y=y, z=z)
      end do

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

   function getOverlapList_Scene(this) result(Overlap)
      class(Scene_), intent(in) :: this
      integer(int32), allocatable::Overlap(:, :)
      logical, allocatable :: contact_table(:, :)
      integer(int32) :: i, j, n, itr

      n = size(this%femdomain)
      allocate (contact_table(n, n))
      contact_table(:, :) = .false.

      itr = 0
      do i = 1, size(this%femdomain)
         do j = 1, i - 1
            contact_table(i, j) = this%femdomain(i)%femdomainp.contacts.this%femdomain(j)%femdomainp
            if (contact_table(i, j)) then
               itr = itr + 1
            end if
         end do
      end do

      allocate (Overlap(itr, 2))
      itr = 0
      do i = 1, size(this%femdomain)
         do j = 1, i - 1
            if (contact_table(i, j)) then
               itr = itr + 1
               overlap(itr, 1:2) = [i, j]
            end if
         end do
      end do

   end function

! #####################################################
   subroutine overset_scene(this, pairing, x, y, z, algorithm)
      class(Scene_), intent(inout) :: this
      integer(int32), optional, intent(in) :: pairing(1:2)
      real(real64), optional, intent(in) :: x, y, z
      character(*), optional, intent(in) :: algorithm
      integer(int32) :: domainID, itr, i
      integer(int32), allocatable :: pairings(:, :)

      character(:), allocatable :: algol

      if (present(algorithm)) then
         algol = algorithm
      else
         algol = "GPP"
      end if

      if (present(pairing)) then
         ! move femdomain pairing
         call this%femdomain(pairing(1))%overset(femdomainp=this%femdomain, to=pairing(2), by=algol)
         call this%femdomain(pairing(2))%overset(femdomainp=this%femdomain, to=pairing(1), by=algol)
      else
         pairings = this%getOverlapList()
         do i = 1, size(pairings, 1)
            call this%femdomain(pairings(i, 1))%overset(femdomainp=this%femdomain, to=pairings(i, 2), by=algol)
            call this%femdomain(pairings(i, 2))%overset(femdomainp=this%femdomain, to=pairings(i, 1), by=algol)
         end do
      end if

   end subroutine
! #####################################################
   function full_select_node_in_range_Scene(this, domainIDs, values, x_min, x_max, y_min, y_max, z_min, z_max) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), intent(in) :: DomainIDs(:)
      real(real64), intent(in) :: values
      real(real64), optional, intent(in) :: x_min, x_max, y_min, y_max, z_min, z_max
      integer(int32) :: itr, DomainID, offset
      integer(int32), allocatable :: num_node_per_domain(:)
      real(real64), allocatable :: ret(:)

      allocate (ret(0))
      allocate (num_node_per_domain(0:size(this%femdomain)))
      num_node_per_domain(0) = 0
      do itr = 1, size(this%femdomain)
         num_node_per_domain(itr) = this%femdomain(itr)%femdomainp%nn()
      end do

      do itr = 1, size(domainIDs)
         domainID = domainIDs(itr)
         ret = ret//values*eyes(size(this%femdomain(domainID)%femdomainp%select( &
                                     x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, z_min=z_min, z_max=z_max)))
      end do

   end function

! #####################################################

   function select_point_ID_Scene(this, domainIDs, x_min, x_max, y_min, y_max, z_min, z_max) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), intent(in) :: DomainIDs(:)
      real(real64), optional, intent(in) :: x_min, x_max, y_min, y_max, z_min, z_max
      integer(int32) :: itr, DomainID, offset
      integer(int32), allocatable :: ret(:), num_node_per_domain(:)

      allocate (ret(0))
      allocate (num_node_per_domain(0:size(this%femdomain)))
      num_node_per_domain(0) = 0
      do itr = 1, size(this%femdomain)
         num_node_per_domain(itr) = this%femdomain(itr)%femdomainp%nn()
      end do

      do itr = 1, size(domainIDs)
         domainID = domainIDs(itr)
         offset = sum(num_node_per_domain(0:domainID - 1))
         ret = ret//this%femdomain(domainID)%femdomainp%select( &
               x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, z_min=z_min, z_max=z_max) &
               + offset
      end do

   end function

! #####################################################

   function xmax_Scene(this, domainIDs) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), optional, intent(in) :: DomainIDs(:)
      real(real64), allocatable :: ret
      integer(int32) :: i
      real(real64), allocatable :: maxvalues(:)

      if (present(domainIDs)) then
         maxvalues = zeros(size(domainIDs))
         do i = 1, size(domainIDs)
            maxvalues(i) = this%femdomain(domainIDs(i))%femdomainp%xmax()
         end do
      else
         maxvalues = zeros(size(this%femdomain))
         do i = 1, size(this%femdomain)
            maxvalues(i) = this%femdomain(i)%femdomainp%xmax()
         end do
      end if
      ret = maxval(maxvalues)

   end function

! #####################################################

   function xmin_Scene(this, domainIDs) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), optional, intent(in) :: DomainIDs(:)
      real(real64), allocatable :: ret
      integer(int32) :: i
      real(real64), allocatable :: minvalues(:)

      if (present(domainIDs)) then
         minvalues = zeros(size(domainIDs))
         do i = 1, size(domainIDs)
            minvalues(i) = this%femdomain(domainIDs(i))%femdomainp%xmin()
         end do
      else
         minvalues = zeros(size(this%femdomain))
         do i = 1, size(this%femdomain)
            minvalues(i) = this%femdomain(i)%femdomainp%xmin()
         end do
      end if
      ret = minval(minvalues)

   end function

! #####################################################

   function ymax_Scene(this, domainIDs) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), optional, intent(in) :: DomainIDs(:)
      real(real64), allocatable :: ret
      integer(int32) :: i
      real(real64), allocatable :: maxvalues(:)

      if (present(domainIDs)) then
         maxvalues = zeros(size(domainIDs))
         do i = 1, size(domainIDs)
            maxvalues(i) = this%femdomain(domainIDs(i))%femdomainp%ymax()
         end do
      else
         maxvalues = zeros(size(this%femdomain))
         do i = 1, size(this%femdomain)
            maxvalues(i) = this%femdomain(i)%femdomainp%ymax()
         end do
      end if
      ret = maxval(maxvalues)

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

! #####################################################
   function ymin_Scene(this, domainIDs) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), optional, intent(in) :: DomainIDs(:)
      real(real64), allocatable :: ret
      integer(int32) :: i
      real(real64), allocatable :: minvalues(:)

      if (present(domainIDs)) then
         minvalues = zeros(size(domainIDs))
         do i = 1, size(domainIDs)
            minvalues(i) = this%femdomain(domainIDs(i))%femdomainp%ymin()
         end do
      else
         minvalues = zeros(size(this%femdomain))
         do i = 1, size(this%femdomain)
            minvalues(i) = this%femdomain(i)%femdomainp%ymin()
         end do
      end if
      ret = minval(minvalues)

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

   function zmax_Scene(this, domainIDs) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), optional, intent(in) :: DomainIDs(:)
      real(real64), allocatable :: ret
      integer(int32) :: i
      real(real64), allocatable :: maxvalues(:)

      if (present(domainIDs)) then
         maxvalues = zeros(size(domainIDs))
         do i = 1, size(domainIDs)
            maxvalues(i) = this%femdomain(domainIDs(i))%femdomainp%zmax()
         end do
      else
         maxvalues = zeros(size(this%femdomain))
         do i = 1, size(this%femdomain)
            maxvalues(i) = this%femdomain(i)%femdomainp%zmax()
         end do
      end if
      ret = maxval(maxvalues)

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

! #####################################################
   function zmin_Scene(this, domainIDs) result(ret)
      class(Scene_), intent(inout) :: this
      integer(int32), optional, intent(in) :: DomainIDs(:)
      real(real64), allocatable :: ret
      integer(int32) :: i
      real(real64), allocatable :: minvalues(:)

      if (present(domainIDs)) then
         minvalues = zeros(size(domainIDs))
         do i = 1, size(domainIDs)
            minvalues(i) = this%femdomain(domainIDs(i))%femdomainp%zmin()
         end do
      else
         minvalues = zeros(size(this%femdomain))
         do i = 1, size(this%femdomain)
            minvalues(i) = this%femdomain(i)%femdomainp%zmin()
         end do
      end if
      ret = minval(minvalues)

   end function
! ###############################################
   function full_function_Scene(this, func, params) result(ret)
      class(Scene_), intent(in) :: this
      interface
         function func(x, params) result(ret)
            use iso_fortran_env
            implicit none
            real(real64), intent(in) :: x(:)
            real(real64), optional, intent(in) :: params(:)
            real(real64) :: ret

         end function
      end interface
      real(real64), optional, intent(in) :: params(:)
      real(real64), allocatable :: ret(:)
      integer(int32) :: domainID

      do DomainID = 1, size(this%femdomain)
         if (DomainID == 1) then
            ret = this%femdomain(DomainID)%femdomainp%full(func=func, params=params)
         else
            ret = ret//this%femdomain(DomainID)%femdomainp%full(func=func, params=params)
         end if
      end do
   end function
! #########################################################

! #########################################################
   function femdomain_contacts_with_femdomain(obj1, obj2) result(ret)
      type(FEMDomain_), intent(in) :: obj1, obj2
      real(real64) :: xyz_ranges(3, 2), center1(3), center2(3)
      logical :: ret

      ret = .false.

      ! Bounding Box Algorithm
      if (obj1%xmax() < obj2%xmin()) return
      if (obj1%ymax() < obj2%ymin()) return
      if (obj1%zmax() < obj2%zmin()) return

      if (obj2%xmax() < obj1%xmin()) return
      if (obj2%ymax() < obj1%ymin()) return
      if (obj2%zmax() < obj1%zmin()) return

      center1 = obj1%CenterPosition()
      center2 = obj2%CenterPosition()
      if (center1(1) < center2(1)) then
         xyz_ranges(1, 1:2) = [obj2%xmin(), obj1%xmax()] ! x
      else
         xyz_ranges(1, 1:2) = [obj1%xmin(), obj2%xmax()] ! x
      end if

      if (center1(2) < center2(2)) then
         xyz_ranges(2, 1:2) = [obj2%ymin(), obj1%ymax()] ! y
      else
         xyz_ranges(2, 1:2) = [obj1%ymin(), obj2%ymax()] ! y
      end if

      if (center1(3) < center2(3)) then
         xyz_ranges(3, 1:2) = [obj2%zmin(), obj1%zmax()] ! z
      else
         xyz_ranges(3, 1:2) = [obj1%zmin(), obj2%zmax()] ! z
      end if

      if (size(obj1%select( &
               x_min=xyz_ranges(1, 1), &
               x_max=xyz_ranges(1, 2), &
               y_min=xyz_ranges(2, 1), &
               y_max=xyz_ranges(2, 2), &
               z_min=xyz_ranges(3, 1), &
               z_max=xyz_ranges(3, 2) &
               )) == 0) return

      if (size(obj2%select( &
               x_min=xyz_ranges(1, 1), &
               x_max=xyz_ranges(1, 2), &
               y_min=xyz_ranges(2, 1), &
               y_max=xyz_ranges(2, 2), &
               z_min=xyz_ranges(3, 1), &
               z_max=xyz_ranges(3, 2) &
               )) == 0) return

      ret = .true.

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

end module

