!*****************************************************************************************
!> author: Jacob Williams
!  license: BSD
!
!  JSON-Fortran support module for string manipulation.
!
!### License
!  * JSON-Fortran is released under a BSD-style license.
!    See the [LICENSE](https://github.com/jacobwilliams/json-fortran/blob/master/LICENSE)
!    file for details.

    module json_string_utilities

    use,intrinsic :: ieee_arithmetic
    use json_kinds
    use json_parameters

    implicit none

    private

    !******************************************************
    !>
    !  Convert a 'DEFAULT' kind character input to
    !  'ISO_10646' kind and return it
    interface to_unicode
        module procedure to_uni, to_uni_vec
    end interface
    !******************************************************

#ifdef USE_UCS4
    !******************************************************
    !>
    ! Provide a means to convert to UCS4 while
    ! concatenating UCS4 and default strings
    interface operator(//)
       module procedure ucs4_join_default, default_join_ucs4
    end interface
    public :: operator(//)
    !******************************************************

    !******************************************************
    !>
    ! Provide a string `==` operator that works
    ! with mixed kinds
    interface operator(==)
       module procedure ucs4_comp_default, default_comp_ucs4
    end interface
    public :: operator(==)
    !******************************************************

    !******************************************************
    !>
    ! Provide a string `/=` operator that works
    ! with mixed kinds
    interface operator(/=)
       module procedure ucs4_neq_default, default_neq_ucs4
    end interface
    public :: operator(/=)
    !******************************************************
#endif

    public :: integer_to_string
    public :: real_to_string
    public :: string_to_integer
    public :: string_to_real
    public :: string_to_real_c
    public :: valid_json_hex
    public :: to_unicode
    public :: escape_string
    public :: unescape_string
    public :: lowercase_string
    public :: replace_string
    public :: decode_rfc6901
    public :: encode_rfc6901

    contains
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!  date: 12/4/2013
!
!  Convert an integer to a string.

    pure subroutine integer_to_string(ival,int_fmt,str)

    implicit none

    integer(IK),intent(in)               :: ival    !! integer value.
    character(kind=CDK,len=*),intent(in) :: int_fmt !! format for integers
    character(kind=CK,len=*),intent(out) :: str     !! `ival` converted to a string.

    integer(IK) :: istat

    write(str,fmt=int_fmt,iostat=istat) ival

    if (istat==0) then
        str = adjustl(str)
    else
        str = repeat(star,len(str))
    end if

    end subroutine integer_to_string
!*****************************************************************************************

!*****************************************************************************************
!>
!  Convert a string into an integer.
!
!# History
!  * Jacob Williams : 12/10/2013 : Rewrote original `parse_integer` routine.
!    Added error checking.
!  * Modified by Izaak Beekman
!  * Jacob Williams : 2/4/2017 : moved core logic to this routine.

    subroutine string_to_integer(str,ival,status_ok)

    implicit none

    character(kind=CK,len=*),intent(in) :: str        !! the string to convert to an integer
    integer(IK),intent(out)             :: ival       !! the integer value
    logical(LK),intent(out)             :: status_ok  !! true if there were no errors

    character(kind=CDK,len=:),allocatable :: digits
    integer(IK) :: ndigits_digits,ndigits,ierr

    ! Compute how many digits we need to read
    ndigits = 2*len_trim(str)
    if (ndigits/=0) then
        ndigits_digits = nint(log10(real(ndigits)))+1
        allocate(character(kind=CDK,len=ndigits_digits) :: digits)
        write(digits,'(I0)') ndigits !gfortran will have a runtime error with * edit descriptor here
        ! gfortran bug: '*' edit descriptor for ISO_10646 strings does bad stuff.
        read(str,'(I'//trim(digits)//')',iostat=ierr) ival   !string to integer
        ! error check:
        status_ok = (ierr==0)
    else
        status_ok = .false.
    end if
    if (.not. status_ok) ival = 0_IK

    end subroutine string_to_integer
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!  date: 12/4/2013
!
!  Convert a real value to a string.
!
!### Modified
!  * Izaak Beekman  : 02/24/2015 : added the compact option.
!  * Jacob Williams : 10/27/2015 : added the star option.
!  * Jacob Williams : 07/07/2019 : added null and ieee options.

    subroutine real_to_string(rval,real_fmt,compact_real,non_normals_to_null,str)

    implicit none

    real(RK),intent(in)                  :: rval         !! real value.
    character(kind=CDK,len=*),intent(in) :: real_fmt     !! format for real numbers
    logical(LK),intent(in)               :: compact_real !! compact the string so that it is
                                                         !! displayed with fewer characters
    logical(LK),intent(in)               :: non_normals_to_null !! If True, NaN, Infinity, or -Infinity are returned as `null`.
                                                                !! If False, the string value will be returned in quotes
                                                                !! (e.g., "NaN", "Infinity", or "-Infinity" )
    character(kind=CK,len=*),intent(out) :: str          !! `rval` converted to a string.

    integer(IK) :: istat !! write `iostat` flag

    if (ieee_is_finite(rval) .and. .not. ieee_is_nan(rval)) then

        ! normal real numbers

        if (real_fmt==star) then
            write(str,fmt=*,iostat=istat) rval
        else
            write(str,fmt=real_fmt,iostat=istat) rval
        end if

        if (istat==0) then
            !in this case, the default string will be compacted,
            ! so that the same value is displayed with fewer characters.
            if (compact_real) call compact_real_string(str)
        else
            str = repeat(star,len(str)) ! error
        end if

    else
        ! special cases for NaN, Infinity, and -Infinity

        if (non_normals_to_null) then
            ! return it as a JSON null value
            str = null_str
        else
            ! Let the compiler do the real to string conversion
            ! like before, but put the result in quotes so it
            ! gets printed as a string
            write(str,fmt=*,iostat=istat) rval
            if (istat==0) then
                str = quotation_mark//trim(adjustl(str))//quotation_mark
            else
                str = repeat(star,len(str)) ! error
            end if
        end if

    end if

    end subroutine real_to_string
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!  date: 1/19/2014
!
!  Convert a string into a `real(RK)`.
!
!# History
!  * Jacob Williams, 10/27/2015 : Now using `fmt=*`, rather than
!    `fmt=real_fmt`, since it doesn't work for some unusual cases
!    (e.g., when `str='1E-5'`).
!  * Jacob Williams : 2/6/2017 : moved core logic to this routine.

    subroutine string_to_real(str,use_quiet_nan,rval,status_ok)

    implicit none

    character(kind=CK,len=*),intent(in) :: str           !! the string to convert to a real
    logical(LK),intent(in)              :: use_quiet_nan !! if true, return NaN's as `ieee_quiet_nan`.
                                                         !! otherwise, use `ieee_signaling_nan`.
    real(RK),intent(out)                :: rval          !! `str` converted to a real value
    logical(LK),intent(out)             :: status_ok     !! true if there were no errors

    integer(IK) :: ierr  !! read iostat error code

    read(str,fmt=*,iostat=ierr) rval

    status_ok = (ierr==0)
    if (.not. status_ok) then
        rval = 0.0_RK
    else
        if (ieee_support_nan(rval)) then
            if (ieee_is_nan(rval)) then
                ! make sure to return the correct NaN
                if (use_quiet_nan) then
                    rval = ieee_value(rval,ieee_quiet_nan)
                else
                    rval = ieee_value(rval,ieee_signaling_nan)
                end if
            end if
        end if
    end if

    end subroutine string_to_real
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!  date: 11/05/2021
!
!  Convert a string into a `real(RK)`.
!  This version uses `strtof`, `strtod`, or `strtold` from C.
!  It will fall back to using `read(fmt=*)` if any errors.
!
!# History
!  * Jacob Williams : 11/05/2021 : created by modification of [[string_to_real]].

    subroutine string_to_real_c(str,use_quiet_nan,rval,status_ok)

    use iso_c_binding, only: c_double, c_float, &
                             c_char, c_ptr, c_null_ptr, c_long, &
                             c_null_char, c_loc, c_associated
#ifdef REAL128
    use iso_c_binding, only: c_long_double
#endif
    implicit none

    character(kind=CK,len=*),intent(in) :: str           !! the string to convert to a real
    logical(LK),intent(in)              :: use_quiet_nan !! if true, return NaN's as `ieee_quiet_nan`.
                                                         !! otherwise, use `ieee_signaling_nan`.
    real(RK),intent(out)                :: rval          !! `str` converted to a real value
    logical(LK),intent(out)             :: status_ok     !! true if there were no errors

    integer(IK) :: ierr  !! read iostat error code
    type(c_ptr) :: endptr !! pointer arg to `strtof`, etc.
    character(kind=c_char,len=:),allocatable,target :: c_str !! for null-terminated C string
    type(c_ptr) :: str_start !! pointer to start of string for comparison
    logical :: done !! if the string has been processed

    interface
        function strtof( str, endptr ) result(d) bind(C, name="strtof" )
            !! <stdlib.h> :: float strtof(const char *str, char **endptr)
            import
            implicit none
            character(kind=c_char,len=1),dimension(*),intent(in) :: str
            type(c_ptr), intent(inout) :: endptr
            real(c_float) :: d
        end function strtof
        function strtod( str, endptr ) result(d) bind(C, name="strtod" )
            !! <stdlib.h> :: double strtod(const char *str, char **endptr)
            import
            implicit none
            character(kind=c_char,len=1),dimension(*),intent(in) :: str
            type(c_ptr), intent(inout) :: endptr
            real(c_double) :: d
        end function strtod
#ifdef REAL128
        function strtold( str, endptr ) result(d) bind(C, name="strtold" )
            !! <stdlib.h> :: long double strtold(const char *str, char **endptr)
            import
            implicit none
            character(kind=c_char,len=1),dimension(*),intent(in) :: str
            type(c_ptr), intent(inout) :: endptr
            real(c_long_double) :: d
        end function strtold
#endif
    end interface

#ifdef USE_UCS4
    ! if using unicode, don't try to call the C routines
    ! [not sure they will work? need to test this... what if c_char /= CK?]
    call string_to_real(str,use_quiet_nan,rval,status_ok)
    return
#endif

    ! Create null-terminated C string
    c_str = trim(str)//C_NULL_CHAR
    str_start = c_loc(c_str)
    endptr = c_null_ptr
    done = .false.

#ifdef REAL32

    ! single precision

    if (RK == c_float) then
        rval = strtof( c_str, endptr )
        ! Check if conversion was successful:
        ! endptr should not point to the start (no conversion) and should not be null
        if (c_associated(endptr) .and. .not. c_associated(endptr, str_start)) then
            ierr = 0
            status_ok = .true.
            done = .true.
        end if
    end if

#elif REAL128

    ! quad precision

    if (RK == c_long_double) then
        rval = strtold( c_str, endptr )
        ! Check if conversion was successful:
        if (c_associated(endptr) .and. .not. c_associated(endptr, str_start)) then
            ierr = 0
            status_ok = .true.
            done = .true.
        end if
    end if

#else

    ! double precision

    if (RK == c_double) then
        rval = strtod( c_str, endptr )
        ! Check if conversion was successful:
        if (c_associated(endptr) .and. .not. c_associated(endptr, str_start)) then
            ierr = 0
            status_ok = .true.
            done = .true.
        end if
    end if

#endif

    if (allocated(c_str)) deallocate(c_str)

    if (.not. done) then
        ! the string was not processed, fallback to read:
        read(str,fmt=*,iostat=ierr) rval
        status_ok = (ierr==0)
    end if

    if (.not. status_ok) then
        rval = 0.0_RK
    else
        if (ieee_support_nan(rval)) then
            if (ieee_is_nan(rval)) then
                ! make sure to return the correct NaN
                if (use_quiet_nan) then
                    rval = ieee_value(rval,ieee_quiet_nan)
                else
                    rval = ieee_value(rval,ieee_signaling_nan)
                end if
            end if
        end if
    end if

    end subroutine string_to_real_c
!*****************************************************************************************

!*****************************************************************************************
!> author: Izaak Beekman
!  date: 02/24/2015
!
!  Compact a string representing a real number, so that
!  the same value is displayed with fewer characters.
!
!# See also
!  * [[real_to_string]]

    subroutine compact_real_string(str)

    implicit none

    character(kind=CK,len=*),intent(inout) :: str  !! string representation of a real number.

    character(kind=CK,len=len(str)) :: significand
    character(kind=CK,len=len(str)) :: expnt
    character(kind=CK,len=2) :: separator
    integer(IK) :: exp_start
    integer(IK) :: decimal_pos
    integer(IK) :: sig_trim
    integer(IK) :: exp_trim
    integer(IK) :: i  !! counter

    str = adjustl(str)
    exp_start = scan(str,CK_'eEdD')
    if (exp_start == 0) exp_start = scan(str,CK_'-+',back=.true.)
    decimal_pos = scan(str,CK_'.')
    if (exp_start /= 0) separator = str(exp_start:exp_start)

    if ( exp_start < decimal_pos ) then !possibly signed, exponent-less float

        significand = str
        sig_trim = len(trim(significand))
        do i = len(trim(significand)),decimal_pos+2,-1 !look from right to left at 0s
                                                       !but save one after the decimal place
            if (significand(i:i) == '0') then
                sig_trim = i-1
            else
                exit
            end if
        end do
        str = trim(significand(1:sig_trim))

    else if (exp_start > decimal_pos) then !float has exponent

        significand = str(1:exp_start-1)
        sig_trim = len(trim(significand))
        do i = len(trim(significand)),decimal_pos+2,-1 !look from right to left at 0s
            if (significand(i:i) == '0') then
                sig_trim = i-1
            else
                exit
            end if
        end do
        expnt = adjustl(str(exp_start+1:))
        if (expnt(1:1) == '+' .or. expnt(1:1) == '-') then
            separator = trim(adjustl(separator))//expnt(1:1)
            exp_start = exp_start + 1
            expnt     = adjustl(str(exp_start+1:))
        end if
        exp_trim = 1
        do i = 1,(len(trim(expnt))-1) !look at exponent leading zeros saving last
            if (expnt(i:i) == '0') then
                exp_trim = i+1
            else
                exit
            end if
        end do
        str = trim(adjustl(significand(1:sig_trim)))// &
              trim(adjustl(separator))// &
              trim(adjustl(expnt(exp_trim:)))

    !else ! mal-formed real, BUT this code should be unreachable

    end if

    end subroutine compact_real_string
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!  date: 1/21/2014
!
!  Add the escape characters to a string for adding to JSON.

    subroutine escape_string(str_in, str_out, escape_solidus)

    implicit none

    character(kind=CK,len=*),intent(in)              :: str_in
    character(kind=CK,len=:),allocatable,intent(out) :: str_out
    logical(LK),intent(in) :: escape_solidus  !! if the solidus (forward slash)
                                              !! is also to be escaped

    integer(IK) :: i    !! counter
    integer(IK) :: ipos !! accumulated string size
                        !! (so we can allocate it in chunks for
                        !! greater runtime efficiency)
    character(kind=CK,len=1) :: c  !! for reading `str_in` one character at a time.
#if defined __GFORTRAN__
    character(kind=CK,len=:),allocatable :: tmp !! workaround for bug in gfortran 6.1
#endif
    logical :: to_be_escaped !! if there are characters to be escaped

    character(kind=CK,len=*),parameter :: specials_no_slash = quotation_mark//&
                                                     backslash//&
                                                     bspace//&
                                                     formfeed//&
                                                     newline//&
                                                     carriage_return//&
                                                     horizontal_tab

    character(kind=CK,len=*),parameter :: specials = specials_no_slash//slash

    !Do a quick scan for the special characters,
    ! if any are present, then process the string,
    ! otherwise, return the string as is.
    if (escape_solidus) then
        to_be_escaped = scan(str_in,specials)>0
    else
        to_be_escaped = scan(str_in,specials_no_slash)>0
    end if

    if (to_be_escaped) then

        str_out = repeat(space,chunk_size)
        ipos = 1

        !go through the string and look for special characters:
        do i=1,len(str_in)

            c = str_in(i:i)    !get next character in the input string

            !if the string is not big enough, then add another chunk:
            if (ipos+3>len(str_out)) str_out = str_out // blank_chunk

            select case(c)
            case(backslash)

                !test for unicode sequence: '\uXXXX'
                ![don't add an extra '\' for those]
                if (i+5<=len(str_in)) then
                    if (str_in(i+1:i+1)==CK_'u' .and. &
                        valid_json_hex(str_in(i+2:i+5))) then
                        str_out(ipos:ipos) = c
                        ipos = ipos + 1
                        cycle
                    end if
                end if

                str_out(ipos:ipos+1) = backslash//c
                ipos = ipos + 2

            case(quotation_mark)
                str_out(ipos:ipos+1) = backslash//c
                ipos = ipos + 2
            case(slash)
                if (escape_solidus) then
                    str_out(ipos:ipos+1) = backslash//c
                    ipos = ipos + 2
                else
                    str_out(ipos:ipos) = c
                    ipos = ipos + 1
                end if
            case(bspace)
                str_out(ipos:ipos+1) = '\b'
                ipos = ipos + 2
            case(formfeed)
                str_out(ipos:ipos+1) = '\f'
                ipos = ipos + 2
            case(newline)
                str_out(ipos:ipos+1) = '\n'
                ipos = ipos + 2
            case(carriage_return)
                str_out(ipos:ipos+1) = '\r'
                ipos = ipos + 2
            case(horizontal_tab)
                str_out(ipos:ipos+1) = '\t'
                ipos = ipos + 2
            case default
                str_out(ipos:ipos) = c
                ipos = ipos + 1
            end select

        end do

        !trim the string if necessary:
        if (ipos<len(str_out)+1) then
            if (ipos==1) then
                str_out = CK_''
            else
#if defined __GFORTRAN__
                tmp = str_out(1:ipos-1)      !workaround for bug in gfortran 6.1
                str_out = tmp
#else
                str_out = str_out(1:ipos-1)  !original
#endif
            end if
        end if

    else

        str_out = str_in

    end if

    end subroutine escape_string
!*****************************************************************************************

!*****************************************************************************************
!>
!  Remove the escape characters from a JSON string and return it.
!
!  The escaped characters are denoted by the `\` character:
!
!  * `\"`        - quotation mark
!  * `\\`        - reverse solidus
!  * `\/`        - solidus
!  * `\b`        - backspace
!  * `\f`        - formfeed
!  * `\n`        - newline (LF)
!  * `\r`        - carriage return (CR)
!  * `\t`        - horizontal tab
!  * `\uXXXX`    - 4 hexadecimal digits

    subroutine unescape_string(str, error_message)

    implicit none

    character(kind=CK,len=:),allocatable,intent(inout) :: str           !! * in: string as stored
                                                                        !!   in a [[json_value]].
                                                                        !! * out: decoded string.
    character(kind=CK,len=:),allocatable,intent(out)   :: error_message !! will be allocated if
                                                                        !! there was an error

    integer :: i   !! counter
    integer :: n   !! length of `str`
    integer :: m   !! length of `str_tmp`
    character(kind=CK,len=1) :: c  !! for scanning each character in string
    character(kind=CK,len=:),allocatable :: str_tmp !! temp decoded string (if the input
                                                    !! string contains an escape character
                                                    !! and needs to be decoded).

    if (scan(str,backslash)>0) then

        !there is at least one escape character, so process this string:

        n = len(str)
        str_tmp = repeat(space,n) !size the output string (will be trimmed later)
        m = 0  !counter in str_tmp
        i = 0  !counter in str

        do

            i = i + 1
            if (i>n) exit ! finished
            c = str(i:i) ! get next character in the string

            if (c == backslash) then

                if (i<n) then

                    i = i + 1
                    c = str(i:i) !character after the escape

                    select case(c)
                    case (quotation_mark,backslash,slash)
                        !use d as is
                        m = m + 1
                        str_tmp(m:m) = c
                    case (CK_'b')
                        c = bspace
                        m = m + 1
                        str_tmp(m:m) = c
                    case (CK_'f')
                        c = formfeed
                        m = m + 1
                        str_tmp(m:m) = c
                    case (CK_'n')
                        c = newline
                        m = m + 1
                        str_tmp(m:m) = c
                    case (CK_'r')
                        c = carriage_return
                        m = m + 1
                        str_tmp(m:m) = c
                    case (CK_'t')
                        c = horizontal_tab
                        m = m + 1
                        str_tmp(m:m) = c

                    case (CK_'u') ! expecting 4 hexadecimal digits after
                                  ! the escape character    [\uXXXX]

                        !for now, we are just returning them as is
                        ![not checking to see if it is a valid hex value]
                        !
                        ! Example:
                        !   123456
                        !   \uXXXX

                        if (i+4<=n) then

                            ! validate the hex string:
                            if (valid_json_hex(str(i+1:i+4))) then
                                m = m + 1
                                str_tmp(m:m+5) = str(i-1:i+4)
                                i = i + 4
                                m = m + 5
                            else
                                error_message = 'Error in unescape_string:'//&
                                                ' Invalid hexadecimal sequence in string "'//&
                                                trim(str)//'" ['//str(i-1:i+4)//']'
                                if (allocated(str_tmp)) deallocate(str_tmp)
                                return
                            end if
                        else
                            error_message = 'Error in unescape_string:'//&
                                            ' Invalid hexadecimal sequence in string "'//&
                                            trim(str)//'" ['//str(i-1:)//']'
                            if (allocated(str_tmp)) deallocate(str_tmp)
                            return
                        end if

                    case default

                        !unknown escape character
                        error_message = 'Error in unescape_string:'//&
                                        ' unknown escape sequence in string "'//&
                                        trim(str)//'" ['//backslash//c//']'
                        if (allocated(str_tmp)) deallocate(str_tmp)
                        return

                    end select

                else
                    ! an escape character is the last character in
                    ! the string. This is an error.
                    error_message = 'Error in unescape_string:'//&
                                    ' invalid escape character in string "'//&
                                    trim(str)//'"'
                    if (allocated(str_tmp)) deallocate(str_tmp)
                    return
                end if

            else
                m = m + 1
                str_tmp(m:m) = c
            end if

        end do

        !trim trailing space:
        str = str_tmp(1:m)

    end if

    end subroutine unescape_string
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!  date: 6/14/2014
!
!  Returns true if the string is a valid 4-digit hex string.
!
!# Examples
!```fortran
!    valid_json_hex('0000')  !returns true
!    valid_json_hex('ABC4')  !returns true
!    valid_json_hex('AB')    !returns false (< 4 characters)
!    valid_json_hex('WXYZ')  !returns false (invalid characters)
!```

    pure function valid_json_hex(str) result(valid)

    implicit none

    logical(LK)                         :: valid  !! is str a value 4-digit hex string
    character(kind=CK,len=*),intent(in) :: str    !! the string to check.

    integer(IK) :: n  !! length of `str`
    integer(IK) :: i  !! counter

    !> an array of the valid hex characters
    character(kind=CK,len=1),dimension(22),parameter :: valid_chars = &
        [ (achar(i),i=48,57), & ! decimal digits
          (achar(i),i=65,70), & ! capital A-F
          (achar(i),i=97,102) ] ! lowercase a-f

    !initialize
    valid = .false.

    !check all the characters in the string:
    n = len(str)
    if (n==4) then
        do i=1,n
            if (.not. any(str(i:i)==valid_chars)) return
        end do
        valid = .true.    !all are in the set, so it is OK
    end if

    end function valid_json_hex
!*****************************************************************************************

!*****************************************************************************************
!> author: Izaak Beekman
!
!  Convert string to unicode (CDK to CK).

    pure function to_uni(str)

    implicit none

    character(kind=CDK,len=*), intent(in) :: str
    character(kind=CK,len=len(str))       :: to_uni

    to_uni = str

    end function to_uni
!*****************************************************************************************

!*****************************************************************************************
!> author: Izaak Beekman
!
!  Convert array of strings to unicode (CDK to CK).
!
!@note JW: may be able to remove this by making [[to_uni]] PURE ELEMENTAL ?

    pure function to_uni_vec(str)

    implicit none

    character(kind=CDK,len=*), dimension(:), intent(in)   :: str
    character(kind=CK,len=len(str)), dimension(size(str)) :: to_uni_vec

    to_uni_vec = str

    end function to_uni_vec
!*****************************************************************************************

!*****************************************************************************************
!> author: Izaak Beekman
!
!  `CK`//`CDK` operator.

    pure function ucs4_join_default(ucs4_str,def_str) result(res)

    implicit none

    character(kind=CK, len=*), intent(in) :: ucs4_str
    character(kind=CDK,len=*), intent(in) :: def_str
    character(kind=CK,len=(len(ucs4_str)+len(def_str))) :: res

    res = ucs4_str//to_unicode(def_str)

    end function ucs4_join_default
!*****************************************************************************************

!*****************************************************************************************
!> author: Izaak Beekman
!
!  `CDK`//`CK` operator.

    pure function default_join_ucs4(def_str,ucs4_str) result(res)

    implicit none

    character(kind=CDK,len=*), intent(in) :: def_str
    character(kind=CK, len=*), intent(in) :: ucs4_str
    character(kind=CK,len=(len(def_str)+len(ucs4_str))) :: res

    res = to_unicode(def_str)//ucs4_str

    end function default_join_ucs4
!*****************************************************************************************

!*****************************************************************************************
!> author: Izaak Beekman
!
!  `CK`==`CDK` operator.

    pure elemental function ucs4_comp_default(ucs4_str,def_str) result(res)

    implicit none

    character(kind=CK, len=*), intent(in) :: ucs4_str
    character(kind=CDK,len=*), intent(in) :: def_str
    logical(LK) :: res

    res = ( ucs4_str == to_unicode(def_str) )

    end function ucs4_comp_default
!*****************************************************************************************

!*****************************************************************************************
!> author: Izaak Beekman
!
!  `CDK`==`CK` operator.

    pure elemental function default_comp_ucs4(def_str,ucs4_str) result(res)

    implicit none

    character(kind=CDK,len=*), intent(in) :: def_str
    character(kind=CK, len=*), intent(in) :: ucs4_str
    logical(LK) :: res

    res = (to_unicode(def_str) == ucs4_str)

    end function default_comp_ucs4
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!
!  `CK`/=`CDK` operator.

    pure elemental function ucs4_neq_default(ucs4_str,def_str) result(res)

    implicit none

    character(kind=CK, len=*), intent(in) :: ucs4_str
    character(kind=CDK,len=*), intent(in) :: def_str
    logical(LK) :: res

    res = ( ucs4_str /= to_unicode(def_str) )

    end function ucs4_neq_default
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!
!  `CDK`/=`CK` operator.

    pure elemental function default_neq_ucs4(def_str,ucs4_str) result(res)

    implicit none

    character(kind=CDK,len=*), intent(in) :: def_str
    character(kind=CK, len=*), intent(in) :: ucs4_str
    logical(LK) :: res

    res = (to_unicode(def_str) /= ucs4_str)

    end function default_neq_ucs4
!*****************************************************************************************

!*****************************************************************************************
!> author: Jacob Williams
!
!  Returns lowercase version of the `CK` string.

    pure function lowercase_string(str) result(s_lower)

    implicit none

    character(kind=CK,len=*),intent(in) :: str      !! input string
    character(kind=CK,len=(len(str)))   :: s_lower  !! lowercase version of the string

    integer :: i  !! counter
    integer :: j  !! index of uppercase character

    s_lower = str

    do i = 1, len_trim(str)
        j = index(upper,s_lower(i:i))
        if (j>0) s_lower(i:i) = lower(j:j)
    end do

    end function lowercase_string
!*****************************************************************************************

!*****************************************************************************************
!>
!  Replace all occurrences of `s1` in `str` with `s2`.
!
!  A case-sensitive match is used.
!
!@note `str` must be allocated.

    pure subroutine replace_string(str,s1,s2)

    implicit none

    character(kind=CK,len=:),allocatable,intent(inout) :: str
    character(kind=CK,len=*),intent(in) :: s1
    character(kind=CK,len=*),intent(in) :: s2

    character(kind=CK,len=:),allocatable :: tmp  !! temporary string for accumulating result
    integer(IK) :: i      !! counter
    integer(IK) :: n      !! for accumulating the string
    integer(IK) :: ilen   !! length of `str` string
    integer(IK) :: ilen1  !! length of `s1` string

    if (len(str)>0) then

        tmp = CK_''  ! initialize
        ilen1 = len(s1)

        !     .
        ! '123ab789'

        do
            ilen = len(str)
            i = index(str,s1)
            if (i>0) then
                if (i>1) tmp = tmp//str(1:i-1)
                tmp = tmp//s2 ! replace s1 with s2 in new string
                n = i+ilen1   ! start of remainder of str to keep
                if (n<=ilen) then
                    str = str(n:ilen)
                else
                    ! done
                    exit
                end if
            else
                ! done: get remainder of string
                tmp = tmp//str
                exit
            end if
        end do

        str = tmp

    end if

    end subroutine replace_string
!*****************************************************************************************

!*****************************************************************************************
!>
!  Decode a string from the "JSON Pointer" RFC 6901 format.
!
!  It replaces `~1` with `/` and `~0` with `~`.

    pure function decode_rfc6901(str) result(str_out)

    implicit none

    character(kind=CK,len=*),intent(in) :: str
    character(kind=CK,len=:),allocatable :: str_out

    str_out = str

    call replace_string(str_out,tilde//CK_'1',slash)
    call replace_string(str_out,tilde//CK_'0',tilde)

    end function decode_rfc6901
!*****************************************************************************************

!*****************************************************************************************
!>
!  Encode a string into the "JSON Pointer" RFC 6901 format.
!
!  It replaces `~` with `~0` and `/` with `~1`.

    pure function encode_rfc6901(str) result(str_out)

    implicit none

    character(kind=CK,len=*),intent(in) :: str
    character(kind=CK,len=:),allocatable :: str_out

    str_out = str

    call replace_string(str_out,tilde,tilde//CK_'0')
    call replace_string(str_out,slash,tilde//CK_'1')

    end function encode_rfc6901
!*****************************************************************************************

    end module json_string_utilities
!*****************************************************************************************
