-- | Class for reading bounded values.
module Text.Read.Bounded (
    BoundedRead(..),
    ReadBounded(..),
    readBoundedInteger,
) where


import Data.Int
import Data.Word
import Text.Read (readMaybe)


-- | Information about a bounded read.
data BoundedRead a
    = NoRead -- ^ The read failed.
    | ExactRead a -- ^ The value was successfully read exactly, and did not have to be clamped to a narrower representation.
    | ClampedRead a -- ^ The value was successfully read, but had to be clamped to a narrower representation because its value was too wide.
    deriving (Show, Read, Eq, Ord)


fromMaybe :: Maybe a -> BoundedRead a
fromMaybe = maybe NoRead ExactRead


data Clamped a
    = Exact a
    | Clamped a


clamp :: (Bounded a, Integral a) => Integer -> Clamped a
clamp x = if x < minInteger
    then Clamped minNum
    else if x > maxInteger
        then Clamped maxNum
        else Exact $ fromInteger x
    where
        minNum = minBound
        maxNum = maxBound
        minInteger = toInteger minNum
        maxInteger = toInteger maxNum


-- | Reads a clamped value for any integer type with the given class constraints.
-- Useful for implementing a 'ReadBounded' instance or avoiding one.
readBoundedInteger :: (Bounded a, Read a, Integral a) => String -> BoundedRead a
readBoundedInteger str = case readMaybe str of
    Nothing -> NoRead
    Just x -> case clamp x of
        Exact y -> ExactRead y
        Clamped y -> ClampedRead y


-- | Much like the 'Prelude.Read' class, but will return (possibly) clamped values.
--
-- Typical instances of this class will clamp against 'Prelude.Bounded.minBound' and 'Prelude.Bounded.maxBound'
--
-- This class is designed to avoid inconsistency problems such as the following:
--
-- >>> read "321" :: Word8
-- 65
-- >>> read "4321" :: Word8
-- 225
-- >>> read "-4" :: Word8
-- 252
--
-- Using this class, the results are predictable and precise:
--
-- >>> readBounded "321" :: BoundedRead Word8
-- ClampedRead 255
-- >>> readBounded "4321" :: BoundedRead Word8
-- ClampedRead 255
-- >>> readBounded "-4" :: BoundedRead Word8
-- ClampedRead 0
-- >>> readBounded "255" :: BoundedRead Word8
-- ExactRead 255
-- >>> readBounded "6" :: BoundedRead Word8
-- ExactRead 6
-- >>> readBounded "xxx" :: BoundedRead Word8
-- NoRead
class ReadBounded a where
    readBounded :: String -> BoundedRead a


instance ReadBounded Integer where
    readBounded = fromMaybe . readMaybe


instance ReadBounded Int where
    readBounded = readBoundedInteger


instance ReadBounded Int8 where
    readBounded = readBoundedInteger


instance ReadBounded Int16 where
    readBounded = readBoundedInteger


instance ReadBounded Int32 where
    readBounded = readBoundedInteger


instance ReadBounded Int64 where
    readBounded = readBoundedInteger


instance ReadBounded Word where
    readBounded = readBoundedInteger


instance ReadBounded Word8 where
    readBounded = readBoundedInteger


instance ReadBounded Word16 where
    readBounded = readBoundedInteger


instance ReadBounded Word32 where
    readBounded = readBoundedInteger


instance ReadBounded Word64 where
    readBounded = readBoundedInteger