-- | Implementation of the primitives relating to Cubical identity types.
module Agda.TypeChecking.Primitive.Cubical.Id
  ( -- * General elimination form
    primIdElim'
  -- * Introduction form
  , primConId'
  -- * Projection maps (primarily used internally)
  , primIdFace'
  , primIdPath'
  -- * Kan operations
  , doIdKanOp
  )
  where

import Control.Monad.Except

import Agda.Utils.Impossible (__IMPOSSIBLE__)
import Agda.Utils.Monad

import qualified Data.IntMap as IntMap
import Data.Traversable
import Data.Maybe

import Agda.TypeChecking.Monad.Builtin
import Agda.TypeChecking.Monad.Base
import Agda.TypeChecking.Monad.Env
import Agda.TypeChecking.Substitute (apply, sort, listS, applySubst)
import Agda.TypeChecking.Reduce (reduceB', reduce')
import Agda.TypeChecking.Names
  (runNamesT, runNames, cl, lam, ilam, open)

import Agda.Interaction.Options.Base (optCubical)

import Agda.Syntax.Common (Cubical(..), Arg(..), defaultArgInfo, hasQuantity0, defaultArg)

import Agda.TypeChecking.Primitive.Base
  ((-->), nPi', pPi', hPi', el, el', el's, (<@>), (<#>), (<..>), argN)

import Agda.Syntax.Internal
import Agda.TypeChecking.Monad.Debug (__IMPOSSIBLE_VERBOSE__)

import Agda.TypeChecking.Primitive.Cubical.Base

-- | Primitive elimination rule for the cubical identity types. Unlike
-- J, @idElim@ makes explicit the structure of Swan's identity types as
-- being pairs of a cofibration and a path. Moreover, it records that
-- the path is definitionally @refl@ under that cofibration.
primIdElim' :: TCM PrimitiveImpl
primIdElim' = do
  -- The implementation here looks terrible but most of it is actually
  -- the type.
  requireCubical CErased ""
  t <- runNamesT [] $
    hPi' "a" (el $ cl primLevel) $ \ a ->
    hPi' "c" (el $ cl primLevel) $ \ c ->
    hPi' "A" (sort . tmSort <$> a) $ \ bA ->
    hPi' "x" (el' a bA) $ \ x ->
    nPi' "C" (nPi' "y" (el' a bA) $ \ y ->
              el' a (cl primId <#> a <#> bA <@> x <@> y) --> (sort . tmSort <$> c)) $ \ bC ->
    -- To construct (C : (y : A) → Id A x y → Type c), it suffices to:

    -- For all cofibrations φ,
    nPi' "φ" primIntervalType (\ phi ->
      -- For all y : A [ φ → (λ _ → x) ]
      nPi' "y" (el's a $ cl primSub <#> a <@> bA <@> phi <@> lam "o" (const x)) $ \ y ->
      let pathxy = cl primPath <#> a <@> bA <@> x <@> outSy
          outSy  = cl primSubOut <#> a <#> bA <#> phi <#> lam "o" (const x) <@> y
          reflx  = lam "o" $ \ _ -> lam "i" $ \ _ -> x -- TODO Andrea, should block on o
      -- For all w : (Path A x (outS y)) [ φ (λ _ → refl {x = outS y} ]
      in nPi' "w" (el's a $ cl primSub <#> a <@> pathxy <@> phi <@> reflx) $ \ w ->
      let outSw = (cl primSubOut <#> a <#> pathxy <#> phi <#> reflx <@> w)
      in el' c $ bC <@> outSy <@> (cl primConId <#> a <#> bA <#> x <#> outSy <@> phi <@> outSw))
      -- Construct an inhabitant of (C (outS y) (conid φ (outS w)))
    -->
    hPi' "y" (el' a bA) (\ y ->
      nPi' "p" (el' a $ cl primId <#> a <#> bA <@> x <@> y) $ \ p ->
      el' c $ bC <@> y <@> p)

  -- Implementation starts here:
  conid <- primConId
  sin <- primSubIn
  path <- primPath
  return $ PrimImpl t $ primFun __IMPOSSIBLE__ 8 $ \case
    [a,c,bA,x,bC,f,y,p] -> do
      sp <- reduceB' p
      cview <- conidView'
      case cview (unArg x) $ unArg $ ignoreBlocking sp of
        -- Record that the right endpoint and the path definitionally
        -- agree with x φ holds. This is guaranteed internally by the
        -- typing rule for @conId@ but can't be recovered from
        -- @primIdPath@ and @primIdFace@ (see #2598)
        Just (phi, w) -> do
          let y' = sin `apply` [a, bA, phi, argN (unArg y)]
          let w' = sin `apply` [a, argN (path `apply` [a, bA, x, y]), phi, argN (unArg w)]
          redReturn $ unArg f `apply` [phi, defaultArg y', defaultArg w']
        _ -> return $ NoReduction $ map notReduced [a,c,bA,x,bC,f,y] ++ [reduced sp]
    _ -> __IMPOSSIBLE_VERBOSE__ "implementation of primIdElim called with wrong arity"

-- | Introduction form for the cubical identity types.
primConId' :: TCM PrimitiveImpl
primConId' = do
  requireCubical CErased ""
  t <- runNamesT [] $
    hPi' "a" (el $ cl primLevel) $ \ a ->
    hPi' "A" (sort . tmSort <$> a) $ \ bA ->
    hPi' "x" (el' a bA) $ \ x ->
    hPi' "y" (el' a bA) $ \ y ->
    primIntervalType -- Cofibration
    --> (el' a $ cl primPath <#> a <#> bA <@> x <@> y)
    --> (el' a $ cl primId <#> a <#> bA <@> x <@> y)

  -- Implementation note: conId, as the name implies, is a constructor.
  -- It's not represented as a constructor because users can't match on
  -- it (but we, internally, can: see createMissingConIdClause).

  return $ PrimImpl t $ primFun __IMPOSSIBLE__ 6 $ \case
    [l,bA,x,y,phi,p] -> do
      sphi <- reduceB' phi
      view <- intervalView'
      case view $ unArg $ ignoreBlocking sphi of
        -- But even though it's a constructor, it does reduce, in some
        -- cases: If the cofibration is definitely true, then we return
        -- reflId.  TODO: Handle this in the conversion checker instead?
        IOne -> do
          reflId <- getTerm builtinConId builtinReflId
          redReturn $ reflId
        _ -> return $ NoReduction $ map notReduced [l,bA,x,y] ++ [reduced sphi, notReduced p]
    _ -> __IMPOSSIBLE_VERBOSE__ "implementation of primConId called with wrong arity"

-- | Extract the underlying cofibration from an inhabitant of the
-- cubical identity types.
--
-- TODO (Amy, 2022-08-17): Projecting a cofibration from a Kan type
-- violates the cubical phase distinction.
primIdFace' :: TCM PrimitiveImpl
primIdFace' = do
  requireCubical CErased ""
  t <- runNamesT [] $
    hPi' "a" (el $ cl primLevel) $ \ a ->
    hPi' "A" (sort . tmSort <$> a) $ \ bA ->
    hPi' "x" (el' a bA) $ \ x ->
    hPi' "y" (el' a bA) $ \ y ->
    el' a (cl primId <#> a <#> bA <@> x <@> y)
    --> primIntervalType

  return $ PrimImpl t $ primFun __IMPOSSIBLE__ 5 $ \case
    [l,bA,x,y,t] -> do
      st <- reduceB' t
      mConId <- getName' builtinConId
      cview <- conidView'
      case cview (unArg x) $ unArg (ignoreBlocking st) of
        Just (phi, _) -> redReturn (unArg phi)
        _ -> return $ NoReduction $ map notReduced [l,bA,x,y] ++ [reduced st]
    _ -> __IMPOSSIBLE__

-- | Extract the underlying path from an inhabitant of the
-- cubical identity types.
primIdPath' :: TCM PrimitiveImpl
primIdPath' = do
  requireCubical CErased ""
  t <- runNamesT [] $
    hPi' "a" (el $ cl primLevel) $ \ a ->
    hPi' "A" (sort . tmSort <$> a) $ \ bA ->
    hPi' "x" (el' a bA) $ \ x ->
    hPi' "y" (el' a bA) $ \ y ->
    el' a (cl primId <#> a <#> bA <@> x <@> y)
    --> el' a (cl primPath <#> a <#> bA <@> x <@> y)

  return $ PrimImpl t $ primFun __IMPOSSIBLE__ 5 $ \case
    [l,bA,x,y,t] -> do
      st <- reduceB' t
      mConId <- getName' builtinConId
      cview <- conidView'
      case cview (unArg x) $ unArg (ignoreBlocking st) of
        Just (_, w) -> redReturn (unArg w)
        _ -> return $ NoReduction $ map notReduced [l,bA,x,y] ++ [reduced st]
    _ -> __IMPOSSIBLE__

-- | Check that a term matches a given predicate on every consistent
-- substitution of interval variables which makes the given cofibration
-- hold.
onEveryFace
  :: Term -- ^ The cofibration @φ@
  -> Term -- ^ The term to test
  -> (Term -> Bool)
  -- ^ The predicate to test with.
  -> ReduceM Bool
onEveryFace phi u p = do
  unview <- intervalUnview'
  let boolToI b = if b then unview IOne else unview IZero
  as <- decomposeInterval phi
  bools <- for as $ \ (bs,ts) -> do
    let u' = listS (IntMap.toAscList $ IntMap.map boolToI bs) `applySubst` u
    t <- reduce2Lam u'
    return $! p $ ignoreBlocking t
  pure (and bools)

doIdKanOp
  :: KanOperation           -- ^ Are we composing or transporting?
  -> FamilyOrNot (Arg Term) -- ^ Level argument
  -> FamilyOrNot (Arg Term, Arg Term, Arg Term)
    -- ^ Domain, left and right endpoints of the identity type
  -> ReduceM (Maybe (Reduced t Term))
doIdKanOp kanOp l bA_x_y = do
  let getTermLocal = getTerm $ kanOpName kanOp ++ " for " ++ builtinId

  unview <- intervalUnview'
  mConId <- getName' builtinConId
  cview <- conidView'
  let isConId t = isJust $ cview __DUMMY_TERM__ t

  sa0 <- reduceB' (kanOpBase kanOp)
  -- TODO: wasteful to compute b even when cheaper checks might fail
  --
  -- Should we go forward with the Kan operation? This is the case when
  -- doing transport always, and when every face fo the partial element
  -- has reduced to @conid@ otherwise. Note that @conidView@ treats
  -- @reflId@ as though it were @conid i1 refl@.
  b <- case kanOp of
    TranspOp{}    -> return True
    HCompOp _ u _ ->
      onEveryFace (unArg . ignoreBlocking . kanOpCofib $ kanOp) (unArg u) isConId

  case mConId of
    Just conid | isConId (unArg . ignoreBlocking $ sa0), b -> (Just <$>) . (redReturn =<<) $ do
      tHComp    <- getTermLocal builtinHComp
      tTrans    <- getTermLocal builtinTrans
      tIMin     <- getTermLocal "primDepIMin"
      idFace    <- getTermLocal "primIdFace"
      idPath    <- getTermLocal "primIdPath"
      tPathType <- getTermLocal builtinPath
      tConId    <- getTermLocal builtinConId

      runNamesT [] $ do
        let
          io = pure $ unview IOne
          iz = pure $ unview IZero
          conId = pure tConId

          eval TranspOp{} l bA phi _ u0 =
            pure tTrans <#> l <@> bA <@> phi <@> u0
          eval HCompOp{} l bA phi u u0 =
            pure tHComp <#> (l <@> io) <#> (bA <@> io) <#> phi <@> u <@> u0

        -- Compute a line of levels. So we can invoke 'eval' uniformly.
        l <- case l of
          IsFam l -> open . unArg $ l
          IsNot l -> open (Lam defaultArgInfo $ NoAbs "_" $ unArg l)

        p0 <- open . unArg $ kanOpBase kanOp

        -- p is the partial element we are extending against. This is
        -- used to compute the resulting cofibration, so we fake a
        -- partial element when doing transport.
        p <- case kanOp of
          HCompOp _ u _ -> do
            u <- open . unArg $ u
            pure $ \i o -> u <@> i <..> o
          TranspOp{} -> do
            pure $ \i o -> p0

        phi <- open . unArg . ignoreBlocking $ kanOpCofib kanOp

        -- Similarly to the fake line of levels above, fake lines of
        -- everything even when we're doing composition, for uniformity
        -- of eval.
        [bA, x, y] <- case bA_x_y of
          IsFam (bA, x, y) -> for [bA, x, y] $ \a ->
            open $ runNames [] $ lam "i" (const (pure $ unArg a))
          IsNot (bA, x, y) -> for [bA, x, y] $ \a ->
            open (Lam defaultArgInfo $ NoAbs "_" $ unArg a)

        -- The resulting path is constant when when
        --    @Σ φ λ o → -- primIdFace p i1 o@
        -- holds. That's why cofibrations have to be closed under Σ,
        -- c.f. primDepIMin.
        cof <- pure tIMin
          <@> phi
          <@> ilam "o" (\o ->
            pure idFace <#> (l <@> io) <#> (bA <@> io) <#> (x <@> io) <#> (y <@> io) <@> (p io o))

        -- Do the Kan operation for our faces in the Path type.
        path <- eval kanOp l
          (lam "i" $ \i -> pure tPathType <#> (l <@> i) <#> (bA <@> i) <@> (x <@> i) <@> (y <@> i))
          phi
          (lam "i" $ \i -> ilam "o" $ \o ->
            pure idPath <#> (l <@> i) <#> (bA <@> i) <#> (x <@> i) <#> (y <@> i) <@> (p i o))
          (pure idPath <#> (l <@> iz) <#> (bA <@> iz) <#> (x <@> iz) <#> (y <@> iz) <@> p0)

        conId <#> (l <@> io) <#> (bA <@> io) <#> (x <@> io) <#> (y <@> io)
          <@> pure cof
          <@> pure path
    _ -> return $ Nothing
