{-# LANGUAGE CPP                    #-}
{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE Safe                   #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeOperators          #-}
{-# LANGUAGE TypeSynonymInstances   #-}
{-# LANGUAGE UndecidableInstances   #-}

{- |
Copyright:  (c) 2016 Stephen Diehl
            (c) 2016-2018 Serokell
            (c) 2018-2020 Kowainik
SPDX-License-Identifier: MIT
Maintainer:  Kowainik <xrom.xkov@gmail.com>
Stability:   Stable
Portability: Portable

This module implements type class which allow to have conversion to and from
'Relude.String.Reexport.Text', 'String' and 'ByteString' types
(including both strict and lazy versions). Usually you need to export
'Relude.String.Reexport.Text' modules qualified and use 'T.pack' \/ 'T.unpack'
functions to convert to\/from 'Relude.String.Reexport.Text'. Now you can just
use 'toText' \/ 'toString' functions.
-}

module Relude.String.Conversion
    ( -- * Convenient type aliases
      LText
    , LByteString

      -- * Conversion type classes
    , ConvertUtf8 (..)
    , ToText (..)
    , ToLText (..)
    , ToString (..)
    , LazyStrict (..)
    , fromLazy
    , fromStrict

      -- * Show and read functions
    , readEither
    , show
    ) where

import GHC.TypeLits (ErrorMessage (..), Symbol, TypeError)
import Prelude (error)

import Relude.Base (Constraint, Type)
import Relude.Function (id, (.))
import Relude.Functor (first, (<$>))
import Relude.Monad.Reexport (Either)
import Relude.String.Reexport (ByteString, IsString, Read, ShortByteString, String, Text, fromShort,
                               fromString, toShort)

import qualified Data.ByteString.Lazy as LB
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Encoding.Error as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Encoding as LT
import qualified GHC.Show as Show (Show (show))
import qualified Text.Read (readEither)


-- $setup
-- >>> import Relude

-- | Type synonym for 'Data.Text.Lazy.Text'.
type LText = LT.Text

-- | Type synonym for 'Data.ByteString.Lazy.ByteString'.
type LByteString = LB.ByteString


-- | Type class for conversion to utf8 representation of text.
class ConvertUtf8 a b where
    -- | Encode as utf8 string (usually 'B.ByteString').
    --
    -- >>> encodeUtf8 @Text @ByteString "патак"
    -- "\208\191\208\176\209\130\208\176\208\186"
    encodeUtf8 :: a -> b

    -- | Decode from utf8 string.
    --
    -- >>> decodeUtf8 @Text @ByteString "\208\191\208\176\209\130\208\176\208\186"
    -- "\1087\1072\1090\1072\1082"
    -- >>> putTextLn $ decodeUtf8 @Text @ByteString "\208\191\208\176\209\130\208\176\208\186"
    -- патак
    decodeUtf8 :: b -> a

    {- | Decode as utf8 string but returning execption if byte sequence is malformed.

#if MIN_VERSION_text(1,2,3)
    >>> decodeUtf8 @Text @ByteString "\208\208\176\209\130\208\176\208\186"
    "\65533\1072\1090\1072\1082"
#else
    >>> decodeUtf8 @Text @ByteString "\208\208\176\209\130\208\176\208\186"
    "\65533\65533\1090\1072\1082"
#endif
    >>> decodeUtf8Strict @Text @ByteString "\208\208\176\209\130\208\176\208\186"
    Left Cannot decode byte '\xd0': Data.Text.Internal.Encoding.decodeUtf8: Invalid UTF-8 stream
    -}
    decodeUtf8Strict :: b -> Either T.UnicodeException a

instance ConvertUtf8 String ByteString where
    encodeUtf8 :: String -> ByteString
    encodeUtf8 = T.encodeUtf8 . T.pack
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: ByteString -> String
    decodeUtf8 = T.unpack . T.decodeUtf8
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: ByteString -> Either T.UnicodeException String
    decodeUtf8Strict = (T.unpack <$>) . decodeUtf8Strict
    {-# INLINE decodeUtf8Strict #-}

instance ConvertUtf8 Text ByteString where
    encodeUtf8 :: Text -> ByteString
    encodeUtf8 = T.encodeUtf8
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: ByteString -> Text
    decodeUtf8 = T.decodeUtf8With T.lenientDecode
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: ByteString -> Either T.UnicodeException Text
    decodeUtf8Strict = T.decodeUtf8'
    {-# INLINE decodeUtf8Strict #-}

instance ConvertUtf8 LText ByteString where
    encodeUtf8 :: LText -> ByteString
    encodeUtf8 = LB.toStrict . encodeUtf8
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: ByteString -> LText
    decodeUtf8 = LT.decodeUtf8With T.lenientDecode . LB.fromStrict
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: ByteString -> Either T.UnicodeException LText
    decodeUtf8Strict = decodeUtf8Strict . LB.fromStrict
    {-# INLINE decodeUtf8Strict #-}

{- | Converting 'String' to 'LB.ByteString' might be a slow operation.
Consider using lazy bytestring at first place.
-}
instance ConvertUtf8 String LByteString where
    encodeUtf8 :: String -> LByteString
    encodeUtf8 = LT.encodeUtf8 . LT.pack
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: LByteString -> String
    decodeUtf8 = LT.unpack . LT.decodeUtf8
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: LByteString -> Either T.UnicodeException String
    decodeUtf8Strict = (T.unpack <$>) . decodeUtf8Strict
    {-# INLINE decodeUtf8Strict #-}

instance ConvertUtf8 Text LByteString where
    encodeUtf8 :: Text -> LByteString
    encodeUtf8 = LB.fromStrict . T.encodeUtf8
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: LByteString -> Text
    decodeUtf8 = T.decodeUtf8With T.lenientDecode . LB.toStrict
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: LByteString -> Either T.UnicodeException Text
    decodeUtf8Strict = T.decodeUtf8' . LB.toStrict
    {-# INLINE decodeUtf8Strict #-}

instance ConvertUtf8 LText LByteString where
    encodeUtf8 :: LText -> LByteString
    encodeUtf8 = LT.encodeUtf8
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: LByteString -> LText
    decodeUtf8 = LT.decodeUtf8With T.lenientDecode
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: LByteString -> Either T.UnicodeException LText
    decodeUtf8Strict = LT.decodeUtf8'
    {-# INLINE decodeUtf8Strict #-}

-- | @since 0.6.0.0
instance ConvertUtf8 String ShortByteString where
    encodeUtf8 :: String -> ShortByteString
    encodeUtf8 = toShort . encodeUtf8
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: ShortByteString -> String
    decodeUtf8 = decodeUtf8 . fromShort
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: ShortByteString -> Either T.UnicodeException String
    decodeUtf8Strict = decodeUtf8Strict . fromShort
    {-# INLINE decodeUtf8Strict #-}

-- | @since 0.6.0.0
instance ConvertUtf8 Text ShortByteString where
    encodeUtf8 :: Text -> ShortByteString
    encodeUtf8 = toShort . encodeUtf8
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: ShortByteString -> Text
    decodeUtf8 = decodeUtf8 . fromShort
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: ShortByteString -> Either T.UnicodeException Text
    decodeUtf8Strict = decodeUtf8Strict . fromShort
    {-# INLINE decodeUtf8Strict #-}

-- | @since 0.6.0.0
instance ConvertUtf8 LText ShortByteString where
    encodeUtf8 :: LText -> ShortByteString
    encodeUtf8 = toShort . encodeUtf8
    {-# INLINE encodeUtf8 #-}

    decodeUtf8 :: ShortByteString -> LText
    decodeUtf8 = decodeUtf8 . fromShort
    {-# INLINE decodeUtf8 #-}

    decodeUtf8Strict :: ShortByteString -> Either T.UnicodeException LText
    decodeUtf8Strict = decodeUtf8Strict . fromShort
    {-# INLINE decodeUtf8Strict #-}

-- | Type class for converting other strings to 'T.Text'.
class ToText a where
    toText :: a -> Text

instance ToText String where
    toText :: String -> Text
    toText = T.pack
    {-# INLINE toText #-}

instance ToText Text where
    toText :: Text -> Text
    toText = id
    {-# INLINE toText #-}

instance ToText LText where
    toText :: LText -> Text
    toText = LT.toStrict
    {-# INLINE toText #-}

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toText ("some string" :: ByteString)
...
... Type 'ByteString' doesn't have instance of 'ToText'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: ByteString -> Text
          decodeUtf8Strict :: ByteString -> Either UnicodeException Text
...

@since 0.6.0.0
-}
instance EncodingError ToText "ByteString" "Text" => ToText ByteString where
    toText :: ByteString -> Text
    toText = error "Unreachable ByteString instance of ToText"

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toText ("some string" :: LByteString)
...
... Type 'LByteString' doesn't have instance of 'ToText'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: LByteString -> Text
          decodeUtf8Strict :: LByteString -> Either UnicodeException Text
...

@since 0.6.0.0
-}
instance EncodingError ToText "LByteString" "Text" => ToText LByteString where
    toText :: LByteString -> Text
    toText = error "Unreachable LByteString instance of ToText"

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toText ("some string" :: ShortByteString)
...
... Type 'ShortByteString' doesn't have instance of 'ToText'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: ShortByteString -> Text
          decodeUtf8Strict :: ShortByteString -> Either UnicodeException Text
...

@since 0.6.0.0
-}
instance EncodingError ToText "ShortByteString" "Text" => ToText ShortByteString where
    toText :: ShortByteString -> Text
    toText = error "Unreachable ShortByteString instance of ToText"

-- | Type class for converting other strings to 'LT.Text'.
class ToLText a where
    toLText :: a -> LText

instance ToLText String where
    toLText :: String -> LText
    toLText = LT.pack
    {-# INLINE toLText #-}

instance ToLText Text where
    toLText :: Text -> LText
    toLText = LT.fromStrict
    {-# INLINE toLText #-}

instance ToLText LT.Text where
    toLText :: LText -> LText
    toLText = id
    {-# INLINE toLText #-}

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toLText ("some string" :: ByteString)
...
... Type 'ByteString' doesn't have instance of 'ToLText'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: ByteString -> LText
          decodeUtf8Strict :: ByteString -> Either UnicodeException LText
...

@since 0.6.0.0
-}
instance EncodingError ToLText "ByteString" "LText" => ToLText ByteString where
    toLText :: ByteString -> LText
    toLText = error "Unreachable ByteString instance of ToLText"

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toLText ("some string" :: LByteString)
...
... Type 'LByteString' doesn't have instance of 'ToLText'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: LByteString -> LText
          decodeUtf8Strict :: LByteString -> Either UnicodeException LText
...

@since 0.6.0.0
-}
instance EncodingError ToLText "LByteString" "LText" => ToLText LByteString where
    toLText :: LByteString -> LText
    toLText = error "Unreachable LByteString instance of ToLText"

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toLText ("some string" :: ShortByteString)
...
... Type 'ShortByteString' doesn't have instance of 'ToLText'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: ShortByteString -> LText
          decodeUtf8Strict :: ShortByteString -> Either UnicodeException LText
...

@since 0.6.0.0
-}
instance EncodingError ToLText "ShortByteString" "LText" => ToLText ShortByteString where
    toLText :: ShortByteString -> LText
    toLText = error "Unreachable ShortByteString instance of ToLText"

-- | Type class for converting other strings to 'String'.
class ToString a where
    toString :: a -> String

instance ToString String where
    toString :: String -> String
    toString = id
    {-# INLINE toString #-}

instance ToString Text where
    toString :: Text -> String
    toString = T.unpack
    {-# INLINE toString #-}

instance ToString LText where
    toString :: LText -> String
    toString = LT.unpack
    {-# INLINE toString #-}

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toString ("some string" :: ByteString)
...
... Type 'ByteString' doesn't have instance of 'ToString'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: ByteString -> String
          decodeUtf8Strict :: ByteString -> Either UnicodeException String
...

@since 0.6.0.0
-}
instance EncodingError ToString "ByteString" "String" => ToString ByteString where
    toString :: ByteString -> String
    toString = error "Unreachable ByteString instance of ToString"

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toString ("some string" :: LByteString)
...
... Type 'LByteString' doesn't have instance of 'ToString'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: LByteString -> String
          decodeUtf8Strict :: LByteString -> Either UnicodeException String
...

@since 0.6.0.0
-}
instance EncodingError ToString "LByteString" "String" => ToString LByteString where
    toString :: LByteString -> String
    toString = error "Unreachable LByteString instance of ToString"

{- | ⚠️__CAUTION__⚠️ This instance is for custom error display only.

You should always specify encoding of bytes explicitly.

In case it is used by mistake, the user will see the following:

>>> toString ("some string" :: ShortByteString)
...
... Type 'ShortByteString' doesn't have instance of 'ToString'.
      Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:
          decodeUtf8       :: ShortByteString -> String
          decodeUtf8Strict :: ShortByteString -> Either UnicodeException String
...

@since 0.6.0.0
-}
instance EncodingError ToString "ShortByteString" "String" => ToString ShortByteString where
    toString :: ShortByteString -> String
    toString = error "Unreachable ShortByteString instance of ToString"

-- | Helper type family to produce error messages
type family EncodingError
    (c :: Type -> Constraint)
    (from :: Symbol)
    (to :: Symbol)
    :: Constraint
  where
    EncodingError c from to = TypeError
        ( 'Text "Type '" ':<>: 'Text from ':<>: 'Text "' doesn't have instance of '"
        ':<>: 'ShowType c ':<>: 'Text "'."
        ':$$: 'Text "Use 'decodeUtf8' or 'decodeUtf8Strict' to convert from UTF-8:"
        ':$$: 'Text "    decodeUtf8       :: " ':<>: 'Text from
        ':<>: 'Text " -> " ':<>: 'Text to
        ':$$: 'Text "    decodeUtf8Strict :: " ':<>: 'Text from
        ':<>: 'Text " -> Either UnicodeException " ':<>: 'Text to
        )

{- | Version of 'Text.Read.readEither' that returns 'Text' in case of the parse
error.

>>> readEither @Int "123"
Right 123
>>> readEither @Int "aa"
Left "Prelude.read: no parse"
-}
readEither :: (Read a) => String -> Either Text a
readEither = first toText . Text.Read.readEither
{-# INLINEABLE readEither #-}

{- | Generalized version of 'Prelude.show'. Unlike 'Prelude.show' this function
is polymorphic in its result type. This makes it more convenient to work with
data types like 'Relude.String.Reexport.Text' or 'ByteString'. However, if you
pass the result of 'show' to a function that expects polymorphic argument, this
can break type inference, so use @-XTypeApplications@ to specify the textual
type explicitly.

>>> show (42 :: Int)
"42"
>>> show (42 :: Double)
"42.0"
>>> print (show @Text True)
"True"
-}
show :: forall b a . (Show.Show a, IsString b) => a -> b
show x = fromString (Show.show x)
{-# INLINE show #-}
{-# SPECIALIZE show :: Show.Show  a => a -> Text  #-}
{-# SPECIALIZE show :: Show.Show  a => a -> LText  #-}
{-# SPECIALIZE show :: Show.Show  a => a -> ByteString  #-}
{-# SPECIALIZE show :: Show.Show  a => a -> LByteString  #-}
{-# SPECIALIZE show :: Show.Show  a => a -> String  #-}

{- | Type class for lazy-strict conversions.

@since 0.1.0
-}
class LazyStrict l s | l -> s, s -> l where
    toLazy   :: s -> l
    toStrict :: l -> s

-- | Alias for 'toStrict' function.
fromLazy :: LazyStrict l s => l -> s
fromLazy = toStrict
{-# INLINE fromLazy #-}
{-# SPECIALIZE fromLazy :: LByteString -> ByteString  #-}
{-# SPECIALIZE fromLazy :: LText -> Text  #-}

-- | Alias for 'toLazy' function.
fromStrict :: LazyStrict l s => s -> l
fromStrict = toLazy
{-# INLINE fromStrict #-}
{-# SPECIALIZE fromStrict :: ByteString -> LByteString  #-}
{-# SPECIALIZE fromStrict :: Text -> LText  #-}

instance LazyStrict LByteString ByteString where
    toLazy :: ByteString -> LByteString
    toLazy = LB.fromStrict
    {-# INLINE toLazy #-}

    toStrict :: LByteString -> ByteString
    toStrict = LB.toStrict
    {-# INLINE toStrict #-}

instance LazyStrict LText Text where
    toLazy :: Text -> LText
    toLazy = LT.fromStrict
    {-# INLINE toLazy #-}

    toStrict :: LText -> Text
    toStrict = LT.toStrict
    {-# INLINE toStrict #-}