{-# OPTIONS -fno-warn-missing-methods #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE NoRebindableSyntax #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE StandaloneDeriving #-}

-- | Compatible API with the `text' package.

module Data.Text
  ( Text
  -- * Creation and elimination
  , pack
  , unpack
  , fromString
  , empty
  -- * Conversions
  , showInt
  , toShortest
  -- * I/O
  , putStrLn
  -- * Breaking into many substrings
  , splitOn
  , stripSuffix
  -- * Basic interface
  , cons
  , snoc
  , append
  , (<>)
  , uncons
  , head
  , init
  , last
  , tail
  , null
  , length
  -- * Special folds
  , maximum
  , all
  , any
  , concatMap
  , concat
  , minimum
  -- * Case conversion
  , toLower
  , toUpper
  -- * Transformations
  , map
  , intercalate
  , intersperse
  , reverse
  -- * Predicates
  , isPrefixOf
  -- * Substrings
  , drop
  , take
  -- * Breaking into lines and words
  , unlines
  , lines
  ) where

import Data.Data
import FFI
import Data.Nullable (fromNullable)
import Prelude (Eq,String,Int,Bool,Char,Maybe,Double,Ord,error)
import qualified "base" Data.String as B (IsString (..))

-- | A space efficient, packed, unboxed Unicode text type.
data Text
deriving instance Eq Text
deriving instance Data Text
deriving instance Typeable Text
instance Ord Text
instance B.IsString Text where fromString = error "the method fromString can never be called"

-- | O(n) The intercalate function takes a Text and a list of Texts and
-- concatenates the list after interspersing the first argument
-- between each element of the list.
intercalate :: Text -> [Text] -> Text
intercalate = ffi "%2.join(%1)"

-- | Convert from a string to text.
fromString :: String -> Text
fromString = ffi "%1"

-- | O(n) Adds a character to the end of a Text. This copies the
-- entire array in the process, unless fused. Subject to
-- fusion. Performs replacement on invalid scalar values.
snoc :: Text -> Char -> Text
snoc = ffi "%1 + %2"

-- | O(n) Adds a character to the front of a Text. This function is
-- more costly than its List counterpart because it requires copying a
-- new array. Subject to fusion. Performs replacement on invalid
-- scalar values.
cons :: Char -> Text -> Text
cons = ffi "%1 + %2"

-- | O(n) Convert a String into a Text. Subject to fusion. Performs
-- replacement on invalid scalar values.
pack :: String -> Text
pack = ffi "%1"

-- | O(n) Convert a Text into a String. Subject to fusion.
unpack :: Text -> String
unpack = ffi "%1"

-- | O(n) Appends one Text to the other by copying both of them into a
-- new Text. Subject to fusion.
append :: Text -> Text -> Text
append = ffi "%1 + %2"

-- | Append two texts.
(<>) :: Text -> Text -> Text
(<>) = ffi "%1 + %2"

-- | O(n) Returns the number of characters in a Text. Subject to
-- fusion.
length :: Text -> Int
length = ffi "%1.length"

-- | O(1) Tests whether a Text is empty or not. Subject to fusion.
null :: Text -> Bool
null = ffi "%1.length == 0"

-- | O(n) take n, applied to a Text, returns the prefix of the Text of
-- length n, or the Text itself if n is greater than the length of the
-- Text. Subject to fusion.
take :: Int -> Text -> Text
take = ffi "%2.substring(0,%1)"

-- | O(n) drop n, applied to a Text, returns the suffix of the Text
-- after the first n characters, or the empty Text if n is greater
-- than the length of the Text. Subject to fusion.
drop :: Int -> Text -> Text
drop = ffi "%2.substring(%1)"

-- | O(1) The empty Text.

-- Basic interface
empty :: Text
empty = ffi "\"\""

-- | O(n) Breaks a Text up into a list of Texts at newline Chars. The
-- resulting strings do not contain newlines.
lines :: Text -> [Text]
lines = ffi "%1.split('\\n')"

-- | O(n) Joins lines, after appending a terminating newline to each.
unlines :: [Text] -> Text
unlines = ffi "%1.join('\\n')"

-- | O(n) The isPrefixOf function takes two Texts and returns True iff
-- the first is a prefix of the second. Subject to fusion.
-- http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html
isPrefixOf :: Text -> Text -> Bool
isPrefixOf = ffi "%2.lastIndexOf(%1, 0) == 0"

-- | O(n) The intersperse function takes a character and places it
-- between the characters of a Text.  Subject to fusion. Performs
-- replacement on invalid scalar values.
intersperse :: Char -> Text -> Text
intersperse = ffi "%2.split('').join(%1)"

-- | O(n) Reverse the characters of a string. Subject to fusion.
reverse :: Text -> Text
reverse = ffi "%1.split('').reverse().join('')"

-- | O(n) Return the prefix of the second string if its suffix matches
-- the entire first string.
stripSuffix :: Text -- ^ Suffix.
                -> Text -- ^ Text.
                -> Maybe Text
stripSuffix prefix text =
  fromNullable (extract prefix text)
  where extract :: Text -> Text -> Nullable Text
        extract =
          ffi "(function(suffix,text){ return text.substring(text.length - suffix.length) == suffix? text.substring(0,text.length - suffix.length) : null; })(%1,%2)"

-- | O(m+n) Break a Text into pieces separated by the first Text
-- argument, consuming the delimiter. An empty delimiter is
-- invalid, and will cause an error to be raised.
splitOn :: Text -> Text -> [Text]
splitOn = ffi "%2.split(%1)"

-- |
putStrLn :: Text -> Fay ()
putStrLn = ffi "console.log('%%s',%1)"

-- |
toShortest :: Double -> Text
toShortest = ffi "%1.toString()"

-- |
showInt :: Int -> Text
showInt = ffi "%1.toString()"

-- | O(1) Returns the first character and rest of a Text, or Nothing
-- if empty. Subject to fusion.
uncons :: Text -> Maybe (Char, Text)
uncons = ffi "%1[0] ? { instance: 'Just', slot1 : [%1[0],%1.slice(1)] } : { instance : 'Nothing' }"

-- | O(1) Returns the first character of a Text, which must be
-- non-empty. Subject to fusion.
head :: Text -> Char
head = ffi "%1[0] || (function () {throw new Error('Data.Text.head: empty Text'); }())"

-- | O(1) Returns the last character of a Text, which must be
-- non-empty. Subject to fusion.
last :: Text -> Char
last = ffi "%1.length ? %1[%1.length-1] : (function() { throw new Error('Data.Text.last: empty Text') })()"

-- | O(1) Returns all characters after the head of a Text, which must
-- be non-empty. Subject to fusion.
tail :: Text -> Text
tail = ffi "%1.length ? %1.slice(1) : (function () { throw new Error('Data.Text.tail: empty Text') })()"

-- | O(1) Returns all but the last character of a Text, which must be
-- non-empty. Subject to fusion.
init :: Text -> Text
init = ffi "%1.length ? %1.slice(0,-1) : (function () { throw new Error('Data.Text.init: empty Text') })()"

-- | O(n) map f t is the Text obtained by applying f to each element
-- of t. Subject to fusion. Performs replacement on invalid scalar
-- values.
map :: (Char -> Char) -> Text -> Text
map = ffi "[].map.call(%2, %1).join('')"

-- | O(n) Convert a string to lower case, using simple case
-- conversion. The result string may be longer than the input
-- string. For instance, "İ" (Latin capital letter I with dot above,
-- U+0130) maps to the sequence "i" (Latin small letter i, U+0069)
-- followed by " ̇" (combining dot above, U+0307).
toLower :: Text -> Text
toLower = ffi "%1.toLowerCase()"

-- | O(n) Convert a string to upper case, using simple case
-- conversion. The result string may be longer than the input
-- string. For instance, the German "ß" (eszett, U+00DF) maps to the
-- two-letter sequence "SS".
toUpper :: Text -> Text
toUpper = ffi "%1.toUpperCase()"

-- | O(n) Concatenate a list of Texts.
concat :: [Text] -> Text
concat = ffi "%1.join('')"

-- | O(n) Map a function over a Text that results in a Text, and
-- concatenate the results.
concatMap :: (Char -> Text) -> Text -> Text
concatMap = ffi "[].map.call(%2, %1).join('')"

-- | O(n) any p t determines whether any character in the Text t
-- satisifes the predicate p. Subject to fusion.
any :: (Char -> Bool) -> Text -> Bool
any = ffi "[].filter.call(%2, %1).length > 0"

-- | O(n) all p t determines whether all characters in the Text t
-- satisify the predicate p. Subject to fusion.
all :: (Char -> Bool) -> Text -> Bool
all = ffi "[].filter.call(%2, %1).length == %1.length"

-- | O(n) maximum returns the maximum value from a Text, which must be
-- non-empty. Subject to fusion.
maximum :: Text -> Char
maximum = ffi "(function (s) { \
  \   if (s === '') { throw new Error('Data.Text.maximum: empty string'); } \
  \   var max = s[0]; \
  \   for (var i = 1; i < s.length; s++) { \
  \     if (s[i] > max) { max = s[i]; } \
  \   } \
  \   return max; \
  \ })(%1)"

-- | O(n) minimum returns the minimum value from a Text, which must be
-- non-empty. Subject to fusion.
minimum :: Text -> Char
minimum = ffi "(function (s) { \
  \   if (s === '') { throw new Error('Data.Text.maximum: empty string'); } \
  \   var min = s[0]; \
  \   for (var i = 1; i < s.length; s++) { \
  \     if (s[i] < min) { min = s[i]; } \
  \   } \
  \   return min; \
  \ })(%1)"