{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}
module Data.ByteString.Readable (Readable (..)) where

import Control.Applicative
import Data.ByteString (ByteString)
import Data.Monoid
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8', encodeUtf8)
import Data.Text.Read
import qualified Data.Text as T
import qualified Data.ByteString as S

-- | The type-class 'Readable' is used to convert 'ByteString' values to
-- values of other types. Most instances assume the 'ByteString' is
-- encoded via UTF-8.
--
-- Minimal complete instance definition is given by 'readByteString'.
class Readable a where
    -- | Parse the given 'ByteString' into a typed value and also return
    -- the unconsumed bytes. In case of error, provide an error message.
    readByteString :: ByteString -> Either ByteString (a, ByteString)

    -- | Parse the given 'ByteString' into a list of typed values and
    -- return the unconsumed bytes. In case of error, provide an error
    -- message. Instances can use this function to express their way of
    -- reading lists of values. The default implementation parses
    -- comma-separated values (also accepting interspersed spaces).
    readByteStringList :: ByteString -> Either ByteString ([a], ByteString)
    readByteStringList s = parseList ([], s)

    -- | Either turn the given 'ByteString' into a typed value or an error
    -- message. This function also checks, that all input bytes have been
    -- consumed or else it will fail.
    fromByteString :: ByteString -> Either ByteString a
    fromByteString s = do
        (v, s') <- readByteString s
        if S.null s'
            then Right v
            else Left ("unconsumed bytes: '" <> s' <> "'.")

parseList :: Readable a => ([a], ByteString) -> Either ByteString ([a], ByteString)
parseList (!acc, !s)
    | S.null s  = return (reverse acc, s)
    | otherwise = do
        (a, s') <- readByteString s
        parseList (a:acc, S.dropWhile noise s')
  where
    noise w = w `elem` [0x20, 0x2C] -- [' ', ',']

instance Readable a => Readable (Maybe a) where
    readByteString s =
        case readByteString s of
            Left       _  -> Right (Nothing, "")
            Right (v, s') -> Right (Just v, s')

instance Readable a => Readable [a] where
    readByteString = readByteStringList

instance Readable ByteString where
    readByteString = Right . (, "")

instance Readable Text where
    readByteString s = (, "") <$> mapLeft (decodeUtf8' s)

instance Readable Char where
    readByteString s = do
        t <- mapLeft (decodeUtf8' s)
        return (T.head t, encodeUtf8 (T.tail t))

    readByteStringList s = (, "") . T.unpack <$> mapLeft (decodeUtf8' s)

instance Readable Double where
    readByteString = parse (signed double)

instance Readable Int where
    readByteString = parse (signed decimal)

mapLeft :: Show e => Either e a -> Either ByteString a
mapLeft (Left  s) = Left (encodeUtf8 . T.pack . show $ s)
mapLeft (Right x) = Right x

parse :: (Text -> Either String (a, Text)) -> ByteString -> Either ByteString (a, ByteString)
parse f s = do
    t <- mapLeft (decodeUtf8' s)
    case f t of
        Left       e  -> Left (encodeUtf8 (T.pack e))
        Right (v, t') -> return (v, encodeUtf8 t')