-- | Module to perform unit conversions, compute dimensionality, and convert to
-- base units.
module Data.Quantities.Convert where

import Data.List (sort)
import qualified Data.Map as M

import Data.Quantities.Data

-- $setup
-- >>> import Control.Applicative
-- >>> import Data.Quantities

-- | Convert quantity to given units.
--
-- >>> convert <$> fromString "m" <*> unitsFromString "ft"
-- Right (Right 3.280839895013123 foot)
convert :: (Fractional a) => Quantity a -> CompoundUnit ->
           Either (QuantityError a) (Quantity a)
convert q us
  | hq /= hus = Left  $ DifferentDefinitionsError (units q) us
  | otherwise = convert' (defs us) q us
  where hq  = defStringHash (defs' q)
        hus = defStringHash (defs us)


-- | Convert a quantity to its base units.
--
-- >>> convertBase <$> fromString "newton"
-- Right 1000.0 gram meter / second ** 2
convertBase :: (Fractional a) => Quantity a -> Quantity a
convertBase x = convertBase' (defs' x) x


-- | Convert quantity to given units.
convert' :: (Fractional a) => Definitions -> Quantity a -> CompoundUnit ->
            Either (QuantityError a) (Quantity a)
convert' d q us'
  | dimq /= dimus = Left  $ DimensionalityError (CompoundUnit d dimq) (CompoundUnit d dimus)
  | otherwise     = Right $ Quantity (mb / realToFrac mb') us'
  where mb    = magnitude $ convertBase' d q
        mb'   = magnitude $ toBase d (sUnits us')
        dimq  = dimensionality' d (units' q)
        dimus = dimensionality' d (sUnits us')



-- | Convert a quantity to its base units.
convertBase' :: (Fractional a) => Definitions -> Quantity a -> Quantity a
convertBase' d (Quantity m us) = Quantity (m * realToFrac mb) ub
  where (Quantity mb ub) = toBase d (sUnits us)


-- | Converts a composite unit to its base quantity
toBase :: Definitions -> [SimpleUnit] -> Quantity Double
toBase d = foldr (multiplyQuants . simpleToBase d) unityQuant
  where unityQuant = Quantity 1 (CompoundUnit d [])


-- | Converts a simple unit to its base quantity.
simpleToBase :: Definitions -> SimpleUnit -> Quantity Double
simpleToBase d (SimpleUnit sym pre pow) = Quantity m (CompoundUnit d us)
  where (m', u') = bases d M.! sym
        us = map (\(SimpleUnit s p pow') -> SimpleUnit s p (pow*pow')) u'
        m = (m' * (prefixValues d M.! pre)) ** pow


-- | Computes dimensionality of quantity.
--
-- >>> dimensionality <$> fromString "newton"
-- Right [length] [mass] / [time] ** 2
dimensionality :: Quantity a -> CompoundUnit
dimensionality q = CompoundUnit (defs' q) dimUnits
  where dimUnits = dimensionality' (defs' q) (units' q)

-- | Computes dimensionality of a list of SimpleUnits. Stores the
-- dimensionality as a list of SimpleUnits as well, so we don't need a whole
-- new type.
dimensionality' :: Definitions -> [SimpleUnit] -> [SimpleUnit]
dimensionality' d us = sort $ map dim ub
  where ub = units' $ toBase d us
        dim (SimpleUnit sym _ pow) = SimpleUnit ('[' : unitTypes d M.! sym ++ "]") "" pow


-- | Adds two quantities. Second quantity is converted to units of
-- first quantity.
addQuants :: (Fractional a) => Quantity a -> Quantity a ->
             Either (QuantityError a) (Quantity a)
addQuants = linearQuants (+)


-- | Subtract two quantities. Second quantity is converted to units of
-- first quantity.
subtractQuants :: (Fractional a) => Quantity a -> Quantity a ->
                  Either (QuantityError a) (Quantity a)
subtractQuants = linearQuants (-)

-- | Helper function used in addQuants and subtractQuants.
linearQuants :: (Fractional a) => (a -> a -> a) -> Quantity a -> Quantity a
                -> Either (QuantityError a) (Quantity a)
linearQuants f (Quantity m1 u1) q2 = case q of
  (Right q') -> Right $ Quantity (f m1 (magnitude q')) u1
  (Left err) -> Left err
  where q = convert q2 u1