module Data.Bifunctor.BiInvariant
  ( -- * BiInvariant
    BiInvariant (..),
    biinvIso,
    type Coercible1,
    type Coercible2,
  )
where

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

import Control.Arrow (Arrow, Kleisli (Kleisli))
import Control.Category.Tensor (Iso (Iso))
import Control.Comonad (Cokleisli (Cokleisli))
import Data.Bifunctor (Bifunctor (bimap))
import Data.Bifunctor.Biap (Biap (Biap))
import Data.Bifunctor.Biff (Biff (Biff))
import Data.Bifunctor.Clown (Clown (Clown))
import Data.Bifunctor.Flip (Flip (Flip))
import Data.Bifunctor.Joker (Joker (Joker))
import Data.Bifunctor.Product (Product)
import Data.Bifunctor.Sum (Sum)
import Data.Bifunctor.Tannen (Tannen (Tannen))
import Data.Bifunctor.Wrapped (WrappedBifunctor (WrapBifunctor))
import Data.Coerce (Coercible)
import Data.Functor.Const (Const (Const))
import Data.Functor.Contravariant (Contravariant (contramap))
import Data.Kind (Constraint)
import Data.Profunctor (Costar (Costar), Forget (Forget), Profunctor (dimap), Star (Star), WrappedArrow (WrapArrow))
import Data.Profunctor.Cayley (Cayley (Cayley))
import Data.Profunctor.Choice (CopastroSum (CopastroSum), CotambaraSum, PastroSum, TambaraSum (TambaraSum))
import Data.Profunctor.Closed (Closure (Closure), Environment)
import Data.Profunctor.Composition (Procompose, Rift (Rift))
import Data.Profunctor.Mapping (CofreeMapping (CofreeMapping), FreeMapping)
import Data.Profunctor.Ran (Codensity (Codensity), Ran (Ran))
import Data.Profunctor.Strong (Copastro (Copastro), Cotambara, Pastro, Tambara (Tambara))
import Data.Profunctor.Traversing (CofreeTraversing (CofreeTraversing), FreeTraversing)
import Data.Profunctor.Yoneda (Coyoneda, Yoneda (Yoneda))
import Data.Semigroup (Arg)
import Data.Tagged (Tagged (Tagged))
import Data.These (These)
import GHC.Generics (K1)
import Prelude

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

-- | A bifunctor is 'BiInvariant' if it is parametric in both its type
-- parameters.
--
-- === Laws
--
-- @
-- 'biinvmap' 'id' 'id' 'id' 'id' ≡ 'id'
-- 'biinvmap' @g2@ @g2'@ @f2@ @f2'@ 'Control.Category..' 'Data.Functor.Invariant.invmap' @g1@ @g1'@ @f1@ @f1'@ ≡ 'Data.Functor.Invariant.invmap' (@g2@ 'Control.Category..' @g1@) (@g1'@ 'Control.Category..' @g2'@) (@f2@ 'Control.Category..' @f1@) (@f1'@ 'Control.Category..' @f2'@)
-- @
class BiInvariant p where
  -- | Used to apply a pair of isomorphic functions to @p a b@.
  -- 'Biinvmap' picks out the appropriate half of the iso depending if
  -- @p@ is covariant or contravariant on each parameter.
  --
  -- ==== __Examples__
  --
  -- >>> :t biinvmap @(,) (read @Int) show (read @Bool) show
  -- biinvmap @(,) (read @Int) show (read @Bool) show :: (Int, Bool) -> (String, String)
  --
  -- >>> biinvmap @(,) (read @Int) show (read @Bool) show (10, True)
  -- ("10","True")
  --
  -- >>> :t biinvmap @(->) (read @Int) show (read @Bool) show
  -- biinvmap @(->) (read @Int) show (read @Bool) show :: (Int -> Bool) -> String -> String
  --
  -- >>> biinvmap @(->) (read @Int) show (read @Bool) show (\i -> i > 10) "12"
  -- "True"
  biinvmap :: (a' -> a) -> (a -> a') -> (b' -> b) -> (b -> b') -> p a b -> p a' b'

-- | BiInvariant witnesses an Isomorphism
biinvIso :: BiInvariant p => Iso (->) a a' -> Iso (->) b b' -> Iso (->) (p a b) (p a' b')
biinvIso (Iso f f') (Iso g g') = Iso (biinvmap f' f g' g) (biinvmap f f' g g')

-- | Boilerplate newtype to derive 'BiInvariant' for any 'Profunctor'.
newtype FromProfunctor p a b = FromProfunctor { runPro :: p a b}

instance Profunctor p => BiInvariant (FromProfunctor p) where
  biinvmap :: (a' -> a) -> (a -> a') -> (b' -> b) -> (b -> b') -> FromProfunctor p a b -> FromProfunctor p a' b'
  biinvmap f _ _ g = FromProfunctor . dimap f g . runPro

-- | Boilerplate newtype to derive 'BiInvariant' for any 'Bifunctor'.
newtype FromBifunctor p a b = FromBifunctor { runBi :: p a b }

instance Bifunctor p => BiInvariant (FromBifunctor p) where
  biinvmap :: (a' -> a) -> (a -> a') -> (b' -> b) -> (b -> b') -> FromBifunctor p a b -> FromBifunctor p a' b'
  biinvmap _ f _ g = FromBifunctor . bimap f g . runBi

-- | Boilerplate newtype to derive 'BiInvariant' for any 'Contravariant'.
newtype FromContra f a = FromContra { runContra :: f a }

instance Contravariant f => Contravariant (FromContra f) where
  contramap :: Contravariant f => (a' -> a) -> FromContra f a -> FromContra f a'
  contramap f = FromContra . contramap f . runContra

newtype FromFunctor f a = FromFunctor (f a)
  deriving Functor

type Coercible1 f = ((forall a b. Coercible a b => Coercible (f a) (f b)) :: Constraint)
type Coercible2 f = (forall a b c d. (Coercible a b, Coercible c d) => Coercible (f a c) (f b d) :: Constraint)

deriving via (FromProfunctor (->))                   instance BiInvariant (->)
deriving via (FromProfunctor (Biff p f g))           instance (Profunctor p, Functor f, Functor g) => BiInvariant (Biff (FromProfunctor p) f g)
deriving via (FromProfunctor (Cayley f q))           instance (Functor f, Profunctor q) => BiInvariant (Cayley f q)
deriving via (FromProfunctor (Closure p))            instance Profunctor p => BiInvariant (Closure p)
deriving via (FromProfunctor (Clown f :: * -> * -> *)) instance Contravariant f => BiInvariant (Clown (FromContra f) :: * -> * -> *)
deriving via (FromProfunctor (Codensity p))          instance Profunctor p => BiInvariant (Codensity p)
deriving via (FromProfunctor (CofreeMapping p))      instance Profunctor p => BiInvariant (CofreeMapping p)
deriving via (FromProfunctor (CofreeTraversing p))   instance Profunctor p => BiInvariant (CofreeTraversing p)
deriving via (FromProfunctor (Cokleisli w))          instance Functor w => BiInvariant (Cokleisli w)
deriving via (FromProfunctor (Copastro p))           instance BiInvariant (Copastro p)
deriving via (FromProfunctor (CopastroSum p))        instance BiInvariant (CopastroSum p)
deriving via (FromProfunctor (Costar f))             instance Functor f => BiInvariant (Costar f)
deriving via (FromProfunctor (Cotambara p))          instance BiInvariant (Cotambara p)
deriving via (FromProfunctor (CotambaraSum p))       instance BiInvariant (CotambaraSum p)
deriving via (FromProfunctor (Coyoneda p))           instance BiInvariant (Coyoneda p)
deriving via (FromProfunctor (Environment p))        instance BiInvariant (Environment p)
deriving via (FromProfunctor (Forget r :: * -> * -> *)) instance BiInvariant (Forget r :: * -> * -> *)
deriving via (FromProfunctor (FreeMapping p))        instance BiInvariant (FreeMapping p)
deriving via (FromProfunctor (FreeTraversing p))     instance BiInvariant (FreeTraversing p)
deriving via (FromProfunctor (Joker f :: * -> * -> *))  instance Functor f => BiInvariant (Joker (FromContra f) :: * -> * -> *)
deriving via (FromProfunctor (Kleisli m))            instance Monad m => BiInvariant (Kleisli m)
deriving via (FromProfunctor (Pastro p))             instance BiInvariant (Pastro p)
deriving via (FromProfunctor (PastroSum p))          instance BiInvariant (PastroSum p)
deriving via (FromProfunctor (Procompose p q))       instance (Profunctor p, Profunctor q) => BiInvariant (Procompose p q)
deriving via (FromProfunctor (Product p q))          instance (Profunctor p, Profunctor q) => BiInvariant (Product (FromProfunctor p) (FromProfunctor q))
deriving via (FromProfunctor (Ran p q))              instance (Profunctor p, Profunctor q) => BiInvariant (Ran p q)
deriving via (FromProfunctor (Rift p q))             instance (Profunctor p, Profunctor q) => BiInvariant (Rift p q)
deriving via (FromProfunctor (Star f))               instance Functor f => BiInvariant (Star (FromFunctor f))
deriving via (FromProfunctor (Sum p q))              instance (Profunctor p, Profunctor q) => BiInvariant (Sum (FromProfunctor p) (FromProfunctor q))
deriving via (FromProfunctor (Tagged :: * -> * -> *)) instance BiInvariant (Tagged :: * -> * -> *)
deriving via (FromProfunctor (Tambara p))            instance Profunctor p => BiInvariant (Tambara p)
deriving via (FromProfunctor (TambaraSum p))         instance Profunctor p => BiInvariant (TambaraSum p)
deriving via (FromProfunctor (Tannen f q))           instance (Functor f, Profunctor q) => BiInvariant (Tannen f q)
deriving via (FromProfunctor (WrappedArrow p))       instance Arrow p => BiInvariant (WrappedArrow p)
deriving via (FromProfunctor (Yoneda p))             instance BiInvariant (Yoneda p)

deriving via (FromBifunctor ((,,) x1))                 instance BiInvariant ((,,) x1)
deriving via (FromBifunctor ((,,,) x1 x2))             instance BiInvariant ((,,,) x1 x2)
deriving via (FromBifunctor ((,,,,) x1 x2 x3))         instance BiInvariant ((,,,,) x1 x2 x3)
deriving via (FromBifunctor ((,,,,,) x1 x2 x3 x4))     instance BiInvariant ((,,,,,) x1 x2 x3 x4)
deriving via (FromBifunctor ((,,,,,,) x1 x2 x3 x4 x5)) instance BiInvariant ((,,,,,,) x1 x2 x3 x4 x5)
deriving via (FromBifunctor (,))                       instance BiInvariant (,)
deriving via (FromBifunctor Arg)                       instance BiInvariant Arg
deriving via (FromBifunctor (Biap bi))                 instance Bifunctor bi => BiInvariant (Biap bi)
deriving via (FromBifunctor (Biff p f g))              instance (Bifunctor p, Functor f, Functor g) => BiInvariant (Biff (FromBifunctor p) f g)
deriving via (FromBifunctor (Clown f :: * -> * -> *))  instance Functor f => BiInvariant (Clown (FromFunctor f) :: * -> * -> *)
deriving via (FromBifunctor (Const :: * -> * -> *))    instance BiInvariant (Const :: * -> * -> *)
deriving via (FromBifunctor Either)                    instance BiInvariant Either
deriving via (FromBifunctor (Flip p))                  instance Bifunctor p => BiInvariant (Flip p)
deriving via (FromBifunctor (Joker f :: * -> * -> *))  instance Functor f => BiInvariant (Joker (FromFunctor f) :: * -> * -> *)
deriving via (FromBifunctor (K1 i :: * -> * -> *))     instance BiInvariant (K1 i :: * -> * -> *)
deriving via (FromBifunctor (Product p q))             instance (Bifunctor p, Bifunctor q) => BiInvariant (Product (FromBifunctor p) (FromBifunctor q))
deriving via (FromBifunctor (Sum p q))                 instance (Bifunctor p, Bifunctor q) => BiInvariant (Sum (FromBifunctor p) (FromBifunctor q))
deriving via (FromBifunctor (Tannen f q))              instance (Functor f, Coercible1 f, Bifunctor q) => BiInvariant (Tannen (FromFunctor f) (FromBifunctor q))
deriving via (FromBifunctor These)                     instance BiInvariant These
deriving via (FromBifunctor (WrappedBifunctor p))      instance Bifunctor p => BiInvariant (WrappedBifunctor p)
