submodule(diffstruc__operations_broadcast) diffstruc__operations_broadcast_sub
  !! Submodule containing implementations of broadcast operations
  use coreutils, only: stop_program


contains

!###############################################################################
  module function concat_arrays(a, b, dim) result(c)
    !! Concatenate two autodiff arrays along the first dimension
    implicit none
    class(array_type), intent(in), target :: a, b
    integer, intent(in), optional :: dim
    type(array_type), pointer :: c

    integer :: i, s
    integer :: dim_
    integer, dimension(:), allocatable :: input_shape

    if(present(dim)) then
       dim_ = dim
    else
       dim_ = 1
    end if

    ! concatenate 1D array by using shape to swap dimensions
    if(dim_.eq.1)then
       if(size(a%val,2).ne.size(b%val,2)) then
          call stop_program( &
               "concat: arrays must have the same size in non-concat dimension" &
          )
          return
       elseif(size(a%shape).ne.size(b%shape)) then
          call stop_program( &
               "concat: arrays must have the same number of dimensions" &
          )
          return
       end if
       input_shape = a%shape
       input_shape(size(input_shape)) = &
            a%shape(size(input_shape)) + b%shape(size(input_shape))
       c => a%create_result(array_shape = &
            [input_shape, size(a%val,2)])
       c%val = 0._real32
       do concurrent(s=1:size(a%val,2))
          do concurrent(i=1:size(a%val,1))
             c%val(i, s) = a%val(i, s)
          end do
          do concurrent(i=1:size(b%val,1))
             c%val( size(a%val,1) + i, s) = b%val( i, s)
          end do
       end do
    elseif(dim_.eq.2)then
       if(size(a%val,1).ne.size(b%val,1)) then
          call stop_program( &
               "concat: arrays must have the same size in non-concat dimension" &
          )
          return
       end if
       c => a%create_result(array_shape = &
            [a%shape, size(a%val,2) + size(b%val,2)])
       c%val = 0._real32
       do concurrent(s=1:size(a%val,1))
          do concurrent(i=1:size(a%val,2))
             c%val(s, i) = a%val(s, i)
          end do
          do concurrent(i=1:size(b%val,2))
             c%val( s, size(a%val,2) + i) = b%val( s, i)
          end do
       end do
    else
       call stop_program("concat: only 1 or 2 dimensions are supported")
       return
    end if
    c%indices = [ dim_ ]

    c%get_partial_left => get_partial_concat_left
    c%get_partial_right => get_partial_concat_right
    c%get_partial_left_val => get_partial_concat_left_val
    c%get_partial_right_val => get_partial_concat_right_val
    if(a%requires_grad .or. b%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward .or. b%is_forward
       c%operation = 'concat'
       c%left_operand => a
       c%right_operand => b
       c%owns_left_operand = a%is_temporary
       c%owns_right_operand = b%is_temporary
    end if
  end function concat_arrays
!-------------------------------------------------------------------------------
  function get_partial_concat_left(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output

    output = slice_left(upstream_grad, this%left_operand%shape(1), this%indices(1))

  end function get_partial_concat_left
!-------------------------------------------------------------------------------
  function get_partial_concat_right(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output
    type(array_type), pointer :: ptr

    ptr => slice_right(upstream_grad, this%right_operand%shape(1), this%indices(1))
    call output%assign_and_deallocate_source(ptr)
  end function get_partial_concat_right
!-------------------------------------------------------------------------------
  pure subroutine get_partial_concat_left_val(this, upstream_grad, output)
    implicit none
    class(array_type), intent(in) :: this
    real(real32), dimension(:,:), intent(in) :: upstream_grad
    real(real32), dimension(:,:), intent(out) :: output

    if(this%indices(1).eq.1)then
       output = upstream_grad(1:size(this%left_operand%val,1), :)
    else
       output = upstream_grad(:, 1:size(this%left_operand%val,2))
    end if

  end subroutine get_partial_concat_left_val
!-------------------------------------------------------------------------------
  pure subroutine get_partial_concat_right_val(this, upstream_grad, output)
    implicit none
    class(array_type), intent(in) :: this
    real(real32), dimension(:,:), intent(in) :: upstream_grad
    real(real32), dimension(:,:), intent(out) :: output

    if(this%indices(1).eq.1)then
       output = upstream_grad( &
            size(this%left_operand%val,1)+1:size(upstream_grad,1), : &
       )
    else
       output = upstream_grad(:, &
            size(this%left_operand%val,2)+1:size(upstream_grad,2) &
       )
    end if

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


!###############################################################################
  module function slice_left_array(a, b, dim) result(c)
    !! Left trim an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, intent(in) :: b
    integer, intent(in), optional :: dim
    type(array_type), pointer :: c

    integer :: s
    integer :: dim_

    if(present(dim)) then
       dim_ = dim
    else
       dim_ = 1
    end if

    ! left trim 1D array by using shape to swap dimensions
    if(dim_.eq.1)then
       c => a%create_result(array_shape = [b, size(a%val,2)])
       do concurrent(s=1:size(a%val,2))
          c%val( :, s) = a%val( 1:b, s)
       end do
    else
       c => a%create_result(array_shape = [size(a%val,1), b])
       do concurrent(s=1:size(a%val,1))
          c%val( s, : ) = a%val( s, 1:b)
       end do
    end if
    c%indices = [ dim_, b ]

    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'slice_left'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function slice_left_array
!###############################################################################


!###############################################################################
  module function slice_right_array(a, b, dim) result(c)
    !! Right trim an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, intent(in) :: b
    integer, intent(in), optional :: dim
    type(array_type), pointer :: c

    integer :: s
    integer :: dim_

    if(present(dim)) then
       dim_ = dim
    else
       dim_ = 1
    end if

    ! right trim 1D array by using shape to swap dimensions
    if(dim_.eq.1)then
       c => a%create_result(array_shape = [b, size(a%val,2)])
       do concurrent(s=1:size(a%val,2))
          c%val( :, s) = a%val( size(a%val,1)-b+1:size(a%val,1), s)
       end do
    else
       c => a%create_result(array_shape = [size(a%val,1), b])
       do concurrent(s=1:size(a%val,1))
          c%val( s, : ) = a%val( s, size(a%val,2)-b+1:size(a%val,2))
       end do
    end if
    c%indices = [ dim_, b ]

    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'slice_right'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function slice_right_array
!###############################################################################


!###############################################################################
  module function ltrim_array(a, b, dim) result(c)
    !! Left trim an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, intent(in) :: b
    integer, intent(in), optional :: dim
    type(array_type), pointer :: c

    if(present(dim)) then
       c => slice_right_array(a, size(a%val, dim) - b, dim)
    else
       c => slice_right_array(a, size(a%val, 1) - b, 1)
    end if

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


!###############################################################################
  module function rtrim_array(a, b, dim) result(c)
    !! Right trim an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, intent(in) :: b
    integer, intent(in), optional :: dim
    type(array_type), pointer :: c

    if(present(dim)) then
       c => slice_left_array(a, size(a%val, dim) - b, dim)
    else
       c => slice_left_array(a, size(a%val, 1) - b, 1)
    end if

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


!###############################################################################
  module function index_array(a, indices) result(c)
    !! Index an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, dimension(:), intent(in) :: indices
    type(array_type), pointer :: c

    integer :: i, s

    allocate(c)
    call c%allocate(array_shape=[size(a%val,1), size(indices)])
    do concurrent(s=1:size(indices), i=1:size(a%val,1))
       c%val(i, s) = a%val(i, indices(s))
    end do
    c%indices = indices

    c%get_partial_left => get_partial_index
    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'index'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function index_array
!-------------------------------------------------------------------------------
  function get_partial_index(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output
    type(array_type), pointer :: ptr

    ptr => reverse_index( &
         upstream_grad, indices=this%indices, from=.false., &
         new_index_size=size(this%left_operand%val, 2) &
    )
    call output%assign_and_deallocate_source(ptr)
  end function get_partial_index
!###############################################################################


!###############################################################################
  module function reverse_index_array(a, indices, from, new_index_size) result(c)
    !! Index an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, dimension(:), intent(in) :: indices
    logical, intent(in) :: from
    integer, intent(in) :: new_index_size
    type(array_type), pointer :: c

    integer :: i, s

    allocate(c)
    if(from) then
       call c%allocate(array_shape=[size(a%val,1), new_index_size])
       c%val = 0.0_real32
       do concurrent(s=1:size(indices), i=1:size(a%val,1))
          c%val(i, s) = a%val(i, indices(s))
       end do
    else
       call c%allocate(array_shape=[size(a%val,1), new_index_size])
       c%val = 0.0_real32
       do concurrent(s=1:size(indices), i=1:size(a%val,1))
          c%val(i, indices(s)) = a%val(i, s)
       end do
    end if
    c%indices = indices

    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'index'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function reverse_index_array
!###############################################################################


!###############################################################################
  module function pack_mask_array(a, dim, mask) result(c)
    !! Pack an autodiff array using a logical mask
    implicit none
    class(array_type), intent(in), target :: a
    integer, intent(in) :: dim
    logical, dimension(:), intent(in), optional :: mask
    type(array_type), pointer :: c

    integer :: i, j, s, itmp1

    if(present(mask))then
       itmp1 = count(mask)
       if(dim.eq.1)then
          c => a%create_result(array_shape=[itmp1, size(a%val,2)])
          allocate(c%indices(itmp1))
          i = 0
          do concurrent(s=1:size(a%val,2))
             do j = 1, size(a%val,1)
                if(mask(j)) then
                   i = i + 1
                   c%val(i, s) = a%val(j, s)
                end if
             end do
             i = 0
          end do
          c%indices = pack([(j, j=1,size(mask))], mask)
       elseif(dim.eq.2)then
          c => a%create_result(array_shape=[a%shape, itmp1])
          allocate(c%indices(itmp1))
          i = 0
          do concurrent(s=1:size(a%val,1))
             do j = 1, size(a%val,2)
                if(mask(j)) then
                   i = i + 1
                   c%val(s, i) = a%val(s, j)
                end if
             end do
             i = 0
          end do
          c%indices = pack([(j, j=1,size(mask))], mask)
       else
          call stop_program("pack_mask: only 1 or 2 dimensions are supported")
       end if
    else
       if(dim.eq.1)then
          c => a%create_result(array_shape=[size(a%val,1), size(a%val,2)])
          do concurrent(s=1:size(a%val,2), i=1:size(a%val,1))
             c%val(i, s) = a%val(i, s)
          end do
       elseif(dim.eq.2)then
          c => a%create_result()
          do concurrent(s=1:size(a%val,1), i=1:size(a%val,2))
             c%val(s, i) = a%val(s, i)
          end do
       else
          call stop_program("pack: only 1 or 2 dimensions are supported")
       end if
       !c%indices = [(j, j=1,size(a%val,dim))]
    end if
    allocate(c%adj_ja(size(a%shape)+2,1))
    c%adj_ja(1,1) = dim
    c%adj_ja(2:,1) = [ a%shape, size(a%val,2) ]

    c%get_partial_left => get_partial_pack_mask
    c%get_partial_left_val => get_partial_pack_mask_val
    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'pack_mask'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function pack_mask_array
!-------------------------------------------------------------------------------
  function get_partial_pack_mask(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output
    type(array_type), pointer :: ptr

    if(allocated(this%indices))then
       ptr => unpack(upstream_grad, array_shape=this%adj_ja(2:,1), &
            dim = this%adj_ja(1,1), &
            indices = this%indices &
       )
    else
       ptr => unpack(upstream_grad, array_shape=this%adj_ja(2:,1), &
            dim = this%adj_ja(1,1))
    end if
    call output%assign_and_deallocate_source(ptr)
  end function get_partial_pack_mask
!-------------------------------------------------------------------------------
  pure subroutine get_partial_pack_mask_val(this, upstream_grad, output)
    implicit none
    class(array_type), intent(in) :: this
    real(real32), dimension(:,:), intent(in) :: upstream_grad
    real(real32), dimension(:,:), intent(out) :: output

    integer :: i, j, s, itmp1

    output = 0.0_real32
    if(allocated(this%indices))then
       itmp1 = size(this%indices)
       if(this%adj_ja(1,1).eq.1)then
          do concurrent(s=1:size(upstream_grad,2), j=1:itmp1)
             output(this%indices(j), s) = upstream_grad(j, s)
          end do
       elseif(this%adj_ja(1,1).eq.2)then
          do concurrent(s=1:itmp1, j=1:size(upstream_grad,1))
             output(j, this%indices(s)) = upstream_grad(j, s)
          end do
       end if
    end if
  end subroutine get_partial_pack_mask_val
!###############################################################################


!###############################################################################
  module function unpack_mask_array(a, array_shape, dim, indices) result(c)
    !! Unpack an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, dimension(:), intent(in) :: array_shape
    integer, intent(in), optional :: dim
    integer, dimension(:), intent(in), optional :: indices
    type(array_type), pointer :: c

    integer :: i, s, dim_, num_samples, num_elements

    if(present(dim)) then
       dim_ = dim
    else
       dim_ = 1
    end if
    num_samples = array_shape(size(array_shape))
    num_elements = product(array_shape(1:size(array_shape)-1))

    c => a%create_result(array_shape = array_shape)
    c%val = 0.0_real32
    if(dim_.eq.1)then
       if(present(indices))then
          do concurrent(i=1:size(indices,1), s=1:num_samples)
             c%val(indices(i),s) = a%val(i,s)
          end do
       else
          do concurrent(i=1:num_elements, s=1:num_samples)
             c%val(i,s) = a%val(i,s)
          end do
       end if
    elseif(dim_.eq.2)then
       if(present(indices))then
          do concurrent(i=1:num_elements, s=1:size(indices,1))
             c%val(i,indices(s)) = a%val(i,s)
          end do
       else
          do concurrent(i=1:num_elements, s=1:num_samples)
             c%val(i,s) = a%val(i,s)
          end do
       end if
    else
       call stop_program("unpack_mask: only 1 or 2 dimensions are supported")
    end if

    if(present(indices))then
       c%indices = indices
    end if
    allocate(c%adj_ja(1,1))
    c%adj_ja(1,1) = dim_

    c%get_partial_left => get_partial_unpack_mask
    c%get_partial_right => get_partial_pack_mask
    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'unpack_mask'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function unpack_mask_array
!-------------------------------------------------------------------------------
  function get_partial_unpack_mask(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output
    type(array_type), pointer :: ptr

    logical, dimension(:), allocatable :: mask

    if(allocated(this%indices))then
       allocate(mask(size(upstream_grad%val,this%adj_ja(1,1))))
       mask = .false.
       mask(this%indices) = .true.
       ptr => pack(upstream_grad, this%adj_ja(1,1), mask)
    else
       ptr => pack(upstream_grad, this%adj_ja(1,1))
    end if

    call output%assign_and_deallocate_source(ptr)
  end function get_partial_unpack_mask
!###############################################################################



!###############################################################################
  module function pack_indices_array(a, indices, dim) result(c)
    !! Pack an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, dimension(:), intent(in) :: indices
    integer, intent(in) :: dim
    type(array_type), pointer :: c

    integer :: i, s

    if(dim.eq.1)then
       c => a%create_result(array_shape=[size(indices), size(a%val,2)])
       do concurrent(s=1:size(a%val,2), i=1:size(indices))
          c%val(i, s) = a%val(indices(i), s)
       end do
    elseif(dim.eq.2)then
       c => a%create_result(array_shape=[size(a%val,1), size(indices)])
       do concurrent(s=1:size(indices), i=1:size(a%val,1))
          c%val(i, s) = a%val(i, indices(s))
       end do
    end if
    c%indices = indices
    allocate(c%adj_ja(2,1))
    c%adj_ja(:,1) = [ dim, size(a%val,dim) ]

    c%get_partial_left => get_partial_pack_indices
    c%get_partial_right => get_partial_unpack_indices
    c%get_partial_left_val => get_partial_pack_indices_val
    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'pack_indices'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function pack_indices_array
!-------------------------------------------------------------------------------
  function get_partial_pack_indices(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output
    type(array_type), pointer :: ptr

    ptr => unpack(upstream_grad, this%indices, this%adj_ja(1,1), this%adj_ja(2,1))
    call output%assign_and_deallocate_source(ptr)
  end function get_partial_pack_indices
!-------------------------------------------------------------------------------
  pure subroutine get_partial_pack_indices_val(this, upstream_grad, output)
    implicit none
    class(array_type), intent(in) :: this
    real(real32), dimension(:,:), intent(in) :: upstream_grad
    real(real32), dimension(:,:), intent(out) :: output

    integer :: i, s
    integer :: dim, new_size

    dim = this%adj_ja(1,1)
    new_size = this%adj_ja(2,1)

    output = 0.0_real32
    if(dim.eq.1)then
       do concurrent(i=1:size(this%indices,1), s=1:size(upstream_grad,2))
          output(this%indices(i),s) = upstream_grad(i,s)
       end do
    elseif(dim.eq.2)then
       do concurrent(i=1:size(upstream_grad,1), s=1:new_size)
          output(i,this%indices(s)) = upstream_grad(i,s)
       end do
    end if

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


!###############################################################################
  module function unpack_indices_array(a, indices, dim, new_size) result(c)
    !! Unpack an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, dimension(:), intent(in) :: indices
    integer, intent(in) :: new_size, dim
    type(array_type), pointer :: c

    integer :: i, s


    if(dim.eq.1)then
       c => a%create_result(array_shape = [ new_size, size(a%val,2) ])
       c%val = 0.0_real32
       do concurrent(i=1:size(indices,1), s=1:size(a%val,2))
          c%val(indices(i),s) = a%val(i,s)
       end do
    elseif(dim.eq.2)then
       c => a%create_result(array_shape = [ size(a%val,1), new_size ])
       c%val = 0.0_real32
       do concurrent(i=1:size(a%val,1), s=1:new_size)
          c%val(i,indices(s)) = a%val(i,s)
       end do
    end if
    c%indices = indices
    allocate(c%adj_ja(2,1))
    c%adj_ja(:,1) = [ dim, new_size ]

    c%get_partial_left => get_partial_unpack_indices
    c%get_partial_right => get_partial_pack_indices
    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'unpack_indices'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function unpack_indices_array
!-------------------------------------------------------------------------------
  function get_partial_unpack_indices(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output
    type(array_type), pointer :: ptr

    ptr => pack(upstream_grad, this%indices, this%adj_ja(1,1))
    call output%assign_and_deallocate_source(ptr)
  end function get_partial_unpack_indices
!###############################################################################


!###############################################################################
  module function reshape_array(a, new_shape) result(c)
    !! Reshape an autodiff array
    implicit none
    class(array_type), intent(in), target :: a
    integer, dimension(:), intent(in) :: new_shape
    type(array_type), pointer :: c

    integer :: s

    c => a%create_result(array_shape = [ new_shape, size(a%val,2) ])
    do concurrent(s=1:size(a%val,2))
       c%val(:, s) = a%val(:, s)
    end do
    c%indices = a%shape

    c%get_partial_left => get_partial_reshape
    c%get_partial_left_val => get_partial_reshape_val
    if(a%requires_grad) then
       c%requires_grad = .true.
       c%is_forward = a%is_forward
       c%operation = 'reshape'
       c%left_operand => a
       c%owns_left_operand = a%is_temporary
    end if
  end function reshape_array
!-------------------------------------------------------------------------------
  function get_partial_reshape(this, upstream_grad) result(output)
    implicit none
    class(array_type), intent(inout) :: this
    type(array_type), intent(in) :: upstream_grad
    type(array_type) :: output
    type(array_type), pointer :: ptr

    ptr => reshape( &
         upstream_grad, new_shape = this%indices &
    )
    call output%assign_and_deallocate_source(ptr)
  end function get_partial_reshape
!-------------------------------------------------------------------------------
  pure subroutine get_partial_reshape_val(this, upstream_grad, output)
    implicit none
    class(array_type), intent(in) :: this
    real(real32), dimension(:,:), intent(in) :: upstream_grad
    real(real32), dimension(:,:), intent(out) :: output

    output = upstream_grad
  end subroutine get_partial_reshape_val
!###############################################################################

end submodule diffstruc__operations_broadcast_sub
