module bem_periodic_zero_mode_eval
  use bem_kinds, only: dp, i32
  use bem_constants, only: eps0
  use bem_periodic_zero_mode_plan, only: periodic_zero_mode_plan_type, periodic_zero_mode_state_type
  implicit none
  private

  integer(i32), parameter, public :: zero_mode_trace_minus = -1_i32
  integer(i32), parameter, public :: zero_mode_trace_principal_value = 0_i32
  integer(i32), parameter, public :: zero_mode_trace_plus = 1_i32

  public :: eval_periodic_zero_mode

contains

  subroutine eval_periodic_zero_mode(plan, state, z, trace, potential, field)
    type(periodic_zero_mode_plan_type), intent(in) :: plan
    type(periodic_zero_mode_state_type), intent(in) :: state
    real(dp), intent(in) :: z
    integer(i32), intent(in) :: trace
    real(dp), intent(out) :: potential, field
    real(dp) :: cumulative_charge, sheet_correction, primitive, gauge_primitive
    integer(i32) :: interval, breakpoint, gauge_interval

    if (trace < zero_mode_trace_minus .or. trace > zero_mode_trace_plus) then
      error stop 'invalid periodic zero-mode trace.'
    end if
    call locate_interval(plan%break_z, z, interval, breakpoint)
    cumulative_charge = evaluate_polynomial(state%cumulative_charge_coeff(:, interval), z)
    if (breakpoint > 0_i32) then
      sheet_correction = 0.5_dp*real(1_i32 - trace, dp)*state%sheet_charge(breakpoint)
      cumulative_charge = cumulative_charge - sheet_correction
    end if
    field = state%e_bottom + cumulative_charge/(eps0*plan%area_xy)
    primitive = charge_primitive(plan, state, z, interval)
    call locate_interval(plan%break_z, state%z_gauge, gauge_interval, breakpoint)
    gauge_primitive = charge_primitive(plan, state, state%z_gauge, gauge_interval)
    potential = state%phi_gauge - state%e_bottom*(z - state%z_gauge) - &
                (primitive - gauge_primitive)/(eps0*plan%area_xy)
  end subroutine eval_periodic_zero_mode

  pure real(dp) function charge_primitive(plan, state, z, interval) result(primitive)
    type(periodic_zero_mode_plan_type), intent(in) :: plan
    type(periodic_zero_mode_state_type), intent(in) :: state
    real(dp), intent(in) :: z
    integer(i32), intent(in) :: interval
    integer(i32) :: lower_break

    if (interval == 1_i32) then
      primitive = integrate_polynomial(state%cumulative_charge_coeff(:, 1), plan%break_z(1), z)
      return
    end if
    lower_break = interval - 1_i32
    primitive = state%primitive_at_break(lower_break) + &
                integrate_polynomial( &
                state%cumulative_charge_coeff(:, interval), plan%break_z(lower_break), z &
                )
  end function charge_primitive

  pure subroutine locate_interval(break_z, z, interval, breakpoint)
    real(dp), intent(in) :: break_z(:), z
    integer(i32), intent(out) :: interval, breakpoint
    integer(i32) :: low, high, middle
    real(dp) :: tolerance

    ! The plan stores sorted breakpoints, so an endpoint has the largest magnitude.
    tolerance = 128.0_dp*epsilon(1.0_dp)*max(1.0_dp, abs(z), abs(break_z(1)), abs(break_z(size(break_z))))
    low = 1_i32
    high = size(break_z)
    breakpoint = 0_i32
    do while (low <= high)
      middle = (low + high)/2_i32
      if (abs(z - break_z(middle)) <= tolerance) then
        breakpoint = middle
        interval = middle + 1_i32
        return
      else if (z < break_z(middle)) then
        high = middle - 1_i32
      else
        low = middle + 1_i32
      end if
    end do
    interval = low
  end subroutine locate_interval

  pure real(dp) function evaluate_polynomial(coefficient, z) result(value)
    real(dp), intent(in) :: coefficient(3), z

    value = coefficient(1) + z*(coefficient(2) + z*coefficient(3))
  end function evaluate_polynomial

  pure real(dp) function integrate_polynomial(coefficient, lower, upper) result(integral)
    real(dp), intent(in) :: coefficient(3), lower, upper

    integral = coefficient(1)*(upper - lower) + &
               0.5_dp*coefficient(2)*(upper*upper - lower*lower) + &
               coefficient(3)/3.0_dp*(upper**3 - lower**3)
  end function integrate_polynomial

end module bem_periodic_zero_mode_eval
