{-# LANGUAGE StandaloneDeriving, GADTs, MultiParamTypeClasses, FunctionalDependencies, 
	     FlexibleInstances,  RankNTypes, UndecidableInstances #-}

-- | Physical units of measure.
module BuildBox.Aspect.Units
	( 
	  -- * The unit types
	  Seconds	(..)
	, Bytes		(..)

	  -- * IsUnits
	, IsUnits 	(..)

	  -- * HasUnits
	, HasUnits 	(..)
	
	  -- * WithUnits wrappers
	, WithUnits	(..)
	, secs
	, bytes
	, appWithUnits
	, liftWithUnits
	, liftsWithUnits
	, liftsWithUnits2

	  -- * Unit-preserving collation
	, Collatable	(..)
	, collateWithUnits)
	
where
import BuildBox.Aspect.Single
import BuildBox.Aspect.Stats
import BuildBox.Data.Dividable
import BuildBox.Pretty
import Data.Maybe


-- Unit types -------------------------------------------------------------------------------------
-- | Seconds of time.
data Seconds	= Seconds Double
		deriving (Read, Show, Ord, Eq)

instance Real Seconds where
	toRational (Seconds s1) 	= toRational s1

instance Dividable Seconds where
	divide (Seconds s1) (Seconds s2) = Seconds (s1 / s2)	

instance Num Seconds where
	(+) (Seconds f1) (Seconds f2)	= Seconds (f1 + f2)
	(-) (Seconds f1) (Seconds f2)	= Seconds (f1 - f2)
	(*) (Seconds f1) (Seconds f2)	= Seconds (f1 * f2)
	abs (Seconds f1) 		= Seconds (abs f1)
	signum (Seconds f1)		= Seconds (signum f1)
	fromInteger i			= Seconds (fromInteger i)
	
instance Pretty Seconds where
	ppr (Seconds f)			
		= fromMaybe (text (show f))
		$ pprEngDouble "s" f


-- | Bytes of data.
data Bytes	= Bytes	  Integer
		deriving (Read, Show, Ord, Eq)

instance Real Bytes where
	toRational (Bytes b1)		= toRational b1

instance Dividable Bytes where
	divide (Bytes s1) (Bytes s2)	= Bytes (s1 `div` s2)

instance Num Bytes where
	(+) (Bytes f1) (Bytes f2)	= Bytes (f1 + f2)
	(-) (Bytes f1) (Bytes f2)	= Bytes (f1 - f2)
	(*) (Bytes f1) (Bytes f2)	= Bytes (f1 * f2)
	abs (Bytes f1) 			= Bytes (abs f1)
	signum (Bytes f1)		= Bytes (signum f1)
	fromInteger i			= Bytes (fromInteger i)

instance Pretty Bytes where
	ppr (Bytes b)			
		= fromMaybe (text (show b))
		$ pprEngInteger "B" b
	

-- Type classes -----------------------------------------------------------------------------------
-- | Represents the units used for some thing.
data IsUnits a where
	IsSeconds 	:: IsUnits Seconds
	IsBytes		:: IsUnits Bytes	

class HasUnits a a => Units a where
	isUnits :: a -> Maybe (IsUnits a)

instance Units Seconds where
	isUnits s		= hasUnits s

instance Units Bytes where
	isUnits s		= hasUnits s


-- | Determine the units used by the elements of some collection, 
--   by inspecting the elements directly.
--   Returns `Nothing` when applied to empty collections, as they have no units.
class HasUnits a b | a -> b where
	hasUnits :: a -> Maybe (IsUnits b)

instance HasUnits Seconds Seconds where
	hasUnits _		= Just IsSeconds

instance HasUnits Bytes Bytes where
	hasUnits _		= Just IsBytes

instance HasUnits a a => HasUnits (Single a) a where
	hasUnits (Single x)	= hasUnits x

instance HasUnits a a => HasUnits (Stats a) a where
	hasUnits (Stats x _ _)	= hasUnits x

instance HasUnits a a => HasUnits [a] a where
	hasUnits []		= Nothing
	hasUnits (x : _)	= hasUnits x


-- WithUnits --------------------------------------------------------------------------------------
-- | A wrapper type used to store data of varying physical units in a homogenous collection structure.
data WithUnits t where
	WithSeconds	:: t Seconds	-> WithUnits t
	WithBytes	:: t Bytes	-> WithUnits t
	
deriving instance (Show (t Bytes), Show (t Seconds)) => Show (WithUnits t)
deriving instance (Read (t Bytes), Read (t Seconds)) => Read (WithUnits t)

instance  (Pretty (t Bytes), Pretty (t Seconds))
	=> Pretty (WithUnits t) where
 ppr withUnits
  = case withUnits of
	WithSeconds s	-> ppr s
	WithBytes   b	-> ppr b

-- | Helpful wrapper for constructing seconds-valued aspect data. Examples:
-- 
--   @Time TotalWall \`secs\` 10  ::  WithUnits (Aspect Single)@
-- 
secs 	:: (Single Seconds -> c Single Seconds) 
	-> Double -> WithUnits (c Single)
secs mk f  = WithSeconds (mk (Single (Seconds f)))


-- | Similar to `secs`.
bytes 	:: (Single Bytes -> c Single Bytes) 
	-> Integer -> WithUnits (c Single)
bytes mk b = WithBytes   (mk (Single (Bytes b)))


-- | Apply a function to unit-wrapped data
appWithUnits
	:: (forall units. Real units => t1 units -> b)
	-> WithUnits t1 -> b
	
appWithUnits f withUnits
 = case withUnits of
	WithSeconds dat	-> f dat
	WithBytes   dat	-> f dat


-- | Apply a function to unit-wrapped data.
liftWithUnits 
	:: (forall units. Real units => t1 units -> t2 units)
	-> WithUnits t1 -> WithUnits t2

liftWithUnits f withUnits
 = case withUnits of
	WithSeconds dat	-> WithSeconds (f dat)
	WithBytes   dat -> WithBytes   (f dat)


-- | Transform values of each unit type as a group.
liftsWithUnits 
	:: (forall units. Real units => [t1 units] -> [t2 units]) 
	-> [WithUnits t1] -> [WithUnits t2]

liftsWithUnits f us
  = let	asSeconds	= [a | WithSeconds a	<- us]
	asBytes		= [a | WithBytes   a	<- us]

    in	   (map WithSeconds $ f asSeconds)
	++ (map WithBytes   $ f asBytes)
	

-- | Transform values of each unit type as a group
liftsWithUnits2
	:: (forall units. Real units => [t1 units] -> [t2 units] -> [t3 units])
	-> [WithUnits t1] -> [WithUnits t2] -> [WithUnits t3]
	
liftsWithUnits2 f as bs
 = let	asSeconds	= [a | WithSeconds a	<- as]
	bsSeconds	= [b | WithSeconds b	<- bs]

	asBytes		= [a | WithBytes   a	<- as]
	bsBytes		= [b | WithBytes   b	<- bs]
	
   in	(map WithSeconds $ f asSeconds bsSeconds)
    ++	(map WithBytes   $ f asBytes   bsBytes)



-- Unit-safe collation ----------------------------------------------------------------------------
-- | Collate some data, while preserving units.
class Collatable t where
	collate :: forall a. HasUnits a a 
		=> [t Single a] -> [t [] a]


-- | Collate some data.
--
--  @
-- collateWithUnits  [ Time KernelCpu \`secs\`  5
--                   , Time KernelCpu \`secs\`  10
--                   , Time TotalWall \`secs\`  55
--                   , Size ExeSize   \`bytes\` 100884
--                   , Time TotalWall \`secs\`  52 ]
-- =>
--                   [ WithSeconds (Time KernelCpu [Seconds 5.0,  Seconds 10.0])
--                   , WithSeconds (Time TotalWall [Seconds 55.0, Seconds 52.0])
--                   , WithBytes   (Size ExeSize [Bytes 1024])]
--  @
-- 
collateWithUnits :: Collatable c => [WithUnits (c Single)] -> [WithUnits (c [])]
collateWithUnits as
  = let	asSeconds	= [a | WithSeconds a	<- as]
	asBytes		= [a | WithBytes   a	<- as]

    in	   (map WithSeconds $ collate asSeconds)
	++ (map WithBytes   $ collate asBytes)