        ! Module to handle parsing a configuration file
module MW_configuration_line
   implicit none
   private

   ! Public subroutine
   ! -----------------
   public :: get_word
   public :: parse_line
   public :: seek_next_data

   ! Public type
   ! -----------
   public :: MW_configuration_line_t

   integer, parameter, public :: max_word_length = 31             !< maximum number of characters in a word
   integer, parameter, public :: max_line_length = 256           !< maximum number of characters on one line
   integer, parameter :: max_line_words = 60               !< maximum number of words on one line
   character(2), parameter :: comment_char = "!#" !< characters which begin a comment
   character(6), parameter :: whitespace_char = &
        achar(9)  // & !< horizontal tabulation
        achar(10) // & !< Line feed 
        achar(11) // & !< Vertical Tabl
        achar(12) // & !< Form Feed
        achar(13) // & !< Carriage return
        achar(32)      !< space

   interface get_word
      module procedure get_word_as_string
      module procedure get_word_as_int
      module procedure get_word_as_real
      module procedure get_word_as_logical
   end interface get_word

   type MW_configuration_line_t
      character(len=max_line_length) :: line = ''                 !< whole line being read
      integer :: num_words = 0                                    !< number of words on the line
      character(len=max_word_length*max_line_words) :: words = '' !< maximum number of words
   end type MW_configuration_line_t

contains

   ! ================================================================================
   ! Seek next line with data in it
   !
   ! If we reach the end of the file, the subroutine returns an empty configuration line
   subroutine seek_next_data(this, funit, line_num)
      use iso_fortran_env, only: iostat_end
      use MW_errors, only: MW_errors_runtime_error => runtime_error
      implicit none

      ! Parameters
      ! ----------
      type(MW_configuration_line_t), intent(inout) :: this  !< configuration line
      integer, intent(in) :: funit                          !< file unit
      integer, intent(inout) :: line_num                    !< line number in configuration file

      ! Local
      ! -----
      integer :: ierr
      character(256) :: errmsg
      character(max_line_length) :: line

      ! Read configuration file 1 line at a time
      call void(this)
      do while (.true.)
         line_num = line_num + 1
         read(funit, '(a)', iostat=ierr) line
         if (ierr == iostat_end) then
            backspace(funit)
            line_num = line_num - 1
            exit ! End of file
         else if (ierr /= 0) then
            write(errmsg,'("An error occured while reading line ",i4," in configuration file")') &
                  line_num
            call MW_errors_runtime_error("seek_next_data","configuration.f90", errmsg)
         end if
         ! Parse input line
         call parse_line(this, line)

         if (this%num_words > 0) exit
      end do
   end subroutine seek_next_data

   ! ================================================================================
   ! Void a configuration line
   subroutine void(this)
      implicit none
      ! Parameters
      ! ----------
      type(MW_configuration_line_t), intent(inout) :: this
      ! Local
      ! -----

      this%line = ''
      this%num_words = 0
      this%words = ''

   end subroutine void

   ! ================================================================================
   ! Parse a configuration line
   !
   ! Remove comments from line read in the configuration file and split the line into words
   subroutine parse_line(this, line)
      use MW_errors, only: MW_errors_runtime_error => runtime_error
      implicit none

      ! Parameters
      ! ----------
      type(MW_configuration_line_t), intent(inout) :: this
      character(*), intent(in) :: line

      ! locals
      ! ------
      character(max_line_length) :: buffer
      integer :: ichar, icomment, istart, iend
      character(2*max_line_length) :: errmsg
      integer :: word_start, word_end, i
      this%line = line

      ! copy line into buffer
      buffer = adjustl(line)

      ! Remove comments
      icomment = scan(buffer, comment_char)

      if (icomment > 0) then
         do ichar = icomment, max_line_length
            buffer(ichar:ichar) = " "
         end do
      end if

      ! Find word boundaries
      iend = 0
      this%num_words = 0
      do while (iend < max_line_length)

         istart = verify(buffer(iend+1:), whitespace_char)

         ! exit if all characters in buffer(iend+1:) are whitespace characters
         if (istart == 0) then
            exit
         else
            istart = istart + iend
         end if

         iend = scan(buffer(istart:), whitespace_char)
         ! put the next whitespace after the line if there are none in buffer(istart:)
         if (iend == 0) then
            iend = max_line_length+1
         else
            iend = iend + istart - 1
         end if

         ! Check wether we reached the maximum number of words
         if (this%num_words == max_line_words) then
            write(errmsg,'("Too many words in configuration line (max ",i3,")",a1001)') &
                  max_line_words, new_line('A') // line
            call MW_errors_runtime_error("parse_line", "configuration.f90", &
                  errmsg)
         end if

         ! Check wether the word is not too long
         if ((iend-istart) > max_word_length) then
            write(errmsg,'("A word is too long in configuration line (max ",i3,")",a1001)') &
                  max_word_length, new_line('A') // line
            call MW_errors_runtime_error("parse_line", "configuration.f90", &
                  errmsg)
         end if

         ! Store the word between the first non-whitespace character the next whitespace character
         this%num_words = this%num_words+1

         word_start = (this%num_words-1)*max_word_length + 1
         word_end = word_start + iend - istart

         this%words(word_start:word_end) = buffer(istart:iend-1)

         word_start = word_end
         word_end = this%num_words*max_word_length
         do i = word_start, word_end
            this%words(i:i) = " "
         end do

      end do

   end subroutine parse_line

   ! ================================================================================
   ! Get a word as a string
   subroutine get_word_as_string(this, iword, word)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      ! Parameters
      ! ----------
      type(MW_configuration_line_t), intent(in)    :: this
      integer,                       intent(in)    :: iword
      character(*),                  intent(inout) :: word

      ! integer
      integer :: word_start, word_end

      if (iword > this%num_words) then
         call MW_errors_parameter_error("get_word_as_string", "configuration.f90", &
               "iword", iword)
      end if

      word_start = (iword-1)*max_word_length + 1
      word_end = word_start + min(max_word_length,len(word)) - 1
      word = this%words(word_start:word_end)

   end subroutine get_word_as_string

   ! ================================================================================
   ! Get a word as a string
   subroutine get_word_as_int(this, iword, word)
      use MW_errors, only: MW_errors_parameter_error => parameter_error, &
            MW_errors_runtime_error => runtime_error
      implicit none
      ! Parameters
      ! ----------
      type(MW_configuration_line_t), intent(in)    :: this
      integer,                       intent(in)    :: iword
      integer,                       intent(inout) :: word

      ! Local
      ! -----
      integer :: ierr
      character(2*max_line_length) :: errmsg
      integer :: word_start, word_end
      character(max_word_length) :: buffer

      if (iword > this%num_words) then
         call MW_errors_parameter_error("get_word_as_int", "configuration.f90", &
               "iword", iword)
      end if

      word_start = (iword-1)*max_word_length + 1
      word_end = word_start + max_word_length
      buffer = this%words(word_start:word_end)

      read(buffer, *, iostat=ierr) word
      if (ierr /= 0) then
         write(errmsg,'("Failed to read word ",i3," as integer in configuration line",a257)') &
               iword, new_line('A') //  this%line
         call MW_errors_runtime_error("get_word_as_int", "configuration.f90", &
               errmsg)
      end if

   end subroutine get_word_as_int

   ! ================================================================================
   ! Get a word as a real
   subroutine get_word_as_real(this, iword, word)
      use MW_kinds, only: wp
      use MW_errors, only: MW_errors_parameter_error => parameter_error, &
            MW_errors_runtime_error => runtime_error
      implicit none
      ! Parameters
      ! ----------
      type(MW_configuration_line_t), intent(in)    :: this
      integer,                       intent(in)    :: iword
      real(wp),                      intent(inout) :: word

      ! Local
      ! -----
      integer :: ierr
      character(2*max_line_length) :: errmsg
      integer :: word_start, word_end
      character(max_word_length) :: buffer

      if (iword > this%num_words) then
         call MW_errors_parameter_error("get_word_as_real", "configuration.f90", &
               "iword", iword)
      end if

      word_start = (iword-1)*max_word_length + 1
      word_end = word_start + max_word_length
      buffer = this%words(word_start:word_end)

      read(buffer,*, iostat=ierr) word
      if (ierr /= 0) then
         write(errmsg,'("Failed to read word ",i3," as real in configuration line",a257)') &
               iword, new_line('A') // this%line
         call MW_errors_runtime_error("get_word_as_real", "configuration.f90", &
               errmsg)
      end if

   end subroutine get_word_as_real

   ! ================================================================================
   ! Get a word as a logical true/false
   subroutine get_word_as_logical(this, iword, word)
      use MW_errors, only: MW_errors_parameter_error => parameter_error, &
            MW_errors_runtime_error => runtime_error
      implicit none
      ! Parameters
      ! ----------
      type(MW_configuration_line_t), intent(in)    :: this
      integer,                       intent(in)    :: iword
      logical,                       intent(inout) :: word

      ! Local
      ! -----
      character(2*max_line_length) :: errmsg
      integer :: word_start, word_end
      character(max_word_length) :: buffer

      if (iword > this%num_words) then
         call MW_errors_parameter_error("get_word_as_real", "configuration.f90", &
               "iword", iword)
      end if

      word_start = (iword-1)*max_word_length + 1
      word_end = word_start + max_word_length
      buffer = to_lower(this%words(word_start:word_end))

      if (buffer == "true") then
         word = .true.
      else if (buffer == "false") then
         word = .false.
      else
         write(errmsg,'("Failed to read word ",i3," as logical in configuration line",a257)') &
               iword, new_line('A') // this%line
         call MW_errors_runtime_error("get_word_as_logical", "configuration.f90", &
               errmsg)
      end if

   end subroutine get_word_as_logical

   function to_lower(strIn) result(strOut)
      ! Adapted from http://www.star.le.ac.uk/~cgp/fortran.html (25 May 2012)
      ! Original author: Clive Page

     implicit none

     character(len=*), intent(in) :: strIn
     character(len=len(strIn)) :: strOut
     integer :: i,j

     do i = 1, len(strIn)
        j = iachar(strIn(i:i))
        if (j>= iachar("A") .and. j<=iachar("Z") ) then
           strOut(i:i) = achar(iachar(strIn(i:i))+32)
        else
           strOut(i:i) = strIn(i:i)
        end if
     end do

  end function to_lower
end module MW_configuration_line
