!> \file {{ file_name }}
!> \copydoc {{ module_name }}

!> \brief {{ brief_text }}
!> \details {{ details_text | replace('\n', '\n!! ') }}
{% if module_doc %}
{{ module_doc }}
{% endif %}
module {{ module_name }}
  use {{ helper_module }}, only: &
    {{ helper_imports | join(', &\n    ') }}
{% if use_ieee %}
  use ieee_arithmetic, only: ieee_value, ieee_quiet_nan, ieee_is_nan
{% endif %}
{% if kind_imports %}
  ! kind specifiers listed in the nml-tools configuration file
  use {{ kind_module }}, only: &
    {{ kind_imports | join(', &\n    ') }}
{% endif %}

  implicit none

{% if default_parameters %}
  ! default values
{% for stmt in default_parameters %}
  {{ stmt }}
{% endfor %}

{% endif %}
{% if enum_parameters %}
  ! enum values
{% for stmt in enum_parameters %}
  {{ stmt }}
{% endfor %}

{% endif %}
{% if bounds_parameters %}
  ! bounds values
{% for stmt in bounds_parameters %}
  {{ stmt }}
{% endfor %}

{% endif %}
  !> \class {{ doc_class }}
  !> \brief {{ brief_text }}
  !> \details {{ details_text | replace('\n', '\n  !! ') }}
  type, public :: {{ type_name }}
    logical :: is_configured = .false. !< whether the namelist has been configured
{% for field in fields %}
{{ field.declaration | indent(4, True) }}
{% endfor %}
  contains
    procedure :: init => {{ type_prefix }}_init
    procedure :: from_file => {{ type_prefix }}_from_file
    procedure :: set => {{ type_prefix }}_set
    procedure :: is_set => {{ type_prefix }}_is_set
{% if flex_arrays %}
    procedure :: filled_shape => {{ type_prefix }}_filled_shape
{% endif %}
    procedure :: is_valid => {{ type_prefix }}_is_valid
  end type {{ type_name }}

contains

{% if enum_functions %}
{% for enum in enum_functions %}
  !> \brief Check whether a value is part of an enum
  elemental logical function {{ enum.func_name }}(val, allow_missing) result(in_enum)
    {{ enum.arg_type_spec }}, intent(in) :: val
    logical, intent(in), optional :: allow_missing

    if (present(allow_missing)) then
      if (allow_missing) then
        if ({{ enum.missing_condition }}) then
          in_enum = .true.
          return
        end if
      end if
    end if
{% if enum.use_trim %}
    in_enum = any(trim(val) == {{ enum.enum_values_name }})
{% else %}
    in_enum = any(val == {{ enum.enum_values_name }})
{% endif %}
  end function {{ enum.func_name }}

{% endfor %}
{% endif %}
{% if bounds_functions %}
{% for bounds in bounds_functions %}
  !> \brief Check whether a value is within bounds
  elemental logical function {{ bounds.func_name }}(val, allow_missing) result(in_bounds)
    {{ bounds.arg_type_spec }}, intent(in) :: val
    logical, intent(in), optional :: allow_missing

    if (present(allow_missing)) then
      if (allow_missing) then
        if ({{ bounds.missing_condition }}) then
          in_bounds = .true.
          return
        end if
      end if
    end if

    in_bounds = .true.
{% if bounds.has_min %}
    if (val {% if bounds.min_exclusive %}<={% else %}<{% endif %} {{ bounds.min_name }}) in_bounds = .false.
{% endif %}
{% if bounds.has_max %}
    if (val {% if bounds.max_exclusive %}>={% else %}>{% endif %} {{ bounds.max_name }}) in_bounds = .false.
{% endif %}
  end function {{ bounds.func_name }}

{% endfor %}
{% endif %}
  !> \brief Initialize defaults and sentinels for {{ namelist_name }}
  integer function {{ type_prefix }}_init(this, errmsg) result(status)
    class({{ type_name }}), intent(inout) :: this
    character(len=*), intent(out), optional :: errmsg

    status = NML_OK
    if (present(errmsg)) errmsg = ""
    this%is_configured = .false.

{% if sentinel_assignments %}
    ! sentinel values for required/optional parameters
{% for stmt in sentinel_assignments %}
{{ stmt | indent(4, True) }}
{% endfor %}
{% endif %}
{% if default_assignments %}
    ! default values
{% for stmt in default_assignments %}
{{ stmt | indent(4, True) }}
{% endfor %}
{% endif %}
  end function {{ type_prefix }}_init

  !> \brief Read {{ namelist_name }} namelist from file
  integer function {{ type_prefix }}_from_file(this, file, errmsg) result(status)
    class({{ type_name }}), intent(inout) :: this
    character(len=*), intent(in) :: file !< path to namelist file
    character(len=*), intent(out), optional :: errmsg
    ! namelist variables
{% for field in fields %}
{{ field.local_declaration | indent(4, True) }}
{% endfor %}
    ! locals
    type(nml_file_t) :: nml
    integer :: iostat
    integer :: close_status
    character(len=nml_line_buffer) :: iomsg

    namelist /{{ namelist_name }}/ &
      {{ namelist_vars | join(', &\n      ') }}

    status = this%init(errmsg=errmsg)
    if (status /= NML_OK) return
{% for stmt in local_init_assignments %}
{{ stmt | indent(4, True) }}
{% endfor %}

    status = nml%open(file, errmsg=errmsg)
    if (status /= NML_OK) return

    status = nml%find("{{ namelist_name }}", errmsg=errmsg)
    if (status /= NML_OK) then
      close_status = nml%close()
      return
    end if

    ! read namelist
    read(nml%unit, nml={{ namelist_name }}, iostat=iostat, iomsg=iomsg)
    if (iostat /= 0) then
      status = NML_ERR_READ
      if (present(errmsg)) errmsg = trim(iomsg)
      close_status = nml%close()
      return
    end if
    close_status = nml%close(errmsg=errmsg)
    if (close_status /= NML_OK) then
      status = close_status
      return
    end if

    ! assign values
{% for stmt in assignments %}
{{ stmt | indent(4, True) }}
{% endfor %}

    ! mark as configured
    this%is_configured = .true.
    status = NML_OK
  end function {{ type_prefix }}_from_file

  !> \brief Set {{ namelist_name }} values
{% if argument_list %}
  integer function {{ type_prefix }}_set(this, &
    {{ argument_list | join(', &\n    ') }}, &
    errmsg) result(status)
{% else %}
  integer function {{ type_prefix }}_set(this, errmsg) result(status)
{% endif %}

    class({{ type_name }}), intent(inout) :: this
    character(len=*), intent(out), optional :: errmsg
{% for decl in required_argument_declarations %}
{{ decl | indent(4, True) }}
{% endfor %}
{% for decl in optional_argument_declarations %}
{{ decl | indent(4, True) }}
{% endfor %}
{% if flex_bound_vars %}
    integer :: &
      {{ flex_bound_vars | join(', &\n      ') }}
{% endif %}

    status = this%init(errmsg=errmsg)
    if (status /= NML_OK) return

    ! required parameters
{% for stmt in set_required_assignments %}
{{ stmt | indent(4, True) }}
{% endfor %}
{% if set_optional_present %}
    ! override with provided values
{% for stmt in set_optional_present %}
{{ stmt | indent(4, True) }}
{% endfor %}
{% endif %}

    ! mark as configured
    this%is_configured = .true.
    status = NML_OK
  end function {{ type_prefix }}_set

  !> \brief Check whether a namelist value was set
  integer function {{ type_prefix }}_is_set(this, name, idx, errmsg) result(status)
    class({{ type_name }}), intent(in) :: this
    character(len=*), intent(in) :: name
    integer, intent(in), optional :: idx(:)
    character(len=*), intent(out), optional :: errmsg

    status = NML_OK
    if (present(errmsg)) errmsg = ""
    select case (trim(name))
{% for case in presence_cases %}
    case ("{{ case.name }}")
{% if case.is_array %}
      if (present(idx)) then
        status = idx_check(idx, lbound(this%{{ case.name }}), ubound(this%{{ case.name }}), &
          "{{ case.name }}", errmsg)
        if (status /= NML_OK) return
{% if not case.always_true %}
        if ({{ case.element_condition }}) status = NML_ERR_NOT_SET
{% endif %}
      else
{% if not case.always_true %}
        if ({{ case.sentinel_condition }}) status = NML_ERR_NOT_SET
{% endif %}
      end if
{% else %}
      if (present(idx)) then
        status = NML_ERR_INVALID_INDEX
        if (present(errmsg)) errmsg = "index not supported for '{{ case.name }}'"
        return
      end if
{% if not case.always_true %}
      if ({{ case.sentinel_condition }}) status = NML_ERR_NOT_SET
{% endif %}
{% endif %}
{% endfor %}
    case default
      status = NML_ERR_INVALID_NAME
      if (present(errmsg)) errmsg = "unknown field: " // trim(name)
    end select
    if (status == NML_ERR_NOT_SET .and. present(errmsg)) then
      if (len_trim(errmsg) == 0) errmsg = "field not set: " // trim(name)
    end if
  end function {{ type_prefix }}_is_set

{% if flex_arrays %}
  !> \brief Determine the filled shape along flexible dimensions
  integer function {{ type_prefix }}_filled_shape(this, name, filled, errmsg) result(status)
    class({{ type_name }}), intent(in) :: this
    character(len=*), intent(in) :: name
    integer, intent(out) :: filled(:)
    character(len=*), intent(out), optional :: errmsg
    integer :: idx
    integer :: dim
{% if flex_bound_vars %}
    integer :: &
      {{ flex_bound_vars | join(', &\n      ') }}
{% endif %}

    status = NML_OK
    if (present(errmsg)) errmsg = ""
    select case (trim(name))
{% for flex in flex_arrays %}
    case ("{{ flex.name }}")
      if (size(filled) /= {{ flex.rank }}) then
        status = NML_ERR_INVALID_INDEX
        if (present(errmsg)) errmsg = "shape rank mismatch for '{{ flex.name }}'"
        return
      end if
      do dim = 1, {{ flex.rank }}
        filled(dim) = size(this%{{ flex.name }}, dim)
      end do
{% for dim in flex.flex_dims %}
      filled({{ dim }}) = 0
      do idx = ubound(this%{{ flex.name }}, {{ dim }}), &
        lbound(this%{{ flex.name }}, {{ dim }}), -1
        if (.not. ({{ flex.slice_missing_conditions[loop.index0] }})) then
          filled({{ dim }}) = idx - lbound(this%{{ flex.name }}, {{ dim }}) + 1
          exit
        end if
      end do
{% endfor %}
      if (minval(filled) > 0) then
{% for bound in flex.bounds %}
        {{ bound.lb_var }} = lbound(this%{{ flex.name }}, {{ bound.dim }})
        {{ bound.ub_var }} = {{ bound.lb_var }} + filled({{ bound.dim }}) - 1
{% endfor %}
        if ({{ flex.prefix_any_missing_condition }}) then
          status = NML_ERR_PARTLY_SET
          if (present(errmsg)) errmsg = "array partly set: {{ flex.name }}"
          return
        end if
      end if
{% endfor %}
    case default
      status = NML_ERR_INVALID_NAME
      if (present(errmsg)) errmsg = "field is not a flexible array: " // trim(name)
    end select
  end function {{ type_prefix }}_filled_shape

{% endif %}
  !> \brief Validate required values and constraints
  integer function {{ type_prefix }}_is_valid(this, errmsg) result(status)
    class({{ type_name }}), intent(in) :: this
    character(len=*), intent(out), optional :: errmsg
    integer :: istat
{% if flex_arrays %}
    integer, allocatable :: filled(:)
{% endif %}

    status = NML_OK
    if (present(errmsg)) errmsg = ""

{% if required_scalar_validations %}
    ! required parameters
{% for name in required_scalar_validations %}
    istat = this%is_set("{{ name }}", errmsg=errmsg)
    if (istat == NML_ERR_NOT_SET) then
      status = NML_ERR_REQUIRED
      if (present(errmsg)) then
        if (len_trim(errmsg) == 0) then
          errmsg = "field not set: {{ name }}"
        end if
        errmsg = "required " // trim(errmsg)
      end if
      return
    end if
    if (istat /= NML_OK) then
      status = istat
      return
    end if
{% endfor %}
{% endif %}
{% if required_array_validations %}
    ! required arrays
{% for entry in required_array_validations %}
    if ({{ entry.all_missing_condition }}) then
      status = NML_ERR_REQUIRED
      if (present(errmsg)) errmsg = "required field not set: {{ entry.name }}"
      return
    end if
    if ({{ entry.any_missing_condition }}) then
      status = NML_ERR_PARTLY_SET
      if (present(errmsg)) errmsg = "array partly set: {{ entry.name }}"
      return
    end if
{% endfor %}
{% endif %}
{% if flex_arrays %}
    ! flexible arrays
{% for entry in flex_arrays %}
    if (allocated(filled)) deallocate(filled)
    allocate(filled({{ entry.rank }}))
    istat = this%filled_shape("{{ entry.name }}", filled, errmsg=errmsg)
    if (istat == NML_ERR_PARTLY_SET) then
      status = istat
      if (present(errmsg)) then
        if (len_trim(errmsg) == 0) errmsg = "array partly set: {{ entry.name }}"
      end if
      return
    end if
    if (istat /= NML_OK) then
      status = istat
      return
    end if
{% if entry.required %}
    if (minval(filled) == 0) then
      status = NML_ERR_REQUIRED
      if (present(errmsg)) errmsg = "required field not set: {{ entry.name }}"
      return
    end if
{% endif %}
{% endfor %}
{% endif %}
{% if enum_checks %}
    ! enum constraints
{% for check in enum_checks %}
{% if check.is_array %}
    if (.not. all({{ check.func_name }}({{ check.array_ref }}, allow_missing=.true.))) then
      status = NML_ERR_ENUM
      if (present(errmsg)) errmsg = "enum constraint failed: {{ check.name }}"
      return
    end if
{% else %}
    istat = this%is_set("{{ check.name }}", errmsg=errmsg)
    if (istat == NML_OK) then
      if (.not. {{ check.func_name }}({{ check.element_ref }})) then
        status = NML_ERR_ENUM
        if (present(errmsg)) errmsg = "enum constraint failed: {{ check.name }}"
        return
      end if
    else if (istat /= NML_ERR_NOT_SET) then
      status = istat
      return
    end if
{% endif %}
{% endfor %}
{% endif %}
{% if bounds_checks %}
    ! bounds constraints
{% for check in bounds_checks %}
{% if check.is_array %}
    if (.not. all({{ check.func_name }}({{ check.array_ref }}, allow_missing=.true.))) then
      status = NML_ERR_BOUNDS
      if (present(errmsg)) errmsg = "bounds constraint failed: {{ check.name }}"
      return
    end if
{% else %}
    istat = this%is_set("{{ check.name }}", errmsg=errmsg)
    if (istat == NML_OK) then
      if (.not. {{ check.func_name }}({{ check.element_ref }})) then
        status = NML_ERR_BOUNDS
        if (present(errmsg)) errmsg = "bounds constraint failed: {{ check.name }}"
        return
      end if
    else if (istat /= NML_ERR_NOT_SET) then
      status = istat
      return
    end if
{% endif %}
{% endfor %}
{% endif %}
  end function {{ type_prefix }}_is_valid

end module {{ module_name }}
