{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE PatternSynonyms     #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE TypeInType          #-}
{-# LANGUAGE TypeOperators       #-}

-- |
-- Module      : Data.Type.Functor.XProduct
-- Copyright   : (c) Justin Le 2018
-- License     : BSD3
--
-- Maintainer  : justin@jle.im
-- Stability   : experimental
-- Portability : non-portable
--
-- Generalize "Data.Vinyl.XRec": provides a version of products in
-- "Data.Type.Functor.Product" that "erases" newtype wrappers and other
-- syntactical noise.
--
-- "Data.Type.Functor.Product" is the "main functionality", but this module
-- provides an alternative interface that may be more convenient in some
-- situations, in the same way that 'XRec' can be more convenient than
-- 'Rec' in some situations.
--
module Data.Type.Functor.XProduct (
    XProd
  , fromXProd
  , toXProd
  -- * Functions
  , mapProdX, mapProdXEndo
  , imapProdX, zipWithProdX
  , ixProdX, traverseProdX, traverseProdXEndo, itraverseProdX
  , foldMapProdX, ifoldMapProdX
  -- * Instances
  , XRec, pattern (::&), pattern XRNil
  , XMaybe, pattern XNothing, pattern XJust
  , XEither, pattern XLeft, pattern XRight
  , XNERec, pattern (::&|)
  , XTup, pattern XTup
  , XIdentity, pattern XIdentity
  ) where

import           Data.Functor.Identity
import           Data.Kind
import           Data.List.NonEmpty        (NonEmpty(..))
import           Data.Type.Functor.Product
import           Data.Vinyl
import           Data.Vinyl.XRec
import           Lens.Micro
import qualified Data.Vinyl.Functor        as V

-- | Generalize 'XRec' to work over any foldable @f@ that implements
-- 'FProd'.  See 'Prod' and 'FProd' for more information.
type XProd f g = (Prod f (XData g) :: f k -> Type)

-- | Convert an 'XProd' back into a regular ol' 'Prod'.
fromXProd :: forall f g as. (FProd f, PureProdC f (IsoHKD g) as) => XProd f g as -> Prod f g as
fromXProd = zipWithProd (\(V.Lift u) x -> u x)
              (pureProdC @_ @(IsoHKD g) (V.Lift (unHKD . unX)))

-- | Convert a 'Prod' into a fancy 'XProd'.
toXProd :: forall f g as. (FProd f, PureProdC f (IsoHKD g) as) => Prod f g as -> XProd f g as
toXProd = zipWithProd (\(V.Lift u) x -> u x)
              (pureProdC @_ @(IsoHKD g) (V.Lift (XData . toHKD)))

-- | Convenient wrapper over 'mapProd' that lets you deal with the
-- "simplified" inner types.  Generalizes 'rmapX'.
mapProdX
    :: forall f g h as. FProd f
    => (forall a. HKD g a -> HKD h a)
    -> XProd f g as
    -> XProd f h as
mapProdX f = mapProd $ \(XData x :: XData g a) -> XData (f @a x)

-- | A version of 'mapProdX' that doesn't change the context @g@; this can
-- be easier for type inference in some situations.  Generalizes
-- 'rmapXEndo'.
mapProdXEndo
    :: forall f g as. FProd f
    => (forall a. HKD g a -> HKD g a)
    -> XProd f g as
    -> XProd f g as
mapProdXEndo f = mapProd $ \(XData x :: XData g a) -> XData (f @a x)

-- | A version of 'mapProdX' that passes along the index 'Elem' with each
-- value.  This can help with type inference in some situations.
imapProdX
    :: forall f g h as. FProd f
    => (forall a. Elem f as a -> HKD g a -> HKD h a)
    -> XProd f g as
    -> XProd f h as
imapProdX f = imapProd $ \i -> XData . f i . unX

-- | Zip two 'XProd's together by supplying a function that works on their
-- simplified 'HKD' values.
zipWithProdX
    :: forall f g h j as. FProd f
    => (forall a. HKD g a -> HKD h a -> HKD j a)
    -> XProd f g as
    -> XProd f h as
    -> XProd f j as
zipWithProdX f = zipWithProd $ \(XData x :: XData g a) (XData y) -> XData (f @a x y)

-- | Given an index into an 'XProd', provides a lens into the simplified
-- item that that index points to.
ixProdX
    :: FProd f
    => Elem f as a
    -> Lens' (XProd f g as) (HKD g a)
ixProdX i = ixProd i . (\f (XData x) -> XData <$> f x)

-- | Convenient wrapper over 'traverseProd' that lets you deal with the
-- "simplified" inner types.
traverseProdX
    :: forall f g h m as. (FProd f, Applicative m)
    => (forall a. HKD g a -> m (HKD h a))
    -> XProd f g as
    -> m (XProd f h as)
traverseProdX f = traverseProd $ \(XData x :: XData g a) -> XData <$> f @a x

-- | A version of 'traverseProdX' that doesn't change the context @g@; this can
-- be easier for type inference in some situations.
traverseProdXEndo
    :: forall f g m as. (FProd f, Applicative m)
    => (forall a. HKD g a -> m (HKD g a))
    -> XProd f g as
    -> m (XProd f g as)
traverseProdXEndo f = traverseProd $ \(XData x :: XData g a) -> XData <$> f @a x

-- | A version of 'traverseProdX' that passes along the index 'Elem' with
-- each value.  This can help with type inference in some situations.
itraverseProdX
    :: forall f g h m as. (FProd f, Applicative m)
    => (forall a. Elem f as a -> HKD g a -> m (HKD h a))
    -> XProd f g as
    -> m (XProd f h as)
itraverseProdX f = itraverseProd $ \i -> fmap XData . f i . unX

-- | Convenient wrapper over 'foldMapProd' that lets you deal with the
-- "simplified" inner types.
foldMapProdX
    :: forall f g m as. (FProd f, Monoid m)
    => (forall a. HKD g a -> m)
    -> XProd f g as
    -> m
foldMapProdX f = foldMapProd $ \(XData x :: XData g a) -> f @a x

-- | A version of 'foldMapProdX' that passes along the index 'Elem' with
-- each value.  This can help with type inference in some situations.
ifoldMapProdX
    :: forall f g m as. (FProd f, Monoid m)
    => (forall a. Elem f as a -> HKD g a -> m)
    -> XProd f g as
    -> m
ifoldMapProdX f = ifoldMapProd $ \i -> f i . unX

-- | 'PMaybe' over 'HKD'-d types.
type XMaybe f    = PMaybe (XData f)

-- | 'PEither' over 'HKD'-d types.
type XEither f   = PEither (XData f)

-- | 'NERec' over 'HKD'-d types.
type XNERec f    = NERec (XData f)

-- | 'PTup' over 'HKD'-d types.
type XTup f      = PTup (XData f)

-- | 'PIdentity' over 'HKD'-d types.
type XIdentity f = PIdentity (XData f)

-- | 'PNothing' for 'XMaybe'.
pattern XNothing :: XMaybe f 'Nothing
pattern XNothing = PNothing

-- | 'PJust' for 'XMaybe': allows you to provide the simplified type.
pattern XJust :: HKD f a -> XMaybe f ('Just a)
pattern XJust x = PJust (XData x)

-- | 'PLeft' for 'XEither'.
pattern XLeft :: Sing e -> XEither f ('Left e)
pattern XLeft e = PLeft e

-- | 'PRight' for 'XEither': allows you to provide the simplified type.
pattern XRight :: HKD f a -> XEither f ('Right a)
pattern XRight x = PRight (XData x)

-- | A version of ':&|' that allows you to provide the simplified type, for
-- 'XNERec'.
pattern (::&|) :: HKD f a -> XRec f as -> XNERec f (a ':| as)
pattern x ::&| xs = XData x :&| xs

-- | A version of 'PTup' that allows you to provide the simplified type,
-- for 'XTup'.
pattern XTup :: Sing w -> HKD f a -> XTup f '(w, a)
pattern XTup w x = PTup w (XData x)

-- | A version of 'PIdentity' that allows you to provide the simplified
-- type, for 'XIdentity'.
pattern XIdentity :: HKD f a -> XIdentity f ('Identity a)
pattern XIdentity x = PIdentity (XData x)

{-# COMPLETE (::&|)    #-}
{-# COMPLETE XIdentity #-}
{-# COMPLETE XJust     #-}
{-# COMPLETE XLeft     #-}
{-# COMPLETE XNothing  #-}
{-# COMPLETE XRight    #-}
{-# COMPLETE XTup      #-}