{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

--------------------------------------------------------------------------------
-- |
--
-- Module      :  Data.Units.Base.Convert
-- Description :  Conversion between units
-- Copyright   :  (c) Alice Rixte 2025
-- License     :  BSD 3
-- Maintainer  :  alice.rixte@u-bordeaux.fr
-- Stability   :  unstable
-- Portability :  non-portable (GHC extensions)
--
-- Conversion between units. Use @'from'@, @'to'@, or  @'fromTo'@ to convert
-- between two units of the same dimension.
--
-- = Implementing conversions for custom units
--
-- Depending on how the custom unit is converted to its standard unit, there are
-- three ways to implement its conversion summarized in the following table and
-- described with further details afterwards:
--
--  +-----------------------+---------------------------+----------------------+
--  |                       | Which instances           | Note                 |
--  |                       | to declare                |                      |
--  +=======================+===========================+======================+
--  | Conversion factor     | @'ConversionFactor'@ and  |                      |
--  |                       | @'ConvertibleUnit'@ using |                      |
--  |                       | the default               | @'fromTo' == @       |
--  |                       | implementations           | @'fromTo''@          |
--  |                       | for 'fromBaseUnit' and    |                      |
--  |                       | 'toBaseUnit'              |                      |
--  +-----------------------+---------------------------+----------------------+
--  | Affine conversion     | @'ConversionFactor'@ and  |                      |
--  |                       | @'ConvertibleUnit'@       |                      |
--  |                       |                           | @'fromTo' /= @       |
--  |                       |                           | @'fromTo''@          |
--  +-----------------------+---------------------------+----------------------+
--  | Non linear conversion |                           | @'from''@, @'to''@   |
--  |                       | @'ConvertibleUnit'@       | and @'fromTo''@      |
--  |                       |                           | cannot be used       |
--  +-----------------------+---------------------------+----------------------+
--
--
-- === Multiplication by a conversion factor
--
-- For units that can be converted to and from their corresponding standard
-- units by multiplication of a converion factor, you only need to declare an
-- instance of @'ConversionFactor'@, like
--
-- @
-- instance Fractional a => ConversionFactor Hour a where
--   factor = 3600
--
-- instance Fractional a => ConvertibleUnit Hour a
--    -- uses default implementations for 'fromBaseUnit' and 'toBaseUnit'
-- @
--
-- >>> fromTo @Hour @Second 1
-- Second 3600.0
-- >>> fromTo' @Hour @Second 1
-- Second 3600.0
--
-- === Affine conversion (with an offset)
--
-- Some units cannot be conversed by a simple multiplication. For instance, the
-- conversion between Celsius degrees and Kelvin degrees involves addition
-- @x °C = x + 273.15 K@.
--
-- However, when considered as /differences/ of temperatures, Celsius degrees
-- are converted to Kelvin degrees by a multiplication of @1@.
--
-- This can be expressed by the following instances:
--
-- @
-- instance Num a => ConversionFactor Celsius a where
--   factor = 1
--
-- instance Fractional a => ConvertibleUnit Celsius a where
--   toBaseUnit (Celsius x) = Kelvin (x - 273.15)
--   fromBaseUnit (Kelvin x) = Celsius (x + 273.15)
-- @
--
-- >>> fromTo @Celsius @Kelvin 0
-- Kelvin 273.15
-- >>> fromTo' @Celsius @Kelvin 0
-- Kelvin 0.0
--
-- === Other conversions
--
-- Any other conversion can be implemented, like for instance logarithmic units.
-- In this case, you should only give an instance for @'ConvertibleUnit'@, and
-- no instance for @'ConversionFactor'@. See for instance linear picth
-- @'Data.Unit.NonStd.Frequency.Tet'@.
--
--------------------------------------------------------------------------------

module Data.Units.Base.Convert
  ( DimEq
  -- * Generic conversion between units
  , ConvertibleUnit (..)
  , FromTo
  , fromTo
  , from
  , to
  , ($~)
  , (~&)
  -- * Conversion using conversion factors
  , ConversionFactor (..)
  , toBaseUnit'
  , fromBaseUnit'
  , FromTo'
  , fromTo'
  , from'
  , to'
  )
  where

import Data.Proxy
import Data.Kind
import Data.Type.Bool
import Data.Type.Equality
import GHC.TypeError

import Data.Type.Int

import Data.Units.Base.System

-- | A constraint to test whether two units have
type family DimEq (u :: Unit) (v :: Unit) :: Constraint where
  DimEq u v = DimEqStd u v (DimOf u) (DimOf v)

type family DimEqStd (u :: Unit) (v :: Unit) (du :: Dim) (dv :: Dim)
  :: Constraint where
  DimEqStd u v du dv =
    ( IsUnit u
    , IsUnit v
    , du ~ dv
    , If (du == dv) (() :: Constraint)
      (TypeError (
            Text "Cannot convert unit ‘"
            :<>: ShowUnitType u
            :<>: Text "’ to unit ‘"
            :<>: ShowUnitType v
            :<>: Text "’ because their dimensions do not match."
            :$$: Text "Dimension of ‘"
            :<>: ShowUnitType u
            :<>: Text "’ is: "
            :<>: ShowDimType du
            :$$: Text "Dimension of ‘"
            :<>: ShowUnitType v
            :<>: Text "’ is: "
            :<>: ShowDimType dv
    )))



-- | A unit whose quantities are convertible from that unit to its corresponding
-- base unit.
--
-- Instances must satisfy the following law :
--
-- * @'toBaseUnit' . 'fromBaseUnit' == 'id'@
--
class (IsUnit u, IsUnit (BaseUnitOf u)) => ConvertibleUnit u a where
  -- | Convert a quantity to its base unit.
  --
  -- >>> import Data.Units.NonStd.Time
  -- >>> toBaseUnit @Hour 1
  -- Second 3600.0
  -- >>> toBaseUnit (Hour 1)
  -- Second 3600.0
  -- >>> toBaseUnit @(Kilo Meter ./. Hour) 36
  -- quantity @(Meter .*. Second .^- 1) 10.0
  -- >>> toBaseUnit (Celsius 0)
  -- Kelvin 273.15
  toBaseUnit :: u a -> BaseUnitOf u a
  default toBaseUnit :: ConversionFactor u a => u a -> BaseUnitOf u a
  toBaseUnit = toBaseUnit'
  {-# INLINE toBaseUnit #-}

  -- | Convert a quantity from its base unit to another unit.
  --
  -- >>> fromBaseUnit @Hour 1800
  -- Hour 0.5
  -- >>> fromBaseUnit 1800 :: Hour Double
  -- Hour 0.5
  -- >>> fromBaseUnit @(Kilo Meter ./. Hour) 10
  -- quantity @(Kilo Meter .*. Hour .^- 1) 36.0
  -- >>> fromBaseUnit @Celsius 0
  -- Celsius (-273.15)
  --
  fromBaseUnit :: BaseUnitOf u a -> u a
  default fromBaseUnit :: ConversionFactor u a => BaseUnitOf u a -> u a
  fromBaseUnit = fromBaseUnit'
  {-# INLINE fromBaseUnit #-}



-- | A constraint that is satisfied when both units have the same dimension and
-- are such that @u@ can be converted to @v@.
--
type FromTo u v a = (DimEq u v, ConvertibleUnit u a, ConvertibleUnit v a)

-- | Conversion between two quantities with the same dimension.
--
--  >>> fromTo @Celsius @Kelvin 0
--  Kelvin 273.15
-- >>> fromTo @(Milli Second) @Hour 1
-- Hour 2.7777777777777776e-7
-- >>> fromTo (Milli (Second 1)) :: Hour Double
-- Hour 2.7777777777777776e-7
-- >>> fromTo @Turn @Degree (1/4) -- angle conversion
-- Degree 90.0
-- >>> fromTo @(Kilo Meter ./. Hour) @(Milli Meter ./. Milli Second) 36
-- quantity @(Milli Meter .*. Milli Second .^- 1) 10.0
--
fromTo :: FromTo u v a => u a -> v a
fromTo = fromBaseUnit . toBaseUnit
{-# INLINE fromTo #-}

-- | A mere synonym of @'fromTo'@ where it is more intuitive to use only one
-- type application.
--
-- >>> from @Celsius 0 :: Kelvin Double
-- Kelvin 273.15
--
from :: FromTo u v a => u a -> v a
from = fromTo
{-# INLINE from #-}

-- | Same as @'fromTo'@ but the type applications are reversed
--
-- >>> to @Kelvin (Celsius 0)
-- Kelvin 273.15
--
to :: forall v u a. FromTo u v a => u a -> v a
to = fromTo
{-# INLINE to #-}

-- | A convenient operator for converting a unit before feeding it to a
-- function.
--
-- >>> import Linear
-- >>> rotation (Radian th) = V2 (V2 (cos th) (- sin th)) (V2 (sin th)  (cos th))
-- >>> rotation $~ Degree 90
-- V2 (V2 6.123031769111886e-17 (-1.0)) (V2 1.0 6.123031769111886e-17)
--
($~) :: FromTo u v a => (v a -> b) -> u a -> b
f $~ x = f (fromTo x)
{-# INLINE ($~) #-}

infixr 0 $~

-- | Same as @'($~)'@ but with arguments flipped.
--
(~&) :: FromTo u v a => u a -> (v a -> b) -> b
(~&) = flip ($~)
{-# INLINE (~&) #-}

infixl 1 ~&

--------------------------------------------------------------------------------

-- | Unit that can be converted to their corresponding standard unit by
-- multiplication with a conversion factor.
--
-- Instances must satisfy the following laws:
--
-- * @'toBaseUnit' @u ==  'quantity' ('unQuantity' q * 'factor' @u)@
-- * @'fromBaseUnit' @u  == 'quantity' (''unQuantity' q / 'factor' @u)@
--
class (ConvertibleUnit u a, Fractional a) => ConversionFactor u a where
  {-# MINIMAL factor #-}

  -- | Multiplying a quantity of type @u a@ with @'factor'@ will convert it
  -- to its corresponding base unit @BaseUnitOf u a@
  --
  -- >>> factor @Hour :: Double
  -- 3600.0
  -- >>> factor @Celsius :: Double
  -- 1.0
  -- >>> factor @(Kilo Meter ./. Hour) :: Double
  -- 0.2777777777777778
  factor :: a

instance Fractional a => ConvertibleUnit NoUnit a

instance Fractional a => ConversionFactor NoUnit a where
  factor = 1
  {-# INLINE factor #-}

instance (Num a, ConversionFactor u a, ConversionFactor v a, IsUnit (BaseUnitOf (u .*. v)))
  => ConvertibleUnit (u .*. v) a

instance (Num a, ConversionFactor u a, ConversionFactor v a, IsUnit (BaseUnitOf (u .*. v)))
  =>  ConversionFactor (u .*. v) a where
  factor = factor @u * factor @v
  {-# INLINE factor #-}

instance (ConversionFactor u a, IsUnit (BaseUnitOf (u .^. n)),  KnownInt n)
  => ConvertibleUnit (u .^. n) a

instance (ConversionFactor u a, IsUnit (BaseUnitOf (u .^. n)),  KnownInt n)
  =>  ConversionFactor (u .^. n) a where
  factor = factor @u ^^ intVal (Proxy :: Proxy n)
  {-# INLINE factor #-}

-- | Convert a quantity to its corresponding base unit by multiplying it
-- by  @'factor'@.
--
-- >>> toBaseUnit' @Hour 1
-- Second 3600.0
-- >>> toBaseUnit' (Hour 1)
-- Second 3600.0
-- >>> toBaseUnit' @(Kilo Meter ./. Hour) 36
-- quantity @(Meter .*. Second .^- 1) 10.0
-- >>> toBaseUnit' (Celsius 0)
-- Kelvin 0.0
--
toBaseUnit' :: forall u a. ConversionFactor u a
  => u a -> BaseUnitOf u a
toBaseUnit' q = quantity (unQuantity q * factor @u)
{-# INLINE toBaseUnit' #-}

-- | Convert a standard quantity to a unit @u@ by dividing it by
-- by  @'factor'@.
--
-- >>> fromBaseUnit' @Hour 1800
-- Hour 0.5
-- >>> fromBaseUnit' 1800 :: Hour Double
-- Hour 0.5
-- >>> fromBaseUnit' @(Kilo Meter ./. Hour) 10
-- quantity @(Kilo Meter .*. Hour .^- 1) 36.0
-- >>> fromBaseUnit' @Celsius 0
-- Celsius 0.0
--
fromBaseUnit' :: forall u a. ConversionFactor u a
  => BaseUnitOf u a -> u a
fromBaseUnit' q = quantity (unQuantity q / factor @u)
{-# INLINE fromBaseUnit' #-}

-- | A constraint that is satisfied when both units have the same dimension and
-- are such that @u@ can be converted to @v@ by using a conversion factor.
--
type FromTo' u v a = (DimEq u v, ConversionFactor u a, ConversionFactor v a)

-- | Conversion, using conversion factors, between two quantities with the same
-- dimension
--
-- >>> fromTo' @Celsius @Kelvin 0
-- Kelvin 0.0
-- >>> fromTo' @(Milli Second) @Hour 1
-- Hour 2.7777777777777776e-7
-- >>> fromTo' (Milli (Second 1)) :: Hour Double
-- Hour 2.7777777777777776e-7
-- >>> fromTo' @Turn @Degree (1/4) -- angle conversion
-- Degree 90.0
-- >>> fromTo' @(Kilo Meter ./. Hour) @(Milli Meter ./. Milli Second) 36
-- quantity @(Milli Meter .*. Milli Second .^- 1) 10.0
--
fromTo' :: forall u v a.
  FromTo' u v a
  => u a -> v a
fromTo' q = quantity (unQuantity q * (factor @u / factor @v))
{-# INLINE fromTo' #-}



-- | A mere synonym of @'fromTo''@ where it is more intuitive to use only one
-- type application.
--
-- >>> from' @Celsius 0 :: Kelvin Double
-- Kelvin 0.0
--
from' :: FromTo' u v a => u a -> v a
from' = fromTo'
{-# INLINE from' #-}

-- | Same as @'fromTo''@ but the type applications are reversed
--
-- >>> to' @Kelvin (Celsius 0)
-- Kelvin 0.0
--
to' :: forall v u a. FromTo' u v a => u a -> v a
to' = fromTo'
{-# INLINE to' #-}


