module MaterialPropClass
   use, intrinsic :: iso_fortran_env
   use MeshClass
   implicit none

   type :: MaterialProp_
      type(Mesh_) :: Mesh
      real(real64), allocatable:: meshPara(:, :)

      real(real64) ::x_max, x_min, y_max, y_min, z_max, z_min, t_max, t_min
      integer(int32) :: Mcount

      integer(int32) :: layer
      character(:), allocatable :: Name

      real(real64), allocatable::MatPara(:, :)
      integer(int32) :: NumOfMatPara
      integer(int32) :: NumOfMaterial

      character(:), allocatable :: MaterialType

      character(:), allocatable :: ErrorMsg
   contains
      procedure :: Init => initializeMaterial
      procedure :: export => exportMaterialProp
      procedure :: save => saveMaterialProp
      procedure :: Delete => DeallocateMaterialProp
      procedure :: create => createMaterialProp
      procedure :: set => setMaterialProp
      procedure :: gmsh => gmshMaterialProp
      procedure :: show => showMaterialProp
      procedure :: getValues => getValuesMaterialProp
      procedure :: remove => removeMaterialProp
      procedure :: open => openMaterialProp

   end type MaterialProp_

contains

! ###########################################################################
   subroutine openMaterialProp(obj, path, name)
      class(MaterialProp_), intent(inout)::obj
      character(*), intent(in) :: path
      character(*), optional, intent(in) :: name
      type(IO_) :: f

      if (present(name)) then
         call f%open(path//"/"//name//"/", "Material", ".prop")
         call obj%Mesh%open(path=path//"/"//name, name="Mesh")
         call openArray(f%fh, obj%meshPara)
         read (f%fh, *) obj%x_max
         read (f%fh, *) obj%x_min
         read (f%fh, *) obj%y_max
         read (f%fh, *) obj%y_min
         read (f%fh, *) obj%z_max
         read (f%fh, *) obj%z_min
         read (f%fh, *) obj%t_max
         read (f%fh, *) obj%t_min
         read (f%fh, *) obj%Mcount
         read (f%fh, *) obj%layer
         read (f%fh, '(A)') obj%Name
         call openArray(f%fh, obj%MatPara)
         read (f%fh, *) obj%NumOfMatPara
         read (f%fh, *) obj%NumOfMaterial
         read (f%fh, '(A)') obj%MaterialType
         read (f%fh, '(A)') obj%ErrorMsg
         call f%close()
      else
         call execute_command_line("mkdir -p "//path//"/Material")
         call f%open(path//"/Material/", "Material", ".prop")
         call obj%Mesh%open(path=path//"/"//"Material", name="Mesh")
         call openArray(f%fh, obj%meshPara)
         read (f%fh, *) obj%x_max
         read (f%fh, *) obj%x_min
         read (f%fh, *) obj%y_max
         read (f%fh, *) obj%y_min
         read (f%fh, *) obj%z_max
         read (f%fh, *) obj%z_min
         read (f%fh, *) obj%t_max
         read (f%fh, *) obj%t_min
         read (f%fh, *) obj%Mcount
         read (f%fh, *) obj%layer
         read (f%fh, '(A)') obj%Name
         call openArray(f%fh, obj%MatPara)
         read (f%fh, *) obj%NumOfMatPara
         read (f%fh, *) obj%NumOfMaterial
         read (f%fh, '(A)') obj%MaterialType
         read (f%fh, '(A)') obj%ErrorMsg
         call f%close()
      end if

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

! ###########################################################################
   subroutine removeMaterialProp(obj)
      class(MaterialProp_), intent(inout) :: obj

      call obj%Mesh%remove()
      if (allocated(obj%meshPara)) then
         deallocate (obj%meshPara)
      end if

      obj%x_max = 0.0d0
      obj%x_min = 0.0d0
      obj%y_max = 0.0d0
      obj%y_min = 0.0d0
      obj%z_max = 0.0d0
      obj%z_min = 0.0d0
      obj%t_max = 0.0d0
      obj%t_min = 0.0d0
      obj%Mcount = 1
      obj%layer = 1
      obj%Name = " "

      if (allocated(obj%MatPara)) then
         deallocate (obj%MatPara)
      end if

      obj%NumOfMatPara = 1
      obj%NumOfMaterial = 1

      obj%MaterialType = " "
      obj%ErrorMsg = " "

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

! ###########################################################################
   subroutine createMaterialProp(obj, Name, x_max, x_min, y_max, y_min, z_max, z_min, t_max, t_min, &
                                 ParaValue, Layer)
      class(MaterialProp_), intent(inout) :: obj
      character(*), optional, intent(in) :: Name
      real(real64), optional, intent(in) :: x_max, x_min, y_max, y_min, z_max, z_min, t_max, t_min
      real(real64), optional, intent(in) :: ParaValue
      integer(int32), optional, intent(in) :: Layer
      if (present(Name)) then
         obj%Name = name
      else
         obj%Name = "NoName"
      end if
      call obj%set(x_max=x_max, x_min=x_min, y_max=y_max, y_min=y_min, z_max=z_max, &
                   z_min=z_min, t_max=t_max, t_min=t_min, ParaValue=ParaValue, Layer=Layer)

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

! ###########################################################################
   subroutine saveMaterialProp(obj, path, name)
      class(MaterialProp_), intent(inout)::obj
      character(*), intent(in) :: path
      character(*), optional, intent(in) :: name
      type(IO_) :: f

      if (present(name)) then
         call execute_command_line("mkdir -p "//path//"/"//name)
         call f%open(path//"/"//name//"/", "Material", ".prop")
         call obj%Mesh%save(path=path//"/"//name, name="Mesh")
         call writeArray(f%fh, obj%meshPara)
         write (f%fh, *) obj%x_max
         write (f%fh, *) obj%x_min
         write (f%fh, *) obj%y_max
         write (f%fh, *) obj%y_min
         write (f%fh, *) obj%z_max
         write (f%fh, *) obj%z_min
         write (f%fh, *) obj%t_max
         write (f%fh, *) obj%t_min
         write (f%fh, *) obj%Mcount
         write (f%fh, *) obj%layer
         write (f%fh, '(A)') obj%Name
         call writeArray(f%fh, obj%MatPara)
         write (f%fh, *) obj%NumOfMatPara
         write (f%fh, *) obj%NumOfMaterial
         write (f%fh, '(A)') obj%MaterialType
         write (f%fh, '(A)') obj%ErrorMsg
         call f%close()
      else
         call execute_command_line("mkdir -p "//path//"/Material")
         call f%open(path//"/Material/", "Material", ".prop")
         call obj%Mesh%save(path=path//"/"//"Material", name="Mesh")
         call writeArray(f%fh, obj%meshPara)
         write (f%fh, *) obj%x_max
         write (f%fh, *) obj%x_min
         write (f%fh, *) obj%y_max
         write (f%fh, *) obj%y_min
         write (f%fh, *) obj%z_max
         write (f%fh, *) obj%z_min
         write (f%fh, *) obj%t_max
         write (f%fh, *) obj%t_min
         write (f%fh, *) obj%Mcount
         write (f%fh, *) obj%layer
         write (f%fh, '(A)') obj%Name
         call writeArray(f%fh, obj%MatPara)
         write (f%fh, *) obj%NumOfMatPara
         write (f%fh, *) obj%NumOfMaterial
         write (f%fh, '(A)') obj%MaterialType
         write (f%fh, '(A)') obj%ErrorMsg
         call f%close()
      end if

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

! ###########################################################################
   subroutine exportMaterialProp(obj, restart, path)
      class(MaterialProp_), intent(inout)::obj
      logical, optional, intent(in) :: restart
      character(*), intent(in) :: path
      type(IO_) :: f

      if (present(restart)) then
         call execute_command_line("mkdir -p "//path//"/Material")
         call f%open(path//"/Material/", "Material", ".prop")
         call obj%Mesh%export(path=path//"/Material", restart=.true.)
         write (f%fh, *) obj%meshPara(:, :)
         write (f%fh, *) obj%x_max
         write (f%fh, *) obj%x_min
         write (f%fh, *) obj%y_max
         write (f%fh, *) obj%y_min
         write (f%fh, *) obj%z_max
         write (f%fh, *) obj%z_min
         write (f%fh, *) obj%t_max
         write (f%fh, *) obj%t_min
         write (f%fh, *) obj%Mcount
         write (f%fh, *) obj%layer
         write (f%fh, '(A)') obj%Name
         write (f%fh, *) obj%MatPara(:, :)
         write (f%fh, *) obj%NumOfMatPara
         write (f%fh, *) obj%NumOfMaterial
         write (f%fh, '(A)') obj%MaterialType
         write (f%fh, '(A)') obj%ErrorMsg
         call f%close()
      end if

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

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

      n = size(obj%Mesh%ElemNod, 1)

      print *, obj%Name
      do i = 1, n
         if (i == n) then
            print *, "  L _ Zone #", i, "Parameters : ", obj%meshPara(i, :)
         else
            print *, "  | - Zone #", i, "Parameters : ", obj%meshPara(i, :)
         end if
      end do

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

! ###########################################################################
   subroutine setMaterialProp(obj, x_max, x_min, y_max, y_min, z_max, z_min, t_max, t_min, ParaValue, Layer)
      class(MaterialProp_), intent(inout) :: obj
      real(real64), optional, intent(in) :: x_max, x_min, y_max, y_min, z_max, z_min, t_max, t_min
      real(real64), optional, intent(in) :: ParaValue
      integer(int32) :: i, j, n, m, valsize
      integer(int32), optional, intent(in) :: Layer

      obj%Layer = input(default=1, option=Layer)
      valsize = 1
      if (.not. allocated(obj%mesh%NodCoord)) then
         allocate (obj%mesh%NodCoord(8, 3))
         allocate (obj%mesh%ElemNod(1, 8))
         allocate (obj%meshPara(1, valsize))
         obj%Mcount = 1
      end if
      obj%x_max = input(default=10000000.0d0, option=x_max)
      obj%x_min = input(default=-10000000.0d0, option=x_min)
      obj%y_max = input(default=10000000.0d0, option=y_max)
      obj%y_min = input(default=-10000000.0d0, option=y_min)
      obj%z_max = input(default=10000000.0d0, option=z_max)
      obj%z_min = input(default=-10000000.0d0, option=z_min)
      obj%t_max = input(default=10000000.0d0, option=t_max)
      obj%t_min = input(default=0.0d0, option=t_min)

      if (obj%Mcount == 1) then
         obj%mesh%NodCoord(1, 1) = obj%x_min
         obj%mesh%NodCoord(1, 2) = obj%y_min
         obj%mesh%NodCoord(1, 3) = obj%z_min

         obj%mesh%NodCoord(2, 1) = obj%x_max
         obj%mesh%NodCoord(2, 2) = obj%y_min
         obj%mesh%NodCoord(2, 3) = obj%z_min

         obj%mesh%NodCoord(3, 1) = obj%x_max
         obj%mesh%NodCoord(3, 2) = obj%y_max
         obj%mesh%NodCoord(3, 3) = obj%z_min

         obj%mesh%NodCoord(4, 1) = obj%x_min
         obj%mesh%NodCoord(4, 2) = obj%y_max
         obj%mesh%NodCoord(4, 3) = obj%z_min

         obj%mesh%NodCoord(5, 1) = obj%x_min
         obj%mesh%NodCoord(5, 2) = obj%y_min
         obj%mesh%NodCoord(5, 3) = obj%z_max

         obj%mesh%NodCoord(6, 1) = obj%x_max
         obj%mesh%NodCoord(6, 2) = obj%y_min
         obj%mesh%NodCoord(6, 3) = obj%z_max

         obj%mesh%NodCoord(7, 1) = obj%x_max
         obj%mesh%NodCoord(7, 2) = obj%y_max
         obj%mesh%NodCoord(7, 3) = obj%z_max

         obj%mesh%NodCoord(8, 1) = obj%x_min
         obj%mesh%NodCoord(8, 2) = obj%y_max
         obj%mesh%NodCoord(8, 3) = obj%z_max

         do i = 1, 8
            obj%mesh%ElemNod(1, i) = i
         end do
         obj%meshPara(1, 1) = input(default=0.0d0, option=ParaValue)
         !if(present(Values) )then
         !    obj%meshPara(1,:)=values
         !endif
         obj%Mcount = obj%Mcount + 1
      else
         n = size(obj%mesh%ElemNod, 1)
         do i = 1, 8
            call extendArray(mat=obj%mesh%NodCoord, extend1stColumn=.true.)
         end do
         call extendArray(mat=obj%meshPara, extend1stColumn=.true.)
         call extendArray(mat=obj%mesh%ElemNod, extend1stColumn=.true.)
         obj%Mcount = obj%Mcount + 1
         obj%mesh%NodCoord(n*8 + 1, 1) = obj%x_min
         obj%mesh%NodCoord(n*8 + 1, 2) = obj%y_min
         obj%mesh%NodCoord(n*8 + 1, 3) = obj%z_min

         obj%mesh%NodCoord(n*8 + 2, 1) = obj%x_max
         obj%mesh%NodCoord(n*8 + 2, 2) = obj%y_min
         obj%mesh%NodCoord(n*8 + 2, 3) = obj%z_min

         obj%mesh%NodCoord(n*8 + 3, 1) = obj%x_max
         obj%mesh%NodCoord(n*8 + 3, 2) = obj%y_max
         obj%mesh%NodCoord(n*8 + 3, 3) = obj%z_min

         obj%mesh%NodCoord(n*8 + 4, 1) = obj%x_min
         obj%mesh%NodCoord(n*8 + 4, 2) = obj%y_max
         obj%mesh%NodCoord(n*8 + 4, 3) = obj%z_min

         obj%mesh%NodCoord(n*8 + 5, 1) = obj%x_min
         obj%mesh%NodCoord(n*8 + 5, 2) = obj%y_min
         obj%mesh%NodCoord(n*8 + 5, 3) = obj%z_max

         obj%mesh%NodCoord(n*8 + 6, 1) = obj%x_max
         obj%mesh%NodCoord(n*8 + 6, 2) = obj%y_min
         obj%mesh%NodCoord(n*8 + 6, 3) = obj%z_max

         obj%mesh%NodCoord(n*8 + 7, 1) = obj%x_max
         obj%mesh%NodCoord(n*8 + 7, 2) = obj%y_max
         obj%mesh%NodCoord(n*8 + 7, 3) = obj%z_max

         obj%mesh%NodCoord(n*8 + 8, 1) = obj%x_min
         obj%mesh%NodCoord(n*8 + 8, 2) = obj%y_max
         obj%mesh%NodCoord(n*8 + 8, 3) = obj%z_max

         obj%meshPara(1 + n, 1) = input(default=0.0d0, option=ParaValue)
         do i = 1, 8
            obj%mesh%ElemNod(1 + n, i) = i + n*8
         end do
         !if(present(Values) )then
         !    obj%meshPara(1+n,:)=values
         !endif

      end if

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

!###############################################
   subroutine gmshMaterialProp(obj, Name, Tag)
      class(MaterialProp_), intent(inout) :: obj
      character(*), optional, intent(in) :: Name, Tag
      real(real64), allocatable :: ElemValue(:, :)
      integer(int32) :: i

      if (obj%Mesh%empty() .eqv. .true.) then
         print *, "ERROR gmshMaterialProp :: No Material. is installed. "
         return
      end if
      allocate (ElemValue(size(obj%MeshPara, 1), 1))
      do i = 1, size(obj%MeshPara, 2)
         ElemValue(:, 1) = obj%MeshPara(:, i)
         if (present(tag)) then
            call obj%Mesh%gmsh(Name=Name//"Material", ElemValue=ElemValue, &
                               OptionalContorName=tag)
         else
            call obj%Mesh%gmsh(Name=Name//"Material", ElemValue=ElemValue, &
                               OptionalContorName="Material Value :: ID = "//adjustl(fstring(i)))
         end if
      end do
      deallocate (ElemValue)

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

!##################################################
   subroutine DeallocateMaterialProp(obj)
      class(MaterialProp_), intent(inout)::obj

      if (allocated(obj%MatPara)) then
         deallocate (obj%MatPara)
      end if
      obj%ErrorMsg = "All objectes in MaterialPropClass are deallocated."

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

!##################################################
   subroutine initializeMaterial(obj, MaterialParameters)
      class(MaterialProp_), intent(inout)::obj
      real(real64), allocatable, optional, intent(inout) :: MaterialParameters(:, :)
      if (.not. allocated(obj%MatPara)) then
         obj%ErrorMsg = "ERROR :: MaterialPropClass%Initialize >>.not.allocated(MatPara)"
         print *, "ERROR :: MaterialPropClass%Initialize >>.not.allocated(MatPara)"
      end if
      if (present(MaterialParameters)) then
         if (.not. allocated(MaterialParameters)) then
            print *, "ERROR :: initializeMaterial >> MaterialParameters(:,:) not allocated."
            return
         else
            if (allocated(obj%MatPara)) then
               deallocate (obj%MatPara)
            end if
            obj%NumOfMatPara = size(MaterialParameters, 1)
            obj%NumOfMaterial = size(MaterialParameters, 2)
            allocate (obj%MatPara(obj%NumOfMatPara, obj%NumOfMaterial))
            obj%MatPara(:, :) = MaterialParameters(:, :)
         end if
         return
      end if

      if (.not. allocated(obj%MatPara)) then
         print *, "ERROR :: initializeMaterial >> please import obj%MatPara"
      end if
      obj%NumOfMatPara = size(obj%MatPara, 1)
      obj%NumOfMaterial = size(obj%MatPara, 2)
   end subroutine initializeMaterial
!##################################################

!##################################################
   subroutine ImportMatPara(obj, mat_para)
      class(MaterialProp_), intent(inout)::obj
      real(real64), intent(in)::mat_para(:, :)

      if (allocated(obj%MatPara)) then
         deallocate (obj%MatPara)
      end if
      allocate (obj%MatPara(size(mat_para, 1), size(mat_para, 2)))
      obj%MatPara(:, :) = mat_para(:, :)

      obj%NumOfMatPara = size(mat_para, 1)
      obj%NumOfMaterial = size(mat_para, 2)

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

!##################################################
   subroutine MergeMaterialProp(inobj1, inobj2, outobj)
      class(MaterialProp_), intent(in)::inobj1, inobj2
      class(MaterialProp_), intent(out)::outobj
      integer num1, num2, num3, i

      ! ========= Merge nodes  ============
      num1 = size(inobj1%MatPara, 1)
      num2 = size(inobj2%MatPara, 1)
      num3 = size(inobj2%MatPara, 2)
      if (num3 /= size(inobj1%MatPara, 1)) then
         outobj%ErrorMsg = "MergeMaterialProp >> num3 /= inobj1%MatPara,1"
      end if
      allocate (outobj%MatPara(num1 + num2, num3))
      do i = 1, num1
         outobj%MatPara(i, :) = inobj1%MatPara(i, :)
      end do
      do i = 1, num2
         outobj%MatPara(i + num1, :) = inobj2%MatPara(i, :)
      end do
      outobj%NumOfMatPara = inobj1%NumOfMatPara
      outobj%NumOfMaterial = inobj1%NumOfMaterial + inobj1%NumOfMaterial
      ! ========= Merge nodes  ============

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

!##################################################
   subroutine ShowMatPara(obj)
      class(MaterialProp_), intent(in)::obj

      integer i

      do i = 1, size(obj%MatPara, 1)
         print *, "obj%MatPara : ", obj%MatPara(i, :)
      end do

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

!##################################################
   subroutine getValuesMaterialProp(obj, mesh, Values)
      class(MaterialProp_), intent(in):: obj
      type(Mesh_), intent(in) :: mesh
      type(Rectangle_) ::rect, mrect
      real(real64), allocatable, intent(inout) :: Values(:)
      integer(int32) :: i, j, k, l, n, dim_num, elem_num

      n = size(mesh%NodCoord, 1)
      dim_num = size(mesh%NodCoord, 2)
      elem_num = size(mesh%ElemNod, 1)
      if (.not. allocated(Values)) then
         allocate (Values(elem_num))
      elseif (size(Values) /= elem_num) then
         print *, "ERROR :: getValuesMaterialProp >> size(Values) /= elem_num"
         print *, size(Values), elem_num
         deallocate (Values)
         allocate (Values(elem_num))
         Values(:) = 0.0d0
         print *, size(Values), elem_num
      end if

      allocate (rect%NodCoord(size(obj%Mesh%ElemNod, 2), size(obj%Mesh%NodCoord, 2)))
      allocate (mrect%NodCoord(size(obj%Mesh%ElemNod, 2), size(obj%Mesh%NodCoord, 2)))

      do i = 1, size(Mesh%ElemNod, 1)
         do j = 1, size(Mesh%ElemNod, 2)
            rect%NodCoord(j, :) = Mesh%NodCoord(Mesh%ElemNod(i, j), :)
         end do
         ! for all materials, check material parameters
         ! for each zones, check in-out
         ! import nodal coordinate
         do k = 1, size(obj%Mesh%ElemNod, 1)
            do l = 1, size(obj%Mesh%ElemNod, 2)
               n = obj%Mesh%ElemNod(k, l)
               mrect%NodCoord(l, :) = obj%Mesh%NodCoord(n, :)
            end do
            ! check in-out
            if (rect%contact(mrect) .eqv. .true.) then
               ! in
               Values(i) = obj%meshPara(k, 1)
            else
               cycle
            end if
         end do
      end do

   end subroutine getValuesMaterialProp
!##################################################
end module MaterialPropClass
