{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures   #-}
{-# LANGUAGE MonoLocalBinds   #-}
{-# LANGUAGE RankNTypes       #-}
{-# LANGUAGE TypeFamilies     #-}

module Network.IPFS.Git.RemoteHelper.Generic
    ( HKD
    , gvalidate
    , ginvalidate
    )
where

import           Generics.SOP

-- The venerable defunctionalisation, applied generically, with a hipster touch
-- as popularised by Sandy "type fam" Maguire ("Higher Kinded Data", HKD)

-- | Eliminates the boring identity functor 'I'
type family HKD (f :: * -> *) (a :: *) :: * where
    HKD I a = a
    HKD f a = f a

-- | Generically lift the applicative functor 'f' out of a structure.
--
-- Example:
--
-- Given a datatype parametrised over the functor 'Maybe', return 'Just' the
-- value if all 'Maybe' fields are 'Just', thereby changing the functor to 'I'.
-- 'Nothing' otherwise.
--
-- >>> :{
-- >>> data Person f = Person { name :: f String, age :: f Int }
-- >>>     deriving (Show, Generic, GHC.Generic)
-- >>> :}
-- >>> gvalidate $ Person (Just "LeBoeuf") (Just 42)
-- Just (Person (I "LeBoeuf") (I 42))
-- >>> gvalidate $ Person Nothing (Just 69)
-- Nothing
--
gvalidate
    :: ( Generic (a f)
       , Generic (a I)
       , Applicative f
       , AllZip2 (LiftedCoercible I f) (Code (a f)) (Code (a I))
       )
    => a f
    -> f (a I)
gvalidate = (to <$>) . hsequence . hcoerce . from

-- | Generically change the functor of a structure from 'I' to 'f', given a
-- constructor of 'f'.
--
-- Morally the inverse of 'gvalidate', although the specialisation to the
-- identity functor makes this trivial.
--
-- If 'a f' is a 'Semigroup', this can be used to apply defaults to a partial
-- value.
--
-- >>> :{
-- >>> instance Semigroup (Person Last) where
-- >>>     a <> b = Person { name = on (<>) name a b, age = on (<>) age a b }
-- >>> -- For convenience, also define 'Monoid'
-- >>> instance Monoid (Person Last) were
-- >>>     mempty  = Person mempty mempty
-- >>>     mappend = (<>)
-- >>> :}
-- >>> defaultPerson :: Person I
-- >>> defaultPerson = Person "Sandy" 28
-- >>> gvalidate $ ginvalidate pure defaultPerson <> Person (pure "Maguire") mempty
-- Just (Person (I "Maguire") (I 28)
--
ginvalidate
    :: ( Generic (a f)
       , Generic (a I)
       , AllZip2 (LiftedCoercible f I) (Code (a I)) (Code (a f))
       )
    => (forall b. b -> f b)
    -> a I
    -> a f
ginvalidate f = to . hcoerce . hmap (f . unI) . from