{-# LANGUAGE AllowAmbiguousTypes     #-}
{-# LANGUAGE DeriveGeneric           #-}
{-# LANGUAGE FlexibleContexts        #-}
{-# LANGUAGE FlexibleInstances       #-}
{-# LANGUAGE FunctionalDependencies  #-}
{-# LANGUAGE LambdaCase              #-}
{-# LANGUAGE MultiParamTypeClasses   #-}
{-# LANGUAGE ScopedTypeVariables     #-}
{-# LANGUAGE TypeApplications        #-}
{-# LANGUAGE TypeFamilies            #-}
{-# LANGUAGE TypeFamilyDependencies  #-}
{-# LANGUAGE UndecidableSuperClasses #-}


module Control.Isomorphic where


import           Control.Applicative
import           Control.Arrow
import qualified Control.Monad.ST.Lazy      as SL
import qualified Control.Monad.ST.Strict    as SS
import qualified Control.Newtype.Generics   as N
import qualified Data.ByteString            as BS
import qualified Data.ByteString.Char8      as BSC
import qualified Data.ByteString.Lazy       as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
import           Data.Coerce
import           Data.Fixed
import           Data.Functor.Compose
import           Data.Functor.Identity
import qualified Data.List.NonEmpty         as NE
import           Data.Monoid
import           Data.Profunctor            (Profunctor (..))
import qualified Data.Text                  as TS
import qualified Data.Text.Encoding         as TSE
import qualified Data.Text.Lazy             as TL
import qualified Data.Text.Lazy.Encoding    as TLE
import           GHC.Generics               (C1, D1, Generic, K1 (..), M1 (..),
                                             S1)


class Isomorphic b a => Isomorphic a b where
  {-|
   This type class provides a single method `to'`, which is a __bijective__ function from `a` to `b`.
   We leverage `UndecidableSuperClasses` to demand that every instance of `Isomorphic` have an inverse.
   The sister instance (which may be the same instance) must have the follwing property when used with
   the `to'`

   @
     to' . to' = id
   @

   Note, this class documents type isomorphisms specifically, where two types are isomorphic to one another.
   It does not document endoisomorphisms.
  -}
  to' :: a -> b

instance Isomorphic (D1 d (C1 c (S1 s (K1 i a))) a) a where
  to' (M1 (M1 (M1 (K1 x)))) = x
instance Isomorphic a (D1 d (C1 c (S1 s (K1 i a))) a) where
  to' x = (M1 (M1 (M1 (K1 x))))

-- | Composition of two bijections
via :: forall b a c. (Isomorphic b c, Isomorphic a b) => a -> c
via = from @b . to
{-# INLINE via #-}

{-| This is the perfer version of the `to'` function.
    The order of arguments in @forall@ has been set up so that the codomain is first.
    This is important to work with `TypeApplications`.

    @
      to @Text "hi there"
    @

    For call sites where we need to annotate.
-}
to :: forall b a. Isomorphic a b => a -> b
to = to'
{-# INLINE to #-}

{-| This is the same as `to` but has the @forall@ arguments with @a@ first.
-}
from :: forall a b. Isomorphic a b => a -> b
from = to'
{-# INLINE from #-}

instance {-# OVERLAPPABLE #-} Isomorphic a a where to' = id
                                                   {-# INLINE to' #-}

instance Isomorphic (a,b) (b,a) where to' (a,b) = (b,a)
                                      {-# INLINABLE to' #-}


instance Isomorphic (a,(b,c)) ((a,b),c) where to' (a,(b,c)) = ((a,b),c)
                                              {-# INLINABLE to' #-}
instance Isomorphic ((a,b),c) (a,(b,c)) where to' ((a,b),c) = (a,(b,c))
                                              {-# INLINABLE to' #-}

instance Isomorphic (a,b,c) (a,(b,c))   where to' (a,b,c) = (a,(b,c))
                                              {-# INLINABLE to' #-}
instance Isomorphic (a,(b,c)) (a,b,c)   where to' (a,(b,c)) = (a,b,c)
                                              {-# INLINABLE to' #-}

instance Isomorphic (a,b,c) ((a,b),c)   where to' (a,b,c) = ((a,b),c)
                                              {-# INLINABLE to' #-}
instance Isomorphic ((a,b),c) (a,b,c)   where to' ((a,b),c) = (a,b,c)
                                              {-# INLINABLE to' #-}

-- String -> BS BL TS TL

instance Isomorphic BS.ByteString String where
  to' = BSC.unpack
  {-# INLINE to' #-}
instance Isomorphic String BS.ByteString where
  to' = BSC.pack
  {-# INLINE to' #-}

instance Isomorphic BL.ByteString String where
  to' = BLC.unpack
  {-# INLINE to' #-}
instance Isomorphic String BL.ByteString where
  to' = BLC.pack
  {-# INLINE to' #-}

instance Isomorphic TS.Text String where
  to' = TS.unpack
  {-# INLINE to' #-}
instance Isomorphic String TS.Text where
  to' = TS.pack
  {-# INLINE to' #-}

instance Isomorphic TL.Text String where
  to' = TL.unpack
  {-# INLINE to' #-}
instance Isomorphic String TL.Text where
  to' = TL.pack
  {-# INLINE to' #-}

-- TS -> BS BL TL

instance Isomorphic TS.Text BS.ByteString where
  to' = TSE.encodeUtf8
  {-# INLINE to' #-}
instance Isomorphic BS.ByteString TS.Text where
  to' = TSE.decodeUtf8
  {-# INLINE to' #-}

instance Isomorphic TS.Text BL.ByteString where
  to' = BL.fromStrict . to
  {-# INLINE to' #-}
instance Isomorphic BL.ByteString TS.Text where
  to' = to' . BL.toStrict
  {-# INLINE to' #-}

instance Isomorphic TS.Text TL.Text where
  to' = TL.fromStrict
  {-# INLINE to' #-}
instance Isomorphic TL.Text TS.Text where
  to' = TL.toStrict
  {-# INLINE to' #-}

-- TL -> BS BL


instance Isomorphic TL.Text BS.ByteString where
  to' = BL.toStrict . to
  {-# INLINE to' #-}
instance Isomorphic BS.ByteString TL.Text where
  to' = to' . BL.fromStrict
  {-# INLINE to' #-}

instance Isomorphic TL.Text BL.ByteString where
  to' = TLE.encodeUtf8
  {-# INLINE to' #-}
instance Isomorphic BL.ByteString TL.Text where
  to' = TLE.decodeUtf8
  {-# INLINE to' #-}

-- BS -> BL

instance Isomorphic BS.ByteString BL.ByteString where
  to' = BL.fromStrict
  {-# INLINE to' #-}
instance Isomorphic BL.ByteString BS.ByteString where
  to' = BL.toStrict
  {-# INLINE to' #-}

instance Isomorphic (Maybe a) (Either () a) where
  to' = \case Just a -> Right a; _ -> Left ()
instance Isomorphic (Either () a) (Maybe a) where
  to' = \case Right a -> Just a; _ -> Nothing

instance Isomorphic (Maybe a) (Either a ()) where
  to' = \case Just a -> Left a; _ -> Right ()
instance Isomorphic (Either a ()) (Maybe a) where
  to' = \case Left a -> Just a; _ -> Nothing

instance Isomorphic (a -> b -> c) ((a,b) -> c) where to' = uncurry
                                                     {-# INLINE to' #-}
instance Isomorphic ((a,b) -> c) (a -> b -> c) where to' = curry
                                                     {-# INLINE to' #-}

instance Isomorphic (a -> b -> c) (b -> a -> c) where to' = flip
                                                      {-# INLINE to' #-}

instance Isomorphic (Either a b) (Either b a) where
  to' = \case Right x -> Left x; Left x -> Right x

instance Isomorphic (NE.NonEmpty a) (a, [a]) where to' (x NE.:| xs) = (x,xs)
instance Isomorphic (a, [a]) (NE.NonEmpty a) where to' (x,xs) = x NE.:| xs

-- | A wrapper for @Either () ()@ that decides the isomorphism to `Bool` is `Right` biased
newtype IsRight = IsRight { unIsRight :: Either () () } deriving (Show, Generic)
-- | A wrapper for @Either () ()@ that decides the isomorphism to `Bool` is `Left` biased
newtype IsLeft  = IsLeft { unIsLeft :: Either () () } deriving (Show, Generic)
instance Isomorphic IsRight (Either () ()) where to' = unIsRight
instance Isomorphic (Either () ()) IsRight where to' = IsRight
instance Isomorphic IsLeft (Either () ()) where to' = unIsLeft
instance Isomorphic (Either () ()) IsLeft where to' = IsLeft
instance Isomorphic Bool IsRight where
   to' True = IsRight (Right ())
   to' _    = IsRight (Left ())
instance Isomorphic IsRight Bool where
   to' (IsRight (Right ())) = True
   to' _                    = False
instance Isomorphic Bool IsLeft where
   to' True = IsLeft (Left ())
   to' _    = IsLeft (Right ())
instance Isomorphic IsLeft Bool where
   to' (IsLeft (Left ())) = True
   to' _                  = False

instance Enum a => Isomorphic a Int where to' = fromEnum
                                          {-# INLINE to' #-}
instance Enum a => Isomorphic Int a where to' = toEnum
                                          {-# INLINE to' #-}

instance Isomorphic (SL.ST s a) (SS.ST s a) where to' = SL.lazyToStrictST
                                                  {-# INLINE to' #-}
instance Isomorphic (SS.ST s a)  (SL.ST s a) where to' = SL.strictToLazyST
                                                   {-# INLINE to' #-}

-- | A wrapper for @Maybe ()@ that decides the isomorphism to `Bool` is `Just` biased
newtype IsJust = IsJust { unIsJust :: Maybe () } deriving Show
-- | A wrapper for @Maybe ()@ that decides the isomorphism to `Bool` is `Nothing` biased
newtype IsNothing = IsNothing { unIsNothing :: Maybe () } deriving Show
instance Isomorphic IsJust (Maybe ()) where to' = unIsJust
instance Isomorphic (Maybe ()) IsJust where to' = IsJust
instance Isomorphic IsNothing (Maybe ()) where to' = unIsNothing
instance Isomorphic (Maybe ()) IsNothing  where to' = IsNothing
instance Isomorphic Bool IsJust where
   to' True = IsJust (Just ())
   to' _    = IsJust Nothing
instance Isomorphic IsJust Bool where
   to' (IsJust (Just ())) = True
   to' _                  = False
instance Isomorphic Bool IsNothing where
   to' True = IsNothing Nothing
   to' _    = IsNothing (Just ())
instance Isomorphic IsNothing Bool where
   to' (IsNothing Nothing) = True
   to' _                   = False

instance Isomorphic (WrappedMonad m a) (m a) where to' = N.unpack
                                                   {-# INLINE to' #-}
instance Isomorphic (m a) (WrappedMonad m a) where to' = N.pack
                                                   {-# INLINE to' #-}
instance Isomorphic (WrappedArrow a b c) (a b c) where to' = N.unpack
                                                       {-# INLINE to' #-}
instance Isomorphic (a b c) (WrappedArrow a b c) where to' = N.pack
                                                       {-# INLINE to' #-}
instance Isomorphic (ZipList a) [a] where to' = N.unpack
                                          {-# INLINE to' #-}
instance Isomorphic [a] (ZipList a) where to' = N.pack
                                          {-# INLINE to' #-}
instance Isomorphic (Kleisli m a b) (a -> m b) where to' = N.unpack
                                                     {-# INLINE to' #-}
instance Isomorphic (a -> m b) (Kleisli m a b) where to' = N.pack
                                                     {-# INLINE to' #-}
instance Isomorphic (ArrowMonad a b) (a () b) where to' = N.unpack
                                                    {-# INLINE to' #-}
instance Isomorphic (a () b) (ArrowMonad a b) where to' = N.pack
                                                    {-# INLINE to' #-}
instance Isomorphic (Fixed a) Integer where to' = N.unpack
                                            {-# INLINE to' #-}
instance Isomorphic Integer (Fixed a) where to' = N.pack
                                            {-# INLINE to' #-}
instance Isomorphic (Compose f g a) (f (g a)) where to' = N.unpack
                                                    {-# INLINE to' #-}
instance Isomorphic (f (g a)) (Compose f g a) where to' = N.pack
                                                    {-# INLINE to' #-}
instance Isomorphic (Const a x) a where to' = N.unpack
                                        {-# INLINE to' #-}
instance Isomorphic a (Const a x) where to' = N.pack
                                        {-# INLINE to' #-}
instance Isomorphic (Identity a) a where to' = N.unpack
                                         {-# INLINE to' #-}
instance Isomorphic a (Identity a) where to' = N.pack
                                         {-# INLINE to' #-}
instance Isomorphic (Dual a) a where to' = N.unpack
                                     {-# INLINE to' #-}
instance Isomorphic a (Dual a) where to' = N.pack
                                     {-# INLINE to' #-}
instance Isomorphic (Endo a) (a -> a) where to' = N.unpack
                                            {-# INLINE to' #-}
instance Isomorphic (a -> a) (Endo a) where to' = N.pack
                                            {-# INLINE to' #-}
instance Isomorphic All Bool where to' = N.unpack
                                   {-# INLINE to' #-}
instance Isomorphic Bool All where to' = N.pack
                                   {-# INLINE to' #-}
instance Isomorphic Any Bool where to' = N.unpack
                                   {-# INLINE to' #-}
instance Isomorphic Bool Any where to' = N.pack
                                   {-# INLINE to' #-}
instance Isomorphic (Sum a) a where to' = N.unpack
                                    {-# INLINE to' #-}
instance Isomorphic a (Sum a) where to' = N.pack
                                    {-# INLINE to' #-}
instance Isomorphic (Product a) a where to' = N.unpack
                                        {-# INLINE to' #-}
instance Isomorphic a (Product a) where to' = N.pack
                                        {-# INLINE to' #-}
instance Isomorphic (First a) (Maybe a) where to' = N.unpack
                                              {-# INLINE to' #-}
instance Isomorphic (Maybe a) (First a) where to' = N.pack
                                              {-# INLINE to' #-}
instance Isomorphic (Last a) (Maybe a) where to' = N.unpack
                                             {-# INLINE to' #-}
instance Isomorphic (Maybe a) (Last a) where to' = N.pack
                                             {-# INLINE to' #-}

-- | lift a function to any types to which it is isomorphic
as :: (Isomorphic b d, Isomorphic c a) => (a -> b) -> c -> d
as f = to . f . to
as2 :: (Isomorphic c f, Isomorphic d a, Isomorphic e b) => (a -> b -> c) -> d -> e -> f
as2 f x y = to $ f (to x) (to y)
as3 :: (Isomorphic d h, Isomorphic e a, Isomorphic f b, Isomorphic g c) => (a -> b -> c -> d) -> e -> f -> g -> h
as3 f x y z = to $ f (to x) (to y) (to z)
as4 :: (Isomorphic e j, Isomorphic f a, Isomorphic g b, Isomorphic h c, Isomorphic i d) => (a -> b -> c -> d -> e) -> f -> g -> h -> i -> j
as4 f w x y z = to $ f (to w) (to x) (to y) (to z)
as5 :: (Isomorphic f l, Isomorphic g a, Isomorphic h b, Isomorphic i c, Isomorphic j d, Isomorphic k e) => (a -> b -> c -> d -> e -> f) -> g -> h -> i -> j -> k -> l
as5 f v w x y z = to $ f (to v) (to w) (to x) (to y) (to z)

-- | A free lens @Iso s t a b@ from the `Isomorphic` instances
isoBi :: (Profunctor p, Isomorphic s a, Isomorphic b t, Functor f) => p a (f b) -> p s (f t)
isoBi = dimap to (fmap to)


instance {-# OVERLAPPABLE #-} Coercible a b => Isomorphic a b where to' = coerce
                                                                    {-# INLINE to' #-}

instance (Functor f, Isomorphic a b) => Isomorphic (f a) (f b) where to' = fmap to
-- instance (Functor f, Functor g, Isomorphic a b) => Isomorphic (g (f a)) (g (f b)) where to' = (fmap . fmap) to
-- instance (Functor f, Functor g, Functor h, Isomorphic a b) => Isomorphic (h (g (f a))) (h (g (f b))) where to' = (fmap . fmap . fmap) to
-- instance (Functor f, Functor g, Functor h, Functor k, Isomorphic a b) => Isomorphic (k (h (g (f a)))) (k (h (g (f b)))) where to' = (fmap . fmap . fmap . fmap) to