{-# LANGUAGE CPP
           , KindSignatures
           , DataKinds
           , GADTs
           , StandaloneDeriving
           , ExistentialQuantification
           #-}

{-# OPTIONS_GHC -Wall -fwarn-tabs #-}
----------------------------------------------------------------
--                                                    2015.12.18
-- |
-- Module      :  Language.Hakaru.Types.Coercion
-- Copyright   :  Copyright (c) 2016 the Hakaru team
-- License     :  BSD3
-- Maintainer  :  wren@community.haskell.org
-- Stability   :  experimental
-- Portability :  GHC-only
--
-- Our theory of coercions for Hakaru's numeric hierarchy.
----------------------------------------------------------------
module Language.Hakaru.Types.Coercion
    (
    -- * The primitive coercions
      PrimCoercion(..)
    , PrimCoerce(..)

    -- * The category of general coercions
    , Coercion(..)
    , singletonCoercion
    , signed
    , continuous
    , Coerce(..)
    , singCoerceDom
    , singCoerceCod
    , singCoerceDomCod

    -- * The induced coercion hierarchy
    , CoercionMode(..)
    , findCoercion
    , findEitherCoercion
    , Lub(..)
    , findLub
    , SomeRing(..)
    , findRing
    , SomeFractional(..)
    , findFractional

    -- * Experimental optimization functions
    {-
    , CoerceTo_UnsafeFrom(..)
    , simplifyCTUF
    , UnsafeFrom_CoerceTo(..)
    , simplifyUFCT
    -}
    , ZigZag(..)
    , simplifyZZ
    ) where

import Prelude          hiding (id, (.))
import Control.Category (Category(..))
#if __GLASGOW_HASKELL__ < 710
import Data.Functor        ((<$>))
#endif
import Control.Applicative ((<|>))
import Language.Hakaru.Types.DataKind
import Language.Hakaru.Types.Sing
import Language.Hakaru.Types.HClasses
import Language.Hakaru.Syntax.IClasses
    (TypeEq(..), Eq1(..), Eq2(..), JmEq1(..), JmEq2(..))

----------------------------------------------------------------
----------------------------------------------------------------
-- TODO: (?) coercing HMeasure by coercing the underlying measure space.
-- TODO: lifting coercion over (:->), to avoid the need for eta-expansion
-- TODO: lifting coersion over datatypes, to avoid traversing them at runtime
-- TODO: see how GHC handles lifting coersions these days...

-- N.B., we don't need to store the dictionary values here, we can recover them by typeclass (using -XScopedTypeVariables).
--
-- | Primitive proofs of the inclusions in our numeric hierarchy.
data PrimCoercion :: Hakaru -> Hakaru -> * where
    Signed     :: !(HRing a)       -> PrimCoercion (NonNegative a) a
    Continuous :: !(HContinuous a) -> PrimCoercion (HIntegral   a) a

-- TODO: instance Read (PrimCoercion a b)
deriving instance Show (PrimCoercion a b)

instance Eq (PrimCoercion a b) where -- this one could be derived
    (==) = eq1
instance Eq1 (PrimCoercion a) where
    eq1 = eq2
instance Eq2 PrimCoercion where
    eq2 x y = maybe False (const True) (jmEq2 x y)
instance JmEq1 (PrimCoercion a) where
    jmEq1 x y = snd <$> jmEq2 x y
instance JmEq2 PrimCoercion where
    jmEq2 (Signed r1) (Signed r2) =
        jmEq1 r1 r2 >>= \Refl -> Just (Refl, Refl)
    jmEq2 (Continuous c1) (Continuous c2) =
        jmEq1 c1 c2 >>= \Refl -> Just (Refl, Refl)
    jmEq2 _ _ = Nothing


----------------------------------------------------------------
-- | General proofs of the inclusions in our numeric hierarchy.
-- Notably, being a partial order, 'Coercion' forms a category. In
-- addition to the 'Category' instance, we also have the class
-- 'Coerce' for functors from 'Coercion' to the category of Haskell
-- functions, and you can get the co\/domain objects (via
-- 'singCoerceDom', 'singCoerceCod', or 'singCoerceDomCod').
data Coercion :: Hakaru -> Hakaru -> * where
    -- BUG: haddock doesn't like annotations on GADT constructors
    -- <https://github.com/hakaru-dev/hakaru/issues/6>

    -- Added the trivial coercion so we get the Category instance.
    -- This may/should make program transformations easier to write
    -- by allowing more intermediate ASTs, but will require a cleanup
    -- pass afterwards to remove the trivial coercions.
    CNil :: Coercion a a

    -- TODO: but sometimes we need the snoc-based inductive hypothesis...
    -- We use a cons-based approach rather than append-based in
    -- order to get a better inductive hypothesis.
    CCons :: !(PrimCoercion a b) -> !(Coercion b c) -> Coercion a c

infixr 5 `CCons`

-- TODO: instance Read (Coercion a b)
deriving instance Show (Coercion a b)

instance Eq  (Coercion a b) where
    (==) = eq1
instance Eq1 (Coercion a) where
    eq1 = eq2
instance Eq2 Coercion where
    eq2 CNil         CNil         = True
    eq2 (CCons x xs) (CCons y ys) =
      case jmEq2 x y of
         Just (Refl, Refl) -> eq2 xs ys
         Nothing -> False
    eq2 _            _            = False

-- TODO: the JmEq2 and JmEq1 instances

instance Category Coercion where
    id = CNil
    xs . CNil       = xs
    xs . CCons y ys = CCons y (xs . ys)


-- BUG: GHC 7.8 does not allow making these into pattern synonyms:
-- (1) it disallows standalone type signatures for pattern synonyms,
-- so we'd need to give it as an annotation, which isn't too terrible;
-- but, (2) it does not allow polymorphic pattern synonyms :(

-- | A smart constructor for lifting 'PrimCoercion' into 'Coercion'
singletonCoercion :: PrimCoercion a b -> Coercion a b
singletonCoercion c = CCons c CNil

-- | A smart constructor for 'Signed'.
signed :: (HRing_ a) => Coercion (NonNegative a) a
signed = singletonCoercion $ Signed hRing

-- | A smart constructor for 'Continuous'.
continuous :: (HContinuous_ a) => Coercion (HIntegral a) a
continuous = singletonCoercion $ Continuous hContinuous


----------------------------------------------------------------
-- | This class defines a mapping from 'PrimCoercion' to the @(->)@
-- category. (Technically these mappings aren't functors, since
-- 'PrimCoercion' doesn't form a category.) It's mostly used for
-- defining the analogous 'Coerce' instance; that is, given a
-- @PrimCoerce F@ instance, we have the following canonical @Coerce
-- F@ instance:
--
-- > instance Coerce F where
-- >     coerceTo   CNil         s = s
-- >     coerceTo   (CCons c cs) s = coerceTo cs (primCoerceTo c s)
-- >
-- >     coerceFrom CNil         s = s
-- >     coerceFrom (CCons c cs) s = primCoerceFrom c (coerceFrom cs s)
--
class PrimCoerce (f :: Hakaru -> *) where
    primCoerceTo   :: PrimCoercion a b -> f a -> f b
    primCoerceFrom :: PrimCoercion a b -> f b -> f a

instance PrimCoerce (Sing :: Hakaru -> *) where
    primCoerceTo (Signed theRing) s =
        case jmEq1 s (sing_NonNegative theRing) of
        Just Refl -> sing_HRing theRing
        Nothing   -> error "primCoerceTo@Sing: the impossible happened"
    primCoerceTo (Continuous theCont) s =
        case jmEq1 s (sing_HIntegral theCont) of
        Just Refl -> sing_HContinuous theCont
        Nothing   -> error "primCoerceTo@Sing: the impossible happened"

    primCoerceFrom (Signed theRing) s =
        case jmEq1 s (sing_HRing theRing) of
        Just Refl -> sing_NonNegative theRing
        Nothing   -> error "primCoerceFrom@Sing: the impossible happened"
    primCoerceFrom (Continuous theCont) s =
        case jmEq1 s (sing_HContinuous theCont) of
        Just Refl -> sing_HIntegral theCont
        Nothing   -> error "primCoerceFrom@Sing: the impossible happened"


-- | This class defines functors from the 'Coercion' category to
-- the @(->)@ category. It's mostly used for defining smart
-- constructors that implement the coercion in @f@. We don't require
-- a 'PrimCoerce' constraint (because we never need it), but given
-- a @Coerce F@ instance, we have the following canonical @PrimCoerce
-- F@ instance:
--
-- > instance PrimCoerce F where
-- >     primCoerceTo   c = coerceTo   (singletonCoercion c)
-- >     primCoerceFrom c = coerceFrom (singletonCoercion c)
--
class Coerce (f :: Hakaru -> *) where
    coerceTo   :: Coercion a b -> f a -> f b
    coerceFrom :: Coercion a b -> f b -> f a

instance Coerce (Sing :: Hakaru -> *) where
    coerceTo   CNil         s = s
    coerceTo   (CCons c cs) s = coerceTo cs (primCoerceTo c s)

    coerceFrom CNil         s = s
    coerceFrom (CCons c cs) s = primCoerceFrom c (coerceFrom cs s)

----------------------------------------------------------------
singPrimCoerceDom :: PrimCoercion a b -> Sing a
singPrimCoerceDom (Signed     theRing) = sing_NonNegative theRing
singPrimCoerceDom (Continuous theCont) = sing_HIntegral   theCont

singPrimCoerceCod :: PrimCoercion a b -> Sing b
singPrimCoerceCod (Signed     theRing) = sing_HRing       theRing
singPrimCoerceCod (Continuous theCont) = sing_HContinuous theCont


-- | Return a singleton for the domain type, or 'Nothing' if it's
-- the 'CNil' coercion.
singCoerceDom :: Coercion a b -> Maybe (Sing a)
singCoerceDom CNil           = Nothing
singCoerceDom (CCons c CNil) = Just $ singPrimCoerceDom c
singCoerceDom (CCons c cs)   = primCoerceFrom c <$> singCoerceDom cs

-- | Return a singleton for the codomain type, or 'Nothing' if it's
-- the 'CNil' coercion.
singCoerceCod :: Coercion a b -> Maybe (Sing b)
singCoerceCod CNil           = Nothing
singCoerceCod (CCons c CNil) = Just $ singPrimCoerceCod c
singCoerceCod (CCons c cs)   = Just . coerceTo cs $ singPrimCoerceCod c

-- | Return singletons for the domain and codomain types, or 'Nothing'
-- if it's the 'CNil' coercion. If you need both types, this is a
-- bit more efficient than calling 'singCoerceDom' and 'singCoerceCod'
-- separately.
singCoerceDomCod :: Coercion a b -> Maybe (Sing a, Sing b)
singCoerceDomCod CNil           = Nothing
singCoerceDomCod (CCons c CNil) =
    Just (singPrimCoerceDom c, singPrimCoerceCod c)
singCoerceDomCod (CCons c cs)   = do
    dom <- singCoerceDom cs
    Just (primCoerceFrom c dom
        , coerceTo cs $ singPrimCoerceCod c
        )


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

-- | Given two types, find a coercion from the first to the second,
-- or return 'Nothing' if there is no such coercion.
findCoercion :: Sing a -> Sing b -> Maybe (Coercion a b)
findCoercion SNat  SInt  = Just signed
findCoercion SProb SReal = Just signed
findCoercion SNat  SProb = Just continuous
findCoercion SInt  SReal = Just continuous
findCoercion SNat  SReal = Just (continuous . signed)
findCoercion a     b     = jmEq1 a b >>= \Refl -> Just CNil


-- | Given two types, find a coercion where an unsafe
-- coercion, followed by a safe coercion are needed to
-- coerce the first type into the second, return otherwise
findMixedCoercion
    :: Sing a
    -> Sing b
    -> Maybe (Sing 'HNat, Coercion 'HNat a, Coercion 'HNat b)
findMixedCoercion SProb SInt  = Just (SNat, continuous, signed)
findMixedCoercion SInt  SProb = Just (SNat, signed, continuous)
findMixedCoercion _     _     = Nothing

data CoercionMode a b = 
              Safe   (Coercion a b)
  |           Unsafe (Coercion b a)
  | forall c. Mixed  (Sing c, Coercion c a, Coercion c b)                

-- | Given two types, find either a coercion from the first to the
-- second or a coercion from the second to the first, or returns
-- 'Nothing' if there is neither such coercion.
--
-- If the two types are equal, then we preferentially return the
-- @Coercion a b@. The ordering of the 'Either' is so that we
-- consider the @Coercion a b@ direction \"success\" in the 'Either'
-- monad (which also expresses our bias when the types are equal).
findEitherCoercion
    :: Sing a
    -> Sing b
    -> Maybe (CoercionMode a b)
findEitherCoercion a b =
    (Safe   <$> findCoercion a b) <|>
    (Unsafe <$> findCoercion b a) <|>
    (Mixed  <$> findMixedCoercion a b)


-- | An upper bound of two types, with the coercions witnessing its
-- upperbound-ness. The type itself ensures that we have /some/
-- upper bound; but in practice we assume it is in fact the /least/
-- upper bound.
data Lub (a :: Hakaru) (b :: Hakaru)
    = forall c. Lub !(Sing c) !(Coercion a c) !(Coercion b c)


-- TODO: is there any way we can reuse 'findCoercion' to DRY?
-- | Given two types, find their least upper bound.
findLub :: Sing a -> Sing b -> Maybe (Lub a b)
-- cases where @a < b@:
findLub SNat  SInt  = Just $ Lub SInt  signed CNil
findLub SProb SReal = Just $ Lub SReal signed CNil
findLub SNat  SProb = Just $ Lub SProb continuous CNil
findLub SInt  SReal = Just $ Lub SReal continuous CNil
findLub SNat  SReal = Just $ Lub SReal (continuous . signed) CNil
-- the symmetric cases where @b < a@:
findLub SInt  SNat  = Just $ Lub SInt  CNil signed
findLub SReal SProb = Just $ Lub SReal CNil signed
findLub SProb SNat  = Just $ Lub SProb CNil continuous
findLub SReal SInt  = Just $ Lub SReal CNil continuous
findLub SReal SNat  = Just $ Lub SReal CNil (continuous . signed)
-- cases where the lub is different from both @a@ and @b@:
findLub SInt  SProb = Just $ Lub SReal continuous signed
findLub SProb SInt  = Just $ Lub SReal signed continuous
-- case where @a == b@:
findLub a     b     = jmEq1 a b >>= \Refl -> Just $ Lub a CNil CNil


data SomeRing (a :: Hakaru)
    = forall b. SomeRing !(HRing b) !(Coercion a b)

-- | Give a type, finds the smallest coercion to another
-- with a HRing instance
findRing :: Sing a -> Maybe (SomeRing a)
findRing SNat  = Just (SomeRing HRing_Int  signed)
findRing SInt  = Just (SomeRing HRing_Int  CNil)
findRing SProb = Just (SomeRing HRing_Real signed)
findRing SReal = Just (SomeRing HRing_Real CNil)
findRing _     = Nothing

data SomeFractional (a :: Hakaru)
    = forall b. SomeFractional !(HFractional b) !(Coercion a b)

-- | Give a type, finds the smallest coercion to another
-- with a HFractional instance
findFractional :: Sing a -> Maybe (SomeFractional a)
findFractional SNat  = Just (SomeFractional HFractional_Prob continuous)
findFractional SInt  = Just (SomeFractional HFractional_Real continuous)
findFractional SProb = Just (SomeFractional HFractional_Prob CNil)
findFractional SReal = Just (SomeFractional HFractional_Real CNil)
findFractional _     = Nothing


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

data CoerceTo_UnsafeFrom :: Hakaru -> Hakaru -> * where
    CTUF :: !(Coercion b c) -> !(Coercion b a) -> CoerceTo_UnsafeFrom a c

-- BUG: deriving instance Eq   (CoerceTo_UnsafeFrom a b)
-- BUG: deriving instance Read (CoerceTo_UnsafeFrom a b)
deriving instance Show (CoerceTo_UnsafeFrom a b)

-- TODO: handle the fact that certain coercions commute over one another!
simplifyCTUF :: CoerceTo_UnsafeFrom a c -> CoerceTo_UnsafeFrom a c
simplifyCTUF (CTUF xs ys) =
    case xs of
    CNil        -> CTUF CNil ys
    CCons x xs' ->
        case ys of
        CNil        -> CTUF xs CNil
        CCons y ys' ->
            case jmEq2 x y of
            Just (Refl, Refl) -> simplifyCTUF (CTUF xs' ys')
            Nothing           -> CTUF xs ys

----------------------------------------------------------------
-- | Choose the other inductive hypothesis.
data RevCoercion :: Hakaru -> Hakaru -> * where
    CLin  :: RevCoercion a a
    CSnoc :: !(RevCoercion a b) -> !(PrimCoercion b c) -> RevCoercion a c

-- BUG: deriving instance Eq   (RevCoercion a b)
-- BUG: deriving instance Read (RevCoercion a b)
deriving instance Show (RevCoercion a b)

instance Category RevCoercion where
    id = CLin
    CLin . xs       = xs
    CSnoc ys y . xs = CSnoc (ys . xs) y

revCons :: PrimCoercion a b -> RevCoercion b c -> RevCoercion a c
revCons x CLin         = CSnoc CLin x
revCons x (CSnoc ys y) = CSnoc (revCons x ys) y

toRev :: Coercion a b -> RevCoercion a b
toRev CNil         = CLin
toRev (CCons x xs) = revCons x (toRev xs)

obvSnoc :: Coercion a b -> PrimCoercion b c -> Coercion a c
obvSnoc CNil         y = CCons y CNil
obvSnoc (CCons x xs) y = CCons x (obvSnoc xs y)

fromRev :: RevCoercion a b -> Coercion a b
fromRev CLin         = CNil
fromRev (CSnoc xs x) = obvSnoc (fromRev xs) x

data UnsafeFrom_CoerceTo :: Hakaru -> Hakaru -> * where
    UFCT
        :: !(Coercion c b)
        -> !(Coercion a b)
        -> UnsafeFrom_CoerceTo a c

-- BUG: deriving instance Eq   (UnsafeFrom_CoerceTo a b)
-- BUG: deriving instance Read (UnsafeFrom_CoerceTo a b)
deriving instance Show (UnsafeFrom_CoerceTo a b)

data RevUFCT :: Hakaru -> Hakaru -> * where
    RevUFCT :: !(RevCoercion c b) -> !(RevCoercion a b) -> RevUFCT a c
        
-- TODO: handle the fact that certain coercions commute over one another!
-- N.B., This version can be tricky to get to type check because our associated type families aren't guaranteed injective.
simplifyUFCT :: UnsafeFrom_CoerceTo a c -> UnsafeFrom_CoerceTo a c
simplifyUFCT (UFCT xs ys) =
    case simplifyRevUFCT $ RevUFCT (toRev xs) (toRev ys) of
    RevUFCT xs' ys' -> UFCT (fromRev xs') (fromRev ys')

simplifyRevUFCT :: RevUFCT a c -> RevUFCT a c
simplifyRevUFCT (RevUFCT xs ys) =
    case xs of
    CLin        -> RevUFCT CLin ys
    CSnoc xs' x ->
        case ys of
        CLin        -> RevUFCT xs CLin
        CSnoc ys' y ->
            case jmEq2 x y of
            Just (Refl, Refl) -> simplifyRevUFCT (RevUFCT xs' ys')
            Nothing           -> RevUFCT xs ys


-- TODO: implement a simplifying pass for pushing/gathering coersions over other things (e.g., Less/Equal)

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

-- | An arbitrary composition of safe and unsafe coercions.
data ZigZag :: Hakaru -> Hakaru -> * where
    ZRefl :: ZigZag a a
    Zig   :: !(Coercion a b) -> !(ZigZag b c) -> ZigZag a c
    Zag   :: !(Coercion b a) -> !(ZigZag b c) -> ZigZag a c

-- BUG: deriving instance Eq   (ZigZag a b)
-- BUG: deriving instance Read (ZigZag a b)
deriving instance Show (ZigZag a b)

-- TODO: whenever we build up a new ZigZag from the remains of a simplification step, do we need to resimplify? If so, how can we avoid quadratic behavior? cf., <http://research.microsoft.com/en-us/um/people/simonpj/papers/ext-f/fc-normalization-rta.pdf> Also, try just doing a shortest-path problem on the graph of coercions (since we can get all the singletons along the way)
-- TODO: handle the fact that certain coercions commute over one another!
simplifyZZ :: ZigZag a b -> ZigZag a b
simplifyZZ ZRefl      = ZRefl
simplifyZZ (Zig x xs) =
    case simplifyZZ xs of
    ZRefl   -> Zig x ZRefl
    Zig y z -> Zig (y . x) z
    Zag y z ->
        -- TODO: can we optimize this to avoid reversing things?
        case simplifyUFCT (UFCT x y) of
        UFCT CNil CNil -> z
        UFCT CNil y'   -> Zag y' z
        UFCT x'   CNil -> Zig x' z
        UFCT x'   y'   -> Zig x' (Zag y' z)
simplifyZZ (Zag x xs) =
    case simplifyZZ xs of
    ZRefl   -> Zag x ZRefl
    Zag y z -> Zag (x . y) z
    Zig y z ->
        case simplifyCTUF (CTUF x y) of
        CTUF CNil CNil -> z
        CTUF CNil y'   -> Zig y' z
        CTUF x'   CNil -> Zag x' z
        CTUF x'   y'   -> Zag x' (Zig y' z)

----------------------------------------------------------------
----------------------------------------------------------- fin.
