{-# LANGUAGE OverloadedStrings, 
             TypeSynonymInstances, 
             FlexibleInstances,
             LambdaCase #-}
module Text.Str (
    Str(..)
  , IsString(..)
  , show
  , error
  , joinLines
  , joinCommas
  , joinSemis
  , joinSlashes
  , asString
  , asByteString
  , asText
  , asString2
  , asByteString2
  , wrapText
  , wrapString
  , wrapByteString
  , wrapByteString2
  , unlines
  , putStrLn
  ) where

import qualified Prelude as P
import Prelude (IO, Show, String, Char, Bool, Int, Ord(..), (.), id, ($), flip)
import qualified Codec.Binary.UTF8.String as US
import Data.Char (isSpace, toLower, toUpper)
import Data.Hashable
import Data.Monoid
import Data.String (IsString(..))
import qualified Data.List as L
import qualified Data.List.Utils as L
import qualified Data.Text as T
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString as B
import qualified Data.ByteString.Base16 as B16
import Data.ByteString (ByteString)
import Codec.Utils (Octet)

-- | @Str@ types are any type which can be thought as abstract strings; that 
-- is, ordered lists of Char. There are at least 3 commonly-used string types
-- in Haskell (String, ByteString and Text), as well as @newtype@d strings.
-- The interop with these types can be tedious or even bug-prone. Using
-- @Str@ allows functions to be written agnostically towards any particular 
-- type. It provides a set of commonly-needed string manipulation functions,
-- and the ability to convert to and from a variety of string types, which
-- lets us "borrow" existing functions which only operate on one of the types
-- (see the various @as-@ functions). @Str@ extends several useful classes, 
-- perhaps most importantly @IsString@, which lets us use string literals to 
-- represent @Str@s.
class (IsString s, Show s, Ord s, Hashable s, Monoid s) => Str s where 
  toString :: s -> String
  toByteString :: s -> ByteString
  toText :: s -> Text
  toOctets :: s -> [Octet]
  toHex :: s -> s
  toHex = asByteString B16.encode
  fromText :: Text -> s
  fromByteString :: ByteString -> s
  fromOctets :: [Octet] -> s
  joinBy :: s -> [s] -> s
  splitOn :: s -> s -> [s]
  smap :: (Char -> Char) -> s -> s
  singleton :: Char -> s
  cons :: Char -> s -> s
  snoc :: s -> Char -> s
  lower :: s -> s
  lower = smap toLower
  upper :: s -> s
  upper = smap toUpper
  capitalize :: s -> s
  capitalize = asString $ \case
    "" -> ""
    (c:cs) -> toUpper c : cs
  reverse :: s -> s
  length :: s -> Int
  dropWhile :: (Char -> Bool) -> s -> s
  isPrefixOf :: s -> s -> Bool
  isSuffixOf :: s -> s -> Bool
  trim :: s -> s
  trim = let f = reverse . dropWhile isSpace in f . f


instance Str String where 
  toString = id
  toByteString = BC.pack
  toText = T.pack
  toOctets = US.encode
  fromByteString = BC.unpack
  fromText = T.unpack
  fromOctets = US.decode
  joinBy = L.intercalate
  smap = P.map
  splitOn = L.split
  singleton c = [c]
  cons = (:)
  snoc s c = s <> [c]
  reverse = P.reverse
  dropWhile = P.dropWhile
  isPrefixOf = L.isPrefixOf
  isSuffixOf = L.isSuffixOf
  length = L.length

instance Str ByteString where 
  toString = BC.unpack
  toByteString = id
  toText = decodeUtf8
  toOctets = B.unpack
  fromByteString = id
  fromText = encodeUtf8
  fromOctets = B.pack
  smap = BC.map
  joinBy = BC.intercalate
  singleton = BC.singleton
  splitOn = P.undefined
  cons = BC.cons
  snoc = BC.snoc
  reverse = BC.reverse
  dropWhile = BC.dropWhile
  isPrefixOf = B.isPrefixOf
  isSuffixOf = B.isSuffixOf
  length = B.length

instance Str Text where 
  toString = T.unpack
  toByteString = encodeUtf8
  toText = id
  toOctets = US.encode . toString
  fromByteString = decodeUtf8
  fromText = id
  fromOctets = fromString . US.decode
  smap = T.map
  joinBy = T.intercalate
  singleton = T.singleton
  cons = T.cons
  snoc = T.snoc
  reverse = T.reverse
  dropWhile = T.dropWhile
  splitOn = T.splitOn
  isPrefixOf = T.isPrefixOf
  isSuffixOf = T.isSuffixOf
  length = T.length

-- | Generalizes @show@ to return any string type.
show :: (Show a, Str s) => a -> s
show = fromString . P.show

-- | Generalizes @error@ to accept any string type.
error :: Str s => s -> a
error = wrapString P.error

-- | Generalizes @putStrLn@.
putStrLn :: Str s => s -> IO ()
putStrLn = wrapString P.putStrLn

-- | Generalizes functions that take a @String@.
wrapString :: Str s => (String -> a) -> s -> a
wrapString f = f . toString

-- | Generalizes functions that take a @ByteString@.
wrapByteString :: Str s => (ByteString -> a) -> s -> a
wrapByteString f = f . toByteString

-- | Generalizes functions that take two @ByteStrings@.
wrapByteString2 :: Str s => (ByteString -> ByteString -> a) -> s -> s -> a
wrapByteString2 f s1 s2 = f (toByteString s1) (toByteString s2)

-- | Converts a function that takes a @Text@ into one that takes any @Str@.
wrapText :: Str s => (Text -> a) -> s -> a
wrapText f = f . toText

-- | Converts a function that operates on @String@s to one that operates
-- on any @Str@.
asString :: Str s => (String -> String) -> s -> s
asString func = fromString . func . toString

-- | Converts a function that operates on @ByteString@s to one that operates
-- on any @Str@.
asByteString :: Str s => (ByteString -> ByteString) -> s -> s
asByteString func = fromByteString . func . toByteString

-- | Converts a function that operates on @Text@ to one that operates on any
-- @Str@.
asText :: Str s => (Text -> Text) -> s -> s
asText func = fromText . func . toText

asOctets :: Str s => ([Octet] -> [Octet]) -> s -> s
asOctets f = fromOctets . f . toOctets

-- | Same as @asString@ but for functions with arity 2.
asString2 :: Str s => (String -> String -> String) -> s -> s -> s
asString2 f s1 s2 = fromString $ f (toString s1) (toString s2)

-- | Same as @asByteString@ but for functions with arity 2.
asByteString2 :: Str s => (ByteString -> ByteString -> ByteString) -> s -> s -> s
asByteString2 f s1 s2 = fromByteString $ f (toByteString s1) (toByteString s2)

asOctets2 :: Str s => ([Octet] -> [Octet] -> [Octet]) -> s -> s -> s
asOctets2 f s1 s2 = fromOctets $ f (toOctets s1) (toOctets s2)

-- | Joins strings with newline separation, and adds a trailing newline.
unlines :: Str s => [s] -> s
unlines s = joinLines s `snoc` '\n'

-- | Joins strings with newlines.
joinLines :: Str s => [s] -> s
joinLines = joinBy "\n"

-- | Joins strings with commas.
joinCommas :: Str s => [s] -> s
joinCommas = joinBy ","

-- | Joins strings with semicolons.
joinSemis :: Str s => [s] -> s
joinSemis = joinBy ";"

-- | Joins strings with forward slashes.
joinSlashes :: Str s => [s] -> s
joinSlashes = joinBy "/"