-----------------------------------------------------------------------------
-- |
-- Module      :  Intro.ConvertString
-- Copyright   :  (c) Daniel Mendler 2017
-- License     :  MIT
--
-- Maintainer  :  mail@daniel-mendler.de
-- Stability   :  experimental
-- Portability :  portable
--
-- String conversion and decoding
--
-----------------------------------------------------------------------------

{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE Safe #-}

module Intro.ConvertString (
    ConvertString(..)
  , EncodeString(..)
  , Lenient(..)
) where

import Data.ByteString (ByteString)
import Data.ByteString.Short (ShortByteString)
import Data.Either.Extra (eitherToMaybe)
import Data.Eq (Eq)
import Data.Foldable (Foldable)
import Data.Function (id, (.))
import Data.Functor (Functor(fmap))
import Data.Maybe (Maybe)
import Data.Ord (Ord)
import Data.String (String)
import Data.Text (Text)
import Data.Text.Encoding.Error (lenientDecode)
import Data.Traversable (Traversable)
import Data.Word (Word8)
import GHC.Generics (Generic, Generic1)
import Text.Read (Read)
import Text.Show (Show)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Short as S
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE

-- | Conversion of strings to other string types
--
-- @
-- ('convertString' :: b -> a)         . ('convertString' :: a -> b) ≡ ('id'      :: a -> a)
-- ('convertString' :: b -> 'Maybe' a)   . ('convertString' :: a -> b) ≡ ('Just'    :: a -> 'Maybe' a)
-- ('convertString' :: b -> 'Lenient' a) . ('convertString' :: a -> b) ≡ ('Lenient' :: a -> 'Lenient' a)
-- @
class ConvertString a b where
  -- | Convert a string to another string type
  convertString :: a -> b

-- | Encode and decode strings as a byte sequence
--
-- @
-- 'decodeString'        . 'encodeString' ≡ 'Just'
-- 'decodeStringLenient' . 'encodeString' ≡ 'id'
-- @
class (ConvertString a b, ConvertString b (Maybe a), ConvertString b (Lenient a)) => EncodeString a b where
  -- | Encode a string as a byte sequence
  encodeString :: a -> b
  encodeString = convertString
  {-# INLINE encodeString #-}

  -- | Lenient decoding of byte sequence
  --
  -- Lenient means that invalid characters are replaced
  -- by the Unicode replacement character '\FFFD'.
  decodeStringLenient :: b -> a
  decodeStringLenient = getLenient . convertString
  {-# INLINE decodeStringLenient #-}

  -- | Decode byte sequence
  --
  -- If the decoding fails, return Nothing.
  decodeString :: b -> Maybe a
  decodeString = convertString
  {-# INLINE decodeString #-}

-- | Newtype wrapper for a string which was decoded leniently.
newtype Lenient a = Lenient { getLenient :: a }
  deriving (Eq, Ord, Read, Show, Functor, Foldable, Traversable, Generic, Generic1)

instance ConvertString BL.ByteString   (Lenient String)  where {-# INLINE convertString #-}; convertString = Lenient . TL.unpack . TLE.decodeUtf8With lenientDecode
instance ConvertString BL.ByteString   (Lenient TL.Text) where {-# INLINE convertString #-}; convertString = Lenient . TLE.decodeUtf8With lenientDecode
instance ConvertString BL.ByteString   (Lenient Text)    where {-# INLINE convertString #-}; convertString = Lenient . TE.decodeUtf8With lenientDecode . BL.toStrict
instance ConvertString BL.ByteString   (Maybe   String)  where {-# INLINE convertString #-}; convertString = fmap TL.unpack . eitherToMaybe . TLE.decodeUtf8'
instance ConvertString BL.ByteString   (Maybe   TL.Text) where {-# INLINE convertString #-}; convertString = eitherToMaybe . TLE.decodeUtf8'
instance ConvertString BL.ByteString   (Maybe   Text)    where {-# INLINE convertString #-}; convertString = eitherToMaybe . TE.decodeUtf8' . BL.toStrict
instance ConvertString BL.ByteString   BL.ByteString     where {-# INLINE convertString #-}; convertString = id
instance ConvertString BL.ByteString   ByteString        where {-# INLINE convertString #-}; convertString = BL.toStrict
instance ConvertString BL.ByteString   ShortByteString   where {-# INLINE convertString #-}; convertString = S.toShort . BL.toStrict
instance ConvertString BL.ByteString   [Word8]           where {-# INLINE convertString #-}; convertString = BL.unpack
instance ConvertString ByteString      (Lenient String)  where {-# INLINE convertString #-}; convertString = Lenient . T.unpack . TE.decodeUtf8With lenientDecode
instance ConvertString ByteString      (Lenient TL.Text) where {-# INLINE convertString #-}; convertString = Lenient . TLE.decodeUtf8With lenientDecode . BL.fromStrict
instance ConvertString ByteString      (Lenient Text)    where {-# INLINE convertString #-}; convertString = Lenient . TE.decodeUtf8With lenientDecode
instance ConvertString ByteString      (Maybe   String)  where {-# INLINE convertString #-}; convertString = fmap T.unpack . eitherToMaybe . TE.decodeUtf8'
instance ConvertString ByteString      (Maybe   TL.Text) where {-# INLINE convertString #-}; convertString = eitherToMaybe . TLE.decodeUtf8' . BL.fromStrict
instance ConvertString ByteString      (Maybe   Text)    where {-# INLINE convertString #-}; convertString = eitherToMaybe . TE.decodeUtf8'
instance ConvertString ByteString      BL.ByteString     where {-# INLINE convertString #-}; convertString = BL.fromStrict
instance ConvertString ByteString      ByteString        where {-# INLINE convertString #-}; convertString = id
instance ConvertString ByteString      ShortByteString   where {-# INLINE convertString #-}; convertString = S.toShort
instance ConvertString ByteString      [Word8]           where {-# INLINE convertString #-}; convertString = B.unpack
instance ConvertString ShortByteString (Lenient String)  where {-# INLINE convertString #-}; convertString = Lenient . T.unpack . TE.decodeUtf8With lenientDecode . S.fromShort
instance ConvertString ShortByteString (Lenient TL.Text) where {-# INLINE convertString #-}; convertString = Lenient . TLE.decodeUtf8With lenientDecode . BL.fromStrict . S.fromShort
instance ConvertString ShortByteString (Lenient Text)    where {-# INLINE convertString #-}; convertString = Lenient . TE.decodeUtf8With lenientDecode . S.fromShort
instance ConvertString ShortByteString (Maybe   String)  where {-# INLINE convertString #-}; convertString = fmap T.unpack . eitherToMaybe . TE.decodeUtf8' . S.fromShort
instance ConvertString ShortByteString (Maybe   TL.Text) where {-# INLINE convertString #-}; convertString = eitherToMaybe . TLE.decodeUtf8' . BL.fromStrict . S.fromShort
instance ConvertString ShortByteString (Maybe   Text)    where {-# INLINE convertString #-}; convertString = eitherToMaybe . TE.decodeUtf8' . S.fromShort
instance ConvertString ShortByteString BL.ByteString     where {-# INLINE convertString #-}; convertString = BL.fromStrict . S.fromShort
instance ConvertString ShortByteString ByteString        where {-# INLINE convertString #-}; convertString = S.fromShort
instance ConvertString ShortByteString ShortByteString   where {-# INLINE convertString #-}; convertString = id
instance ConvertString ShortByteString [Word8]           where {-# INLINE convertString #-}; convertString = S.unpack
instance ConvertString String          BL.ByteString     where {-# INLINE convertString #-}; convertString = TLE.encodeUtf8 . TL.pack
instance ConvertString String          ByteString        where {-# INLINE convertString #-}; convertString = TE.encodeUtf8 . T.pack
instance ConvertString String          ShortByteString   where {-# INLINE convertString #-}; convertString = S.toShort . TE.encodeUtf8 . T.pack
instance ConvertString String          String            where {-# INLINE convertString #-}; convertString = id
instance ConvertString String          TL.Text           where {-# INLINE convertString #-}; convertString = TL.pack
instance ConvertString String          Text              where {-# INLINE convertString #-}; convertString = T.pack
instance ConvertString String          [Word8]           where {-# INLINE convertString #-}; convertString = BL.unpack . TLE.encodeUtf8 . TL.pack
instance ConvertString TL.Text         BL.ByteString     where {-# INLINE convertString #-}; convertString = TLE.encodeUtf8
instance ConvertString TL.Text         ByteString        where {-# INLINE convertString #-}; convertString = BL.toStrict . TLE.encodeUtf8
instance ConvertString TL.Text         ShortByteString   where {-# INLINE convertString #-}; convertString = S.toShort . BL.toStrict . TLE.encodeUtf8
instance ConvertString TL.Text         String            where {-# INLINE convertString #-}; convertString = TL.unpack
instance ConvertString TL.Text         TL.Text           where {-# INLINE convertString #-}; convertString = id
instance ConvertString TL.Text         Text              where {-# INLINE convertString #-}; convertString = TL.toStrict
instance ConvertString TL.Text         [Word8]           where {-# INLINE convertString #-}; convertString = BL.unpack . TLE.encodeUtf8
instance ConvertString Text            BL.ByteString     where {-# INLINE convertString #-}; convertString = BL.fromStrict . TE.encodeUtf8
instance ConvertString Text            ByteString        where {-# INLINE convertString #-}; convertString = TE.encodeUtf8
instance ConvertString Text            ShortByteString   where {-# INLINE convertString #-}; convertString = S.toShort . TE.encodeUtf8
instance ConvertString Text            String            where {-# INLINE convertString #-}; convertString = T.unpack
instance ConvertString Text            TL.Text           where {-# INLINE convertString #-}; convertString = TL.fromStrict
instance ConvertString Text            Text              where {-# INLINE convertString #-}; convertString = id
instance ConvertString Text            [Word8]           where {-# INLINE convertString #-}; convertString = BL.unpack . BL.fromStrict . TE.encodeUtf8
instance ConvertString [Word8]         (Lenient String)  where {-# INLINE convertString #-}; convertString = Lenient . TL.unpack . TLE.decodeUtf8With lenientDecode . BL.pack
instance ConvertString [Word8]         (Lenient TL.Text) where {-# INLINE convertString #-}; convertString = Lenient . TLE.decodeUtf8With lenientDecode . BL.pack
instance ConvertString [Word8]         (Lenient Text)    where {-# INLINE convertString #-}; convertString = Lenient . TE.decodeUtf8With lenientDecode . B.pack
instance ConvertString [Word8]         (Maybe String)    where {-# INLINE convertString #-}; convertString = fmap TL.unpack . eitherToMaybe . TLE.decodeUtf8' . BL.pack
instance ConvertString [Word8]         (Maybe TL.Text)   where {-# INLINE convertString #-}; convertString = eitherToMaybe . TLE.decodeUtf8' . BL.pack
instance ConvertString [Word8]         (Maybe Text)      where {-# INLINE convertString #-}; convertString = eitherToMaybe . TE.decodeUtf8' . B.pack
instance ConvertString [Word8]         BL.ByteString     where {-# INLINE convertString #-}; convertString = BL.pack
instance ConvertString [Word8]         ByteString        where {-# INLINE convertString #-}; convertString = B.pack
instance ConvertString [Word8]         ShortByteString   where {-# INLINE convertString #-}; convertString = S.pack
instance ConvertString [Word8]         [Word8]           where {-# INLINE convertString #-}; convertString = id

instance EncodeString  String          BL.ByteString
instance EncodeString  String          ByteString
instance EncodeString  String          ShortByteString
instance EncodeString  String          [Word8]
instance EncodeString  TL.Text         BL.ByteString
instance EncodeString  TL.Text         ByteString
instance EncodeString  TL.Text         ShortByteString
instance EncodeString  TL.Text         [Word8]
instance EncodeString  Text            BL.ByteString
instance EncodeString  Text            ByteString
instance EncodeString  Text            ShortByteString
instance EncodeString  Text            [Word8]