{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
-----------------------------------------------------------------------------
-- |
-- Module: Data.Row.Switch
--
-- This module provides the ability to discharge a polymorphic variant using
-- a record that has matching fields.
--
-----------------------------------------------------------------------------


module Data.Row.Switch
  ( AppliesTo(..)
  , switch
  , caseon
  )
where

import Data.Proxy

import Data.Bifunctor (Bifunctor(..))
import Data.Row.Internal
import Data.Row.Records
import Data.Row.Variants

-- | A simple class that we use to provide a constraint for function application.
class AppliesTo r f x | r x -> f, f r -> x where
  applyTo :: f -> x -> r
instance AppliesTo r (x -> r) x where
  applyTo = ($)

-- | A pair of a record and a variant.
data SwitchData r v = SwitchData (Rec r) (Var v)

-- | Like 'Const' but for two ignored type arguments.
newtype Const2 x y z = Const2 { getConst2 :: x }

-- | A 'Var' and a 'Rec' can combine if their rows line up properly.
-- Given a Variant along with a Record of functions from each possible value
-- of the variant to a single output type, apply the correct
-- function to the value in the variant.
switch :: forall v r x. BiForall r v (AppliesTo x) => Var v -> Rec r -> x
switch v r = getConst2 $ biMetamorph @_ @_ @r @v @(AppliesTo x) @Either @SwitchData @(Const2 x) @(Const2 x)
  Proxy doNil doUncons doCons $ SwitchData r v
  where
    doNil (SwitchData _ v) = impossible v
    doUncons :: forall ℓ f τ ϕ ρ. (KnownSymbol ℓ, AppliesTo x f τ, HasType ℓ f ϕ, HasType ℓ τ ρ)
             => Label ℓ -> SwitchData ϕ ρ -> Either (SwitchData (ϕ .- ℓ) (ρ .- ℓ)) (Const2 x f τ)
    doUncons l (SwitchData r v) = bimap (SwitchData $ lazyRemove l r) (Const2 . applyTo (r .! l)) $ trial v l
    -- doCons :: forall ℓ f τ ϕ ρ. (KnownSymbol ℓ, AppliesTo x f τ)
    --        => Label ℓ -> Either (Const2 x f τ) (Const2 x ϕ ρ) -> Const2 x (Extend ℓ f ϕ) (Extend ℓ τ ρ)
    doCons _ (Left  (Const2 x)) = Const2 x
    doCons _ (Right (Const2 x)) = Const2 x

-- | The same as 'switch' but with the argument order reversed
caseon :: forall v r x. BiForall r v (AppliesTo x) => Rec r -> Var v -> x
caseon = flip switch
