-- | A built-in representation for efficient string manipulation.
-- @Text@ values are /not/ lists of characters.
module Text
  ( -- * Text
    Text,
    isEmpty,
    length,
    reverse,
    repeat,
    replace,

    -- * Building and Splitting
    append,
    concat,
    split,
    join,
    words,
    lines,

    -- * Get Substrings
    slice,
    left,
    right,
    dropLeft,
    dropRight,

    -- * Check for Substrings
    contains,
    startsWith,
    endsWith,
    indexes,
    indices,

    -- * Int Conversions
    toInt,
    fromInt,

    -- * Float Conversions
    toFloat,
    fromFloat,

    -- * Char Conversions
    fromChar,
    cons,
    uncons,

    -- * List Conversions
    toList,
    fromList,

    -- * Formatting

    -- | Cosmetic operations such as padding with extra characters or trimming whitespace.
    toUpper,
    toLower,
    pad,
    padLeft,
    padRight,
    trim,
    trimLeft,
    trimRight,

    -- * Higher-Order Functions
    map,
    filter,
    foldl,
    foldr,
    any,
    all,
  )
where

import Basics
  ( (+),
    (-),
    (<),
    (<<),
    (<=),
    (>>),
    Bool,
    Float,
    Int,
    clamp,
    (|>),
  )
import Char (Char)
import qualified Data.Text
import qualified List
import List (List)
import Maybe (Maybe)
import qualified Text.Read
import Prelude (otherwise)
import qualified Prelude

-- | A @Text@ is a chunk of text:
--
-- > "Hello!"
-- > "How are you?"
-- > "🙈🙉🙊"
-- >
-- > -- strings with escape characters
-- > "this\n\t\"that\""
-- > "\x1F648\x1F649\x1F64A" -- "🙈🙉🙊"
--
-- A @Text@ can represent any sequence of [unicode characters](https://en.wikipedia.org/wiki/Unicode). You can use the unicode escapes from @\x0000@ to @\x10FFFF@ to represent characters by their code point. You can also include the unicode characters directly. Using the escapes can be better if you need one of the many whitespace characters with different widths.
type Text = Data.Text.Text

-- | Determine if a string is empty.
--
-- > isEmpty "" == True
-- > isEmpty "the world" == False
isEmpty :: Text -> Bool
isEmpty = Data.Text.null

-- | Get the length of a string.
--
-- > length "innumerable" == 11
-- > length "" == 0
length :: Text -> Int
length =
  Data.Text.length >> Prelude.fromIntegral

-- | Reverse a string.
--
-- > reverse "stressed" == "desserts"
reverse :: Text -> Text
reverse = Data.Text.reverse

-- | Repeat a string /n/ times.
--
-- > repeat 3 "ha" == "hahaha"
repeat :: Int -> Text -> Text
repeat =
  Prelude.fromIntegral >> Data.Text.replicate

-- | Replace all occurrences of some substring.
--
-- > replace "." "-" "Json.Decode.succeed" == "Json-Decode-succeed"
-- > replace "," "/" "a,b,c,d,e"           == "a/b/c/d/e"
replace :: Text -> Text -> Text -> Text
replace = Data.Text.replace

-- BUILDING AND SPLITTING

-- | Append two strings. You can also use the @(++)@ operator to do this.
--
-- > append "butter" "fly" == "butterfly"
append :: Text -> Text -> Text
append = Data.Text.append

-- | Concatenate many strings into one.
--
-- > concat ["never","the","less"] == "nevertheless"
concat :: List Text -> Text
concat = Data.Text.concat

-- | Split a string using a given separator.
--
-- > split "," "cat,dog,cow"        == ["cat","dog","cow"]
-- > split "/" "home/evan/Desktop/" == ["home","evan","Desktop", ""]
split :: Text -> Text -> List Text
split = Data.Text.splitOn

-- | Put many strings together with a given separator.
--
-- > join "a" ["H","w","ii","n"]        == "Hawaiian"
-- > join " " ["cat","dog","cow"]       == "cat dog cow"
-- > join "/" ["home","evan","Desktop"] == "home/evan/Desktop"
join :: Text -> List Text -> Text
join = Data.Text.intercalate

-- | Break a string into words, splitting on chunks of whitespace.
--
-- > words "How are \t you? \n Good?" == ["How","are","you?","Good?"]
words :: Text -> List Text
words = Data.Text.words

-- | Break a string into lines, splitting on newlines.
--
-- > lines "How are you?\nGood?" == ["How are you?", "Good?"]
lines :: Text -> List Text
lines = Data.Text.lines

-- SUBSTRINGS

-- | Take a substring given a start and end index. Negative indexes
-- are taken starting from the /end/ of the list.
--
-- > slice  7  9 "snakes on a plane!" == "on"
-- > slice  0  6 "snakes on a plane!" == "snakes"
-- > slice  0 -7 "snakes on a plane!" == "snakes on a"
-- > slice -6 -1 "snakes on a plane!" == "plane"
slice :: Int -> Int -> Text -> Text
slice from to text
  | to' - from' <= 0 = Data.Text.empty
  | otherwise =
    Data.Text.drop from' (Data.Text.take to' text)
  where
    len = Data.Text.length text
    handleNegative value
      | value < 0 = len + value
      | otherwise = value
    normalize =
      Prelude.fromIntegral
        >> handleNegative
        >> clamp 0 len
    from' = normalize from
    to' = normalize to

-- | Take /n/ characters from the left side of a string.
--
-- > left 2 "Mulder" == "Mu"
left :: Int -> Text -> Text
left =
  Prelude.fromIntegral >> Data.Text.take

-- | Take /n/ characters from the right side of a string.
--
-- > right 2 "Scully" == "ly"
right :: Int -> Text -> Text
right =
  Prelude.fromIntegral >> Data.Text.takeEnd

-- | Drop /n/ characters from the left side of a string.
--
-- > dropLeft 2 "The Lone Gunmen" == "e Lone Gunmen"
dropLeft :: Int -> Text -> Text
dropLeft =
  Prelude.fromIntegral >> Data.Text.drop

-- | Drop /n/ characters from the right side of a string.
--
-- > dropRight 2 "Cigarette Smoking Man" == "Cigarette Smoking M"
dropRight :: Int -> Text -> Text
dropRight =
  Prelude.fromIntegral >> Data.Text.dropEnd

-- DETECT SUBSTRINGS

-- | See if the second string contains the first one.
--
-- > contains "the" "theory" == True
-- > contains "hat" "theory" == False
-- > contains "THE" "theory" == False
contains :: Text -> Text -> Bool
contains = Data.Text.isInfixOf

-- | See if the second string starts with the first one.
--
-- > startsWith "the" "theory" == True
-- > startsWith "ory" "theory" == False
startsWith :: Text -> Text -> Bool
startsWith = Data.Text.isPrefixOf

-- | See if the second string ends with the first one.
--
-- > endsWith "the" "theory" == False
-- > endsWith "ory" "theory" == True
endsWith :: Text -> Text -> Bool
endsWith = Data.Text.isSuffixOf

-- | Get all of the indexes for a substring in another string.
--
-- > indexes "i" "Mississippi"   == [1,4,7,10]
-- > indexes "ss" "Mississippi"  == [2,5]
-- > indexes "needle" "haystack" == []
indexes :: Text -> Text -> List Int
indexes n h
  | isEmpty n = []
  | otherwise = indexes' n h
  where
    indexes' needle haystack =
      Data.Text.breakOnAll needle haystack
        |> List.map
          ( \(lhs, _) ->
              Data.Text.length lhs
                |> Prelude.fromIntegral
          )

-- | Alias for @indexes@.
indices :: Text -> Text -> List Int
indices = indexes

-- FORMATTING

-- | Convert a string to all upper case. Useful for case-insensitive comparisons
-- and VIRTUAL YELLING.
--
-- > toUpper "skinner" == "SKINNER"
toUpper :: Text -> Text
toUpper = Data.Text.toUpper

-- | Convert a string to all lower case. Useful for case-insensitive comparisons.
--
-- > toLower "X-FILES" == "x-files"
toLower :: Text -> Text
toLower = Data.Text.toLower

-- | Pad a string on both sides until it has a given length.
--
-- > pad 5 ' ' "1"   == "  1  "
-- > pad 5 ' ' "11"  == "  11 "
-- > pad 5 ' ' "121" == " 121 "
pad :: Int -> Char -> Text -> Text
pad =
  Prelude.fromIntegral >> Data.Text.center

-- | Pad a string on the left until it has a given length.
--
-- > padLeft 5 '.' "1"   == "....1"
-- > padLeft 5 '.' "11"  == "...11"
-- > padLeft 5 '.' "121" == "..121"
padLeft :: Int -> Char -> Text -> Text
padLeft =
  Prelude.fromIntegral >> Data.Text.justifyRight

-- | Pad a string on the right until it has a given length.
--
-- > padRight 5 '.' "1"   == "1...."
-- > padRight 5 '.' "11"  == "11..."
-- > padRight 5 '.' "121" == "121.."
padRight :: Int -> Char -> Text -> Text
padRight =
  Prelude.fromIntegral >> Data.Text.justifyLeft

-- | Get rid of whitespace on both sides of a string.
--
-- > trim "  hats  \n" == "hats"
trim :: Text -> Text
trim = Data.Text.strip

-- | Get rid of whitespace on the left of a string.
--
-- > trimLeft "  hats  \n" == "hats  \n"
trimLeft :: Text -> Text
trimLeft = Data.Text.stripStart

-- | Get rid of whitespace on the right of a string.
--
-- > trimRight "  hats  \n" == "  hats"
trimRight :: Text -> Text
trimRight = Data.Text.stripEnd

-- INT CONVERSIONS

-- | Try to convert a string into an int, failing on improperly formatted strings.
--
-- > Text.toInt "123" == Just 123
-- > Text.toInt "-42" == Just -42
-- > Text.toInt "3.1" == Nothing
-- > Text.toInt "31a" == Nothing
--
-- If you are extracting a number from some raw user input, you will typically
-- want to use [@Maybe.withDefault@](Maybe#withDefault) to handle bad data:
--
-- > Maybe.withDefault 0 (Text.toInt "42") == 42
-- > Maybe.withDefault 0 (Text.toInt "ab") == 0
toInt :: Text -> Maybe Int
toInt text =
  Text.Read.readMaybe str'
  where
    str = Data.Text.unpack text
    str' = case str of
      '+' : rest -> rest
      other -> other

-- | Convert an @Int@ to a @Text@.
--
-- > Text.fromInt 123 == "123"
-- > Text.fromInt -42 == "-42"
fromInt :: Int -> Text
fromInt = Data.Text.pack << Prelude.show

-- FLOAT CONVERSIONS

-- | Try to convert a string into a float, failing on improperly formatted strings.
--
-- > Text.toFloat "123" == Just 123.0
-- > Text.toFloat "-42" == Just -42.0
-- > Text.toFloat "3.1" == Just 3.1
-- > Text.toFloat "31a" == Nothing
--
-- If you are extracting a number from some raw user input, you will typically
-- want to use [@Maybe.withDefault@](Maybe#withDefault) to handle bad data:
--
-- > Maybe.withDefault 0 (Text.toFloat "42.5") == 42.5
-- > Maybe.withDefault 0 (Text.toFloat "cats") == 0
toFloat :: Text -> Maybe Float
toFloat text =
  Text.Read.readMaybe str'
  where
    str = Data.Text.unpack text
    str' = case str of
      '+' : rest -> rest
      '.' : rest -> '0' : '.' : rest
      other -> other

-- | Convert a @Float@ to a @Text@.
--
-- > Text.fromFloat 123 == "123"
-- > Text.fromFloat -42 == "-42"
-- > Text.fromFloat 3.9 == "3.9"
fromFloat :: Float -> Text
fromFloat = Data.Text.pack << Prelude.show

-- LIST CONVERSIONS

-- | Convert a Text to a list of characters.
--
-- > toList "abc" == ['a','b','c']
-- > toList "🙈🙉🙊" == ['🙈','🙉','🙊']
toList :: Text -> List Char
toList = Data.Text.unpack

-- | Convert a list of characters into a Text. Can be useful if you
-- want to create a string primarily by consing, perhaps for decoding
-- something.
--
-- > fromList ['a','b','c'] == "abc"
-- > fromList ['🙈','🙉','🙊'] == "🙈🙉🙊"
fromList :: List Char -> Text
fromList = Data.Text.pack

-- CHAR CONVERSIONS

-- | Create a Text from a given character.
--
-- > fromChar 'a' == "a"
fromChar :: Char -> Text
fromChar = Data.Text.singleton

-- | Add a character to the beginning of a Text.
--
-- > cons 'T' "he truth is out there" == "The truth is out there"
cons :: Char -> Text -> Text
cons = Data.Text.cons

-- | Split a non-empty Text into its head and tail. This lets you
-- pattern match on strings exactly as you would with lists.
--
-- > uncons "abc" == Just ('a',"bc")
-- > uncons ""    == Nothing
uncons :: Text -> Maybe (Char, Text)
uncons = Data.Text.uncons

-- HIGHER-ORDER FUNCTIONS

-- | Transform every character in a Text
--
-- > map (\c -> if c == '/' then '.' else c) "a/b/c" == "a.b.c"
map :: (Char -> Char) -> Text -> Text
map = Data.Text.map

-- | Keep only the characters that pass the test.
--
-- > filter isDigit "R2-D2" == "22"
filter :: (Char -> Bool) -> Text -> Text
filter = Data.Text.filter

-- | Reduce a Text from the left.
--
-- > foldl cons "" "time" == "emit"
foldl :: (Char -> b -> b) -> b -> Text -> b
foldl f = Data.Text.foldl' (\a b -> f b a)

-- | Reduce a Text from the right.
--
-- > foldr cons "" "time" == "time"
foldr :: (Char -> b -> b) -> b -> Text -> b
foldr = Data.Text.foldr

-- | Determine whether /any/ characters pass the test.
--
-- > any isDigit "90210" == True
-- > any isDigit "R2-D2" == True
-- > any isDigit "heart" == False
any :: (Char -> Bool) -> Text -> Bool
any = Data.Text.any

-- | Determine whether /all/ characters pass the test.
--
-- > all isDigit "90210" == True
-- > all isDigit "R2-D2" == False
-- > all isDigit "heart" == False
all :: (Char -> Bool) -> Text -> Bool
all = Data.Text.all