module Data.Quantities.Convert where

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

import Data.Quantities.Data (Quantity(..), CompositeUnit, SimpleUnit(..), Definitions(..)
                            ,multiplyQuants, QuantityError(..))

unityQuant :: Definitions -> Quantity
unityQuant = Quantity 1 []

-- | Convert quantity to given units.
--
-- >>> convert <$> fromString "m" <*> unitsFromString "ft"
-- Right (Right 3.280839895013123 foot)
convert :: Quantity -> CompositeUnit -> Either QuantityError Quantity
convert x = convert' (defs x) x


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


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


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


-- | Converts a composite unit to its base quantity
toBase :: Definitions -> CompositeUnit -> Quantity
toBase d = foldr (multiplyQuants . simpleToBase d) (unityQuant d)


-- | Converts a simple unit to its base quantity.
simpleToBase :: Definitions -> SimpleUnit -> Quantity
simpleToBase d (SimpleUnit sym pre pow) = Quantity m us d
  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.0]
dimensionality :: Quantity -> CompositeUnit
dimensionality q = dimensionality' (defs q) (units q)

dimensionality' :: Definitions -> CompositeUnit -> CompositeUnit
dimensionality' d us = sort $ map dim ub
  where (Quantity _ ub _) = 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 :: Quantity -> Quantity -> Either QuantityError Quantity
addQuants = linearQuants (+)


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


linearQuants :: (Double -> Double -> Double) -> Quantity -> Quantity
                -> Either QuantityError Quantity
linearQuants f (Quantity m1 u1 d) q2 = case q of
  (Right q') -> Right $ Quantity (f m1 (magnitude q')) u1 d
  (Left err) -> Left err
  where q = convert q2 u1