{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE Rank2Types          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns        #-}

-- | Memory units.

module Serokell.Data.Memory.Units
       (
         -- | Type class
         MemoryUnit (..)

         -- | Concrete types
       , Byte
       , Kilobyte
       , Megabyte
       , Gigabyte
       , Terabyte

         -- | Pretty printing
       , unitBuilder
       , memory

         -- | Helpers
       , toBytes
       , fromBytes
       , convertUnit
       ) where

import Universum

import Data.Text.Lazy.Builder (Builder)
import Formatting (bprint, stext, (%))
import Prelude ((!!))

import Serokell.Util.Text (showFixedPretty')

import Test.QuickCheck (Arbitrary)

import qualified Formatting as Fmt

class Integral unit => MemoryUnit unit  where
    -- | This value is n iff (1 :: unit) is n bytes.
    bytesMultiplier :: Proxy unit -> Integer

-- | Convert given memory unit into integer representing bytes.
toBytes
    :: forall unit.
       MemoryUnit unit
    => unit -> Integer
toBytes mu = toInteger mu * bytesMultiplier proxy
  where
    proxy :: Proxy unit
    proxy = Proxy

-- | Convert given number of bytes into memory unit, flooring value if
-- necessary.
fromBytes
    :: forall unit.
       MemoryUnit unit
    => Integer -> unit
fromBytes bytes = fromInteger $ bytes `div` bytesMultiplier proxy
  where
    proxy :: Proxy unit
    proxy = Proxy

-- | Conversion between memory units.
convertUnit :: (MemoryUnit a, MemoryUnit b) => a -> b
convertUnit = fromBytes . toBytes

-- | Construct Text Builder.
unitBuilder :: MemoryUnit unit => unit -> Builder
unitBuilder n@(toBytes -> bytes)
  | bytes == 0 = "0"
  | bytes < 0 = mconcat ["-", unitBuilder n]
  | otherwise = bprint (stext % " " % stext) (showFixedPretty' 3 value) suffix
  where
    suffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
    bytesDouble :: Double
    bytesDouble = realToFrac bytes
    order = min (length suffixes - 1) (floor $ logBase 2 bytesDouble / 10)
    suffix = suffixes !! order
    value = bytesDouble / realToFrac ((2 :: Integer) ^ (order * 10))

-- | Formatter for `formatting` library.
memory :: MemoryUnit unit => Fmt.Format r (unit -> r)
memory = Fmt.later unitBuilder

pow10 :: Num n => Int -> n
pow10 = (10 ^)

newtype Byte =
    Byte Integer
    deriving (Show,Eq,Num,Typeable,Integral,Real,Enum,Ord,Generic,Arbitrary)

instance NFData Byte
instance MemoryUnit Byte where
    bytesMultiplier Proxy = pow10 0

newtype Kilobyte =
    Kilobyte Integer
    deriving (Show,Eq,Num,Typeable,Integral,Real,Enum,Ord,Generic,Arbitrary)

instance NFData Kilobyte
instance MemoryUnit Kilobyte where
    bytesMultiplier Proxy = pow10 3

-- P.S. Feel free to add more.

newtype Megabyte =
    Megabyte Integer
    deriving (Show,Eq,Num,Typeable,Integral,Real,Enum,Ord,Generic,Arbitrary)

instance NFData Megabyte
instance MemoryUnit Megabyte where
    bytesMultiplier Proxy = pow10 6

newtype Gigabyte =
    Gigabyte Integer
    deriving (Show,Eq,Num,Typeable,Integral,Real,Enum,Ord,Generic,Arbitrary)

instance NFData Gigabyte
instance MemoryUnit Gigabyte where
    bytesMultiplier Proxy = pow10 9

newtype Terabyte =
    Terabyte Integer
    deriving (Show,Eq,Num,Typeable,Integral,Real,Enum,Ord,Generic,Arbitrary)

instance NFData Terabyte
instance MemoryUnit Terabyte where
    bytesMultiplier Proxy = pow10 12