;;; Arl Standard Library - String Helpers

(module strings
  (export string-join string-split trim string-format string-contains? string-match?
          string-find string-replace string-replace-all string-append ->string
          char-at string-ref
          string-slice string-length
          string-upcase string-downcase string-titlecase
          string<? string>? string=? string<=? string>=?
          string->list list->string
          number->string string->number
          string-prefix? string-suffix? string-empty? string-repeat)

  (import math :refer (quotient))
  (import core :refer (r-call))

  ;;' @section String Operations
  ;;' Core string manipulation functions: joining, splitting, searching,
  ;;' replacing, and accessing characters. Most search/replace functions
  ;;' default to fixed (literal) matching; pass `:fixed #f` for regex.

  ;;' @description Join strings with separator.
  ;;' @param x A list of strings to join.
  ;;' @param sep Separator inserted between elements (default "").
  ;;' @examples
  ;;' (string-join (list "a" "b" "c") "-")  ; => "a-b-c"
  ;;' (string-join (list "x" "y" "z"))      ; => "xyz"
  ;;' (string-join (list "hello" "world") " ")  ; => "hello world"
  ;;' @assert
  ;;' (assert-equal "a-b-c" (string-join (list "a" "b" "c") "-"))
  ;;' (assert-equal "xyz" (string-join (list "x" "y" "z")))
  ;;' (assert-equal "hello world" (string-join (list "hello" "world") " "))
  ;;' @seealso string-split, string-format
  (define string-join
    (lambda (x (sep ""))
      (paste (unlist (_as-list x)) :collapse sep)))

  ;;' @description Split string on separator.
  ;;' @param x The string to split.
  ;;' @param sep Separator to split on (default ""), splitting into characters.
  ;;' @examples
  ;;' (string-split "a-b-c" "-")  ; => ("a" "b" "c")
  ;;' (string-split "hello" "")   ; => ("h" "e" "l" "l" "o")
  ;;' @assert
  ;;' (assert-equal (list "a" "b" "c") (string-split "a-b-c" "-"))
  ;;' (assert-equal (list "h" "e" "l" "l" "o") (string-split "hello" ""))
  ;;' @seealso string-join
  (define string-split
    (lambda (x (sep ""))
      (as.list (unname (r-call "[[" (list (strsplit x :split sep :fixed #t) 1))))))

  ;; DESIGN NOTE: Simple one-argument pass-through functions use direct
  ;; assignment to avoid unnecessary stack frames.

  ;;' @description Trim leading and trailing whitespace.
  ;;' @signature (trim str)
  ;;' @param str The string to trim.
  ;;' @examples
  ;;' (trim "  hello  ")  ; => "hello"
  ;;' (trim "no spaces")  ; => "no spaces"
  ;;' @assert
  ;;' (assert-equal "hello" (trim "  hello  "))
  ;;' (assert-equal "no spaces" (trim "no spaces"))
  (define trim trimws)

  ;;' @description Format string with sprintf.
  ;;' @param fmt A sprintf-style format string.
  ;;' @param args Values to substitute into the format placeholders.
  ;;' @examples
  ;;' (string-format "Hello, %s!" "world")   ; => "Hello, world!"
  ;;' (string-format "%d + %d = %d" 1 2 3)   ; => "1 + 2 = 3"
  ;;' (string-format "%.2f" 3.14159)          ; => "3.14"
  ;;' @assert
  ;;' (assert-equal "Hello, world!" (string-format "Hello, %s!" "world"))
  ;;' (assert-equal "1 + 2 = 3" (string-format "%d + %d = %d" 1 2 3))
  ;;' (assert-equal "3.14" (string-format "%.2f" 3.14159))
  ;;' @seealso string-concat
  (define string-format
    (lambda (fmt . args)
      (r-call "sprintf" (cons fmt args))))

  ;;' @description Check pattern in string (fixed).
  ;;' @param str The string to search in.
  ;;' @param pattern The pattern to search for.
  ;;' @param fixed If #t (default), match literally; if #f, treat as regex.
  ;;' @examples
  ;;' (string-contains? "hello world" "world")       ; => #t
  ;;' (string-contains? "hello world" "xyz")          ; => #f
  ;;' (string-contains? "hello world" "o.*d" :fixed #f)  ; => #t
  ;;' @assert
  ;;' (assert-true (string-contains? "hello world" "world"))
  ;;' (assert-false (string-contains? "hello world" "xyz"))
  ;;' (assert-true (string-contains? "hello world" "o.*d" :fixed #f))
  ;;' @seealso string-match?, string-find
  (define string-contains?
    (lambda (str pattern (fixed #t))
      (isTRUE (grepl pattern str :fixed fixed))))

  ;;' @description Check pattern in string (regex).
  ;;' @param str The string to search in.
  ;;' @param pattern The pattern to search for.
  ;;' @param fixed If #f (default), treat as regex; if #t, match literally.
  ;;' @examples
  ;;' (string-match? "hello" "^h.*o$" :fixed #f)  ; => #t
  ;;' (string-match? "hello" "xyz" :fixed #f)      ; => #f
  ;;' (string-match? "hello" "hello" :fixed #t)     ; => #t
  ;;' @assert
  ;;' (assert-true (string-match? "hello" "^h.*o$" :fixed #f))
  ;;' (assert-false (string-match? "hello" "xyz" :fixed #f))
  ;;' (assert-true (string-match? "hello" "hello" :fixed #t))
  ;;' @seealso string-contains?, string-find
  (define string-match?
    (lambda (str pattern (fixed #f))
      (isTRUE (grepl pattern str :fixed fixed))))

  ;;' @description Find pattern index or #nil.
  ;;' @param str The string to search in.
  ;;' @param pattern The pattern to find.
  ;;' @param fixed If #t (default), match literally; if #f, treat as regex.
  ;;' @examples
  ;;' (string-find "hello world" "world")  ; => 6
  ;;' (string-find "hello world" "xyz")    ; => #nil
  ;;' (string-find "hello" "l")            ; => 2
  ;;' @assert
  ;;' (assert-equal 6 (string-find "hello world" "world"))
  ;;' (assert-equal #nil (string-find "hello world" "xyz"))
  ;;' (assert-equal 2 (string-find "hello" "l"))
  ;;' @seealso string-contains?, string-match?
  (define string-find
    (lambda (str pattern (fixed #t))
      (define match (regexpr pattern str :fixed fixed))
      (define pos (r-call "[[" (list match 1)))
      (if (< pos 0) #nil (- pos 1))))

  ;;' @description Replace first match. Default is literal (fixed); pass :fixed #f for regex.
  ;;' @param str The string to modify.
  ;;' @param pattern The pattern to match.
  ;;' @param replacement The replacement string.
  ;;' @param fixed If #t (default), match literally; if #f, treat as regex.
  ;;' @examples
  ;;' (string-replace "hello world" "world" "there")    ; => "hello there"
  ;;' (string-replace "aaa" "a" "b")                     ; => "baa"
  ;;' @assert
  ;;' (assert-equal "hello there" (string-replace "hello world" "world" "there"))
  ;;' (assert-equal "baa" (string-replace "aaa" "a" "b"))
  ;;' @seealso string-replace-all
  (define string-replace
    (lambda (str pattern replacement (fixed #t))
      (sub pattern replacement str :fixed fixed)))

  ;;' @description Replace all matches. Default is literal (fixed); pass :fixed #f for regex.
  ;;' @param str The string to modify.
  ;;' @param pattern The pattern to match.
  ;;' @param replacement The replacement string.
  ;;' @param fixed If #t (default), match literally; if #f, treat as regex.
  ;;' @examples
  ;;' (string-replace-all "aaa" "a" "b")   ; => "bbb"
  ;;' (string-replace-all "hello world world" "world" "there")  ; => "hello there there"
  ;;' @assert
  ;;' (assert-equal "bbb" (string-replace-all "aaa" "a" "b"))
  ;;' (assert-equal "hello there there" (string-replace-all "hello world world" "world" "there"))
  ;;' @seealso string-replace
  (define string-replace-all
    (lambda (str pattern replacement (fixed #t))
      (gsub pattern replacement str :fixed fixed)))

  ;;' @description Concatenate strings together.
  ;;' @param args Zero or more strings to concatenate.
  ;;' @examples
  ;;' (string-append "hello" " " "world")  ; => "hello world"
  ;;' (string-append "a" "b" "c")          ; => "abc"
  ;;' (string-append)                       ; => ""
  ;;' @assert
  ;;' (assert-equal "hello world" (string-append "hello" " " "world"))
  ;;' (assert-equal "abc" (string-append "a" "b" "c"))
  ;;' (assert-equal "" (string-append))
  ;;' @seealso string-concat, string-join
  (define string-append
    (lambda (. args)
      (if (r-call "==" (list (length args) 0))
        ""
        (r-call "paste0" args))))

  ;;' @description Convert value to string representation.
  ;;' @signature (->string x)
  ;;' @param x The value to convert to a string.
  ;;' @examples
  ;;' (->string 42)     ; => "42"
  ;;' (->string #t)     ; => "TRUE"
  ;;' (->string 3.14)   ; => "3.14"
  ;;' @assert
  ;;' (assert-equal "42" (->string 42))
  ;;' (assert-equal "TRUE" (->string #t))
  ;;' (assert-equal "3.14" (->string 3.14))
  (define ->string as.character)

  ;; ==========================================================================
  ;; Phase 3: Extended String Operations
  ;; ==========================================================================

  ;; Character access
  ;;' @description Get character at index (0-based).
  ;;' @param str The string to index into.
  ;;' @param index Zero-based character position.
  ;;' @examples
  ;;' (char-at "hello" 0)  ; => "h"
  ;;' (char-at "hello" 4)  ; => "o"
  ;;' @assert
  ;;' (assert-equal "h" (char-at "hello" 0))
  ;;' (assert-equal "o" (char-at "hello" 4))
  ;;' @seealso string-ref, string-slice
  (define char-at
    (lambda (str index)
      (if (< index 0)
        (stop (sprintf "Index %d out of bounds" index))
        (if (>= index (nchar str))
          (stop (sprintf "Index %d out of bounds for string of length %d" index (nchar str)))
          (substr str (+ index 1) (+ index 1))))))

  ;;' @description Scheme-style character accessor (alias for char-at).
  ;;' @param str The string to index into.
  ;;' @param index Zero-based character position.
  ;;' @examples
  ;;' (string-ref "hello" 0)    ; => "h"
  ;;' (string-ref "hello" 4)    ; => "o"
  ;;' @assert
  ;;' (assert-equal "h" (string-ref "hello" 0))
  ;;' (assert-equal "o" (string-ref "hello" 4))
  ;;' @seealso char-at
  (define string-ref
    (lambda (str index)
      (char-at str index)))

  ;; Substring
  ;;' @description Extract substring from start (inclusive) to end (exclusive), 0-based.
  ;;' @param str The string to slice.
  ;;' @param start Zero-based start index (inclusive).
  ;;' @param end Zero-based end index (exclusive).
  ;;' @examples
  ;;' (string-slice "hello" 1 4)  ; => "ell"
  ;;' (string-slice "hello" 0 5)  ; => "hello"
  ;;' (string-slice "hello" 2 2)  ; => ""
  ;;' @assert
  ;;' (assert-equal "ell" (string-slice "hello" 1 4))
  ;;' (assert-equal "hello" (string-slice "hello" 0 5))
  ;;' (assert-equal "" (string-slice "hello" 2 2))
  ;;' @seealso char-at
  (define string-slice
    (lambda (str start end)
      (if (< start 0)
        (stop "string-slice: start index cannot be negative")
        (if (> end (nchar str))
          (stop (sprintf "string-slice: end index %d exceeds string length %d" end (nchar str)))
          (if (> start end)
            (stop "string-slice: start index must be <= end index")
            (if (== start end)
              ""
              (substr str (+ start 1) end)))))))

  ;; String length
  ;;' @description Return length of string.
  ;;' @param str The string to measure.
  ;;' @examples
  ;;' (string-length "hello")  ; => 5
  ;;' (string-length "")       ; => 0
  ;;' @assert
  ;;' (assert-equal 5 (string-length "hello"))
  ;;' (assert-equal 0 (string-length ""))
  (define string-length
    (lambda (str)
      (nchar str)))

  ;;' @section String Case
  ;;' Functions for converting string case.

  ;;' @description Convert string to uppercase.
  ;;' @param str The string to convert.
  ;;' @examples
  ;;' (string-upcase "hello")  ; => "HELLO"
  ;;' (string-upcase "Hello World")  ; => "HELLO WORLD"
  ;;' @assert
  ;;' (assert-equal "HELLO" (string-upcase "hello"))
  ;;' (assert-equal "HELLO WORLD" (string-upcase "Hello World"))
  ;;' @seealso string-downcase, string-titlecase
  (define string-upcase
    (lambda (str)
      (toupper str)))

  ;;' @description Convert string to lowercase.
  ;;' @param str The string to convert.
  ;;' @examples
  ;;' (string-downcase "HELLO")  ; => "hello"
  ;;' (string-downcase "Hello World")  ; => "hello world"
  ;;' @assert
  ;;' (assert-equal "hello" (string-downcase "HELLO"))
  ;;' (assert-equal "hello world" (string-downcase "Hello World"))
  ;;' @seealso string-upcase, string-titlecase
  (define string-downcase
    (lambda (str)
      (tolower str)))

  ;;' @description Convert string to title case (capitalize first letter of each word).
  ;;' @param str The string to convert.
  ;;' @examples
  ;;' (string-titlecase "hello world")  ; => "Hello World"
  ;;' (string-titlecase "HELLO WORLD")  ; => "Hello World"
  ;;' @assert
  ;;' (assert-equal "Hello World" (string-titlecase "hello world"))
  ;;' (assert-equal "Hello World" (string-titlecase "HELLO WORLD"))
  ;;' @seealso string-upcase, string-downcase
  (define string-titlecase
    (lambda (str)
      ;; Simple implementation: split on spaces, capitalize each word
      (begin
        (define words (r-call "[[" (list (strsplit str " ") 1)))
        (define capitalize-word
          (lambda (word)
            (if (== (nchar word) 0)
              word
              (paste0 (toupper (substr word 1 1))
                     (tolower (substr word 2 (nchar word)))))))
        (paste (vapply words capitalize-word (character 1)) :collapse " "))))

  ;;' @section String Comparison
  ;;' Lexicographic comparison functions for strings.

  ;;' @description Lexicographic less-than comparison.
  ;;' @param a First string.
  ;;' @param b Second string.
  ;;' @examples
  ;;' (string<? "abc" "def")  ; => #t
  ;;' (string<? "def" "abc")  ; => #f
  ;;' (string<? "abc" "abc")  ; => #f
  ;;' @assert
  ;;' (assert-true (string<? "abc" "def"))
  ;;' (assert-false (string<? "def" "abc"))
  ;;' (assert-false (string<? "abc" "abc"))
  (define string<?
    (lambda (a b)
      (isTRUE (< a b))))

  ;;' @description Lexicographic greater-than comparison.
  ;;' @param a First string.
  ;;' @param b Second string.
  ;;' @examples
  ;;' (string>? "def" "abc")  ; => #t
  ;;' (string>? "abc" "def")  ; => #f
  ;;' (string>? "abc" "abc")  ; => #f
  ;;' @assert
  ;;' (assert-true (string>? "def" "abc"))
  ;;' (assert-false (string>? "abc" "def"))
  ;;' (assert-false (string>? "abc" "abc"))
  (define string>?
    (lambda (a b)
      (isTRUE (> a b))))

  ;;' @description String equality comparison.
  ;;' @param a First string.
  ;;' @param b Second string.
  ;;' @examples
  ;;' (string=? "hello" "hello")  ; => #t
  ;;' (string=? "hello" "world")  ; => #f
  ;;' @assert
  ;;' (assert-true (string=? "hello" "hello"))
  ;;' (assert-false (string=? "hello" "world"))
  (define string=?
    (lambda (a b)
      (isTRUE (== a b))))

  ;;' @description Lexicographic less-than-or-equal comparison.
  ;;' @param a First string.
  ;;' @param b Second string.
  ;;' @examples
  ;;' (string<=? "abc" "abc")  ; => #t
  ;;' (string<=? "abc" "def")  ; => #t
  ;;' (string<=? "def" "abc")  ; => #f
  ;;' @assert
  ;;' (assert-true (string<=? "abc" "abc"))
  ;;' (assert-true (string<=? "abc" "def"))
  ;;' (assert-false (string<=? "def" "abc"))
  (define string<=?
    (lambda (a b)
      (isTRUE (<= a b))))

  ;;' @description Lexicographic greater-than-or-equal comparison.
  ;;' @param a First string.
  ;;' @param b Second string.
  ;;' @examples
  ;;' (string>=? "abc" "abc")  ; => #t
  ;;' (string>=? "def" "abc")  ; => #t
  ;;' (string>=? "abc" "def")  ; => #f
  ;;' @assert
  ;;' (assert-true (string>=? "abc" "abc"))
  ;;' (assert-true (string>=? "def" "abc"))
  ;;' (assert-false (string>=? "abc" "def"))
  (define string>=?
    (lambda (a b)
      (isTRUE (>= a b))))

  ;;' @section String/List Conversion
  ;;' Functions for converting between strings, lists, and numbers.

  ;;' @description Convert string to list of characters.
  ;;' @param str The string to convert.
  ;;' @examples
  ;;' (string->list "hello")  ; => ("h" "e" "l" "l" "o")
  ;;' (string->list "")       ; => ()
  ;;' @assert
  ;;' (assert-equal (list "h" "e" "l" "l" "o") (string->list "hello"))
  ;;' (assert-equal (list) (string->list ""))
  ;;' @seealso list->string
  (define string->list
    (lambda (str)
      (if (== (nchar str) 0)
        (list)
        (as.list (r-call "[[" (list (strsplit str "") 1))))))

  ;;' @description Convert list of characters to string.
  ;;' @param lst A list of single-character strings.
  ;;' @examples
  ;;' (list->string (list "h" "e" "l" "l" "o"))  ; => "hello"
  ;;' (list->string (list))                        ; => ""
  ;;' @assert
  ;;' (assert-equal "hello" (list->string (list "h" "e" "l" "l" "o")))
  ;;' (assert-equal "" (list->string (list)))
  ;;' @seealso string->list
  (define list->string
    (lambda (lst)
      (paste (unlist (_as-list lst)) :collapse "")))

  ;; Number/String conversions
  ;;' @description Convert number to string with optional base (2-36).
  ;;' @param num The number to convert.
  ;;' @param base-args Optional base for conversion (2, 8, 10, or 16; default 10).
  ;;' @examples
  ;;' (number->string 42)       ; => "42"
  ;;' (number->string 255 16)   ; => "ff"
  ;;' (number->string 10 2)     ; => "1010"
  ;;' (number->string 8 8)      ; => "10"
  ;;' @assert
  ;;' (assert-equal "42" (number->string 42))
  ;;' (assert-equal "ff" (number->string 255 16))
  ;;' (assert-equal "1010" (number->string 10 2))
  ;;' (assert-equal "10" (number->string 8 8))
  ;;' @seealso string->number
  (define number->string
    (lambda (num . base-args)
      (if (> (length base-args) 0)
        (begin
          (define base (car base-args))
          (if (if (< base 2) #t (> base 36))
            (stop "number->string: base must be between 2 and 36")
            (if (== base 10)
              (as.character num)
              (if (== base 16)
                (tolower (r-call "as.hexmode" (list (as.integer num))))
                (if (== base 8)
                  (as.character (r-call "as.octmode" (list (as.integer num))))
                  (if (== base 2)
                    ;; Binary conversion
                    (begin
                      (define n (as.integer num))
                      (if (== n 0)
                        "0"
                        (begin
                          (define binary-digits (list))
                          (define temp n)
                          (define make-binary
                            (lambda (x)
                              (if (> x 0)
                                (begin
                                  (set! binary-digits (cons (as.character (%% x 2)) binary-digits))
                                  (make-binary (quotient x 2)))
                                #nil)))
                          (make-binary temp)
                          (paste (unlist binary-digits) :collapse ""))))
                    ;; Other bases - use R's built-in
                    (stop (sprintf "number->string: base %d not yet implemented" base))))))))
        (as.character num))))

  ;;' @description Parse string to number with optional base (2-36).
  ;;' @param str The string to parse.
  ;;' @param base-args Optional base for parsing (2, 8, 10, or 16; default 10).
  ;;' @examples
  ;;' (string->number "42")       ; => 42
  ;;' (string->number "3.14")     ; => 3.14
  ;;' (string->number "ff" 16)    ; => 255
  ;;' (string->number "1010" 2)   ; => 10
  ;;' (string->number "bad")      ; => #f
  ;;' @assert
  ;;' (assert-equal 42 (string->number "42"))
  ;;' (assert-equal 3.14 (string->number "3.14"))
  ;;' (assert-equal 255 (string->number "ff" 16))
  ;;' (assert-equal 10 (string->number "1010" 2))
  ;;' (assert-false (string->number "bad"))
  ;;' @seealso number->string
  (define string->number
    (lambda (str . base-args)
      (if (> (length base-args) 0)
        (begin
          (define base (car base-args))
          (if (if (< base 2) #t (> base 36))
            (stop "string->number: base must be between 2 and 36")
            (begin
              ;; Handle different bases
              (if (== base 10)
                (begin
                  (define result (suppressWarnings (as.numeric str)))
                  (if (is.na result) #f result))
                (if (== base 16)
                  (begin
                    (define result (suppressWarnings (as.integer (paste0 "0x" str))))
                    (if (is.na result) #f result))
                  (if (== base 8)
                    (begin
                      (define result (suppressWarnings (as.integer (paste0 "0" str))))
                      (if (is.na result) #f result))
                    (if (== base 2)
                      ;; Binary parsing
                      (begin
                        (define chars (r-call "[[" (list (strsplit str "") 1)))
                        (define parse-binary
                          (lambda (digits acc)
                            (if (== (length digits) 0)
                              acc
                              (begin
                                (define d (r-call "[[" (list digits 1)))
                                (define rest (if (> (length digits) 1)
                                              (r-call "[" (list digits (seq 2 (length digits))))
                                              (list)))
                                (if (if (== d "0") #t (== d "1"))
                                  (parse-binary rest (+ (* acc 2) (as.integer d)))
                                  #f)))))
                        (parse-binary chars 0))
                      ;; Other bases not implemented
                      (stop (sprintf "string->number: base %d not yet implemented" base)))))))))
        (begin
          (define result (suppressWarnings (as.numeric str)))
          (if (is.na result) #f result)))))

  ;; ============================================================================
  ;; String Predicates and Utilities
  ;; ============================================================================

  ;;' @section String Predicates
  ;;' Common string predicates for prefix, suffix, and emptiness checks.

  ;;' @description Return #t if str starts with prefix.
  ;;' @param prefix String to check for at the start
  ;;' @param str String to check
  ;;' @examples
  ;;' (string-prefix? "he" "hello")  ; => #t
  ;;' (string-prefix? "wo" "hello")  ; => #f
  ;;' @assert
  ;;' (assert-true (string-prefix? "he" "hello"))
  ;;' (assert-false (string-prefix? "wo" "hello"))
  ;;' @seealso string-suffix?, string-contains?
  (define string-prefix?
    (lambda (prefix str)
      (isTRUE (startsWith str prefix))))

  ;;' @description Return #t if str ends with suffix.
  ;;' @param suffix String to check for at the end
  ;;' @param str String to check
  ;;' @examples
  ;;' (string-suffix? "lo" "hello")  ; => #t
  ;;' (string-suffix? "he" "hello")  ; => #f
  ;;' @assert
  ;;' (assert-true (string-suffix? "lo" "hello"))
  ;;' (assert-false (string-suffix? "he" "hello"))
  ;;' @seealso string-prefix?, string-contains?
  (define string-suffix?
    (lambda (suffix str)
      (isTRUE (endsWith str suffix))))

  ;;' @description Return #t if str is the empty string.
  ;;' @param str String to check
  ;;' @examples
  ;;' (string-empty? "")   ; => #t
  ;;' (string-empty? "a")  ; => #f
  ;;' @assert
  ;;' (assert-true (string-empty? ""))
  ;;' (assert-false (string-empty? "a"))
  ;;' @seealso string-length
  (define string-empty?
    (lambda (str)
      (isTRUE (== (nchar str) 0))))

  ;;' @description Repeat string n times.
  ;;' @param str String to repeat
  ;;' @param n Number of repetitions
  ;;' @examples
  ;;' (string-repeat "ab" 3)  ; => "ababab"
  ;;' (string-repeat "x" 0)   ; => ""
  ;;' @assert
  ;;' (assert-equal "ababab" (string-repeat "ab" 3))
  ;;' (assert-equal "" (string-repeat "x" 0))
  ;;' @seealso string-append
  (define string-repeat
    (lambda (str n)
      (strrep str n)))

)
