!> Named composition parser for user-facing species input.
!!
!! Supports strings such as "CH4:1.0,O2:0.233,N2:0.767" and maps the names
!! into the solver/Cantera species ordering. This avoids fragile assumptions
!! about numeric species indices when a mechanism such as GRI30 dynamically
!! defines the transported species list.
module mod_species_composition
   use mod_precision, only : rk, zero, one, tiny_safe, name_len, fatal_error, lowercase
   use mod_species_registry, only : species_index_of
   implicit none

   private
   public :: parse_named_composition, composition_string_is_set

contains

   !> Returns true if the composition input string is populated and non-empty.
   !!
   !! @param text String representation of initial composition to test.
   logical function composition_string_is_set(text)
      character(len=*), intent(in) :: text !< The composition input text.
      composition_string_is_set = len_trim(adjustl(text)) > 0
   end function composition_string_is_set

   !> Systematically parses a composition string and maps values to species array indexes.
   !!
   !! Splits key-value tokens (e.g., "H2:2.0,O2:1.0") separated by commas, matches
   !! name keys with the registered species, and populates the mass fraction array.
   !! Optionally normalizes the output array to sum to unity.
   !!
   !! @param text The composition input string to parse.
   !! @param species_names Array of active species names.
   !! @param nspecies Number of active species.
   !! @param values Output array populated with parsed fractions.
   !! @param owner Name of the calling subsystem for error traceback.
   !! @param normalize Optional flag to normalize values to sum to 1.0 (default true).
   !! @param require_sum_positive Optional flag to mandate a non-zero sum (default true).
   subroutine parse_named_composition(text, species_names, nspecies, values, owner, normalize, require_sum_positive)
      character(len=*), intent(in) :: text                   !< Composition string to parse.
      character(len=*), intent(in) :: species_names(:)       !< Array of active species names.
      integer, intent(in) :: nspecies                        !< Number of species.
      real(rk), intent(out) :: values(:)                     !< Target array to populate.
      character(len=*), intent(in) :: owner                  !< Traceback owner name.
      logical, intent(in), optional :: normalize              !< Whether to normalize results.
      logical, intent(in), optional :: require_sum_positive  !< Whether sum must be strictly positive.

      character(len=:), allocatable :: work !< Scalable workspace copy of the input string.
      character(len=:), allocatable :: token !< Single name-value pair segment (e.g. "CH4:1.0").
      character(len=:), allocatable :: name !< Parsed species name token.
      character(len=:), allocatable :: value_text !< Parsed numeric value text.
      integer :: pos !< Position index of comma separator.
      integer :: colon !< Position index of colon separator.
      integer :: idx !< Matched index of species in the registry.
      real(rk) :: val !< Parsed real representation of key value.
      real(rk) :: sum_y !< Cumulative sum of fractions.
      logical :: do_normalize !< Internal resolved normalization flag.
      logical :: need_positive !< Internal resolved positive requirement flag.

      if (size(values) < nspecies) call fatal_error(trim(owner), 'composition output vector is too small')
      values = zero
      if (nspecies <= 0) then
         if (len_trim(text) > 0) call fatal_error(trim(owner), 'composition was specified but no species are available')
         return
      end if

      do_normalize = .true.
      if (present(normalize)) do_normalize = normalize
      need_positive = .true.
      if (present(require_sum_positive)) need_positive = require_sum_positive

      work = trim(adjustl(text))
      do while (len_trim(work) > 0)
         pos = index(work, ',')
         if (pos > 0) then
            token = trim(adjustl(work(:pos-1)))
            work = trim(adjustl(work(pos+1:)))
         else
            token = trim(adjustl(work))
            work = ''
         end if

         if (len_trim(token) == 0) cycle
         colon = index(token, ':')
         if (colon <= 1 .or. colon >= len_trim(token)) then
            call fatal_error(trim(owner), 'bad composition token "'//trim(token)//'"; expected NAME:VALUE')
         end if

         name = trim(adjustl(token(:colon-1)))
         value_text = trim(adjustl(token(colon+1:)))
         read(value_text, *, err=100) val
         if (val < zero) call fatal_error(trim(owner), 'negative mass fraction for species '//trim(name))

         idx = species_index_of(species_names, nspecies, name)
         if (idx <= 0) then
            call fatal_error(trim(owner), 'composition species "'//trim(name)//'" is not in the active species list')
         end if
         values(idx) = values(idx) + val
         cycle
 100      call fatal_error(trim(owner), 'could not parse composition value in token "'//trim(token)//'"')
      end do

      sum_y = sum(values(1:nspecies))
      if (sum_y <= tiny_safe) then
         if (need_positive) call fatal_error(trim(owner), 'composition mass-fraction sum is zero')
      else if (do_normalize) then
         values(1:nspecies) = values(1:nspecies) / sum_y
      end if
   end subroutine parse_named_composition

end module mod_species_composition
