-----------------------------------------------------------------------------
-- |
-- Module      :  Data.String.Encode
-- 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 Safe #-}

module Data.String.Encode (
    ConvertString(..)
  , EncodeString(..)
  , Lenient(..)
) where

import Data.ByteString (ByteString)
import Data.ByteString.Short (ShortByteString)
import Data.Text (Text)
import Data.Text.Encoding.Error (lenientDecode)
import Data.Word (Word8)
import GHC.Generics (Generic, Generic1)
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]

eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe = either (const Nothing) Just
{-# INLINE eitherToMaybe #-}