{-# 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,Show,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
deriving instance Show 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)"