{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Glazier.Gadget where

import Control.Applicative
import Control.Lens
import qualified Control.Monad.Fail as Fail
import Control.Monad.Fix (MonadFix)
import Control.Monad.Morph
import Control.Monad.Reader
import Control.Monad.State.Strict
import Data.Semigroup

-- | The Elm update function is @a -> s -> (s, c)@
-- This is isomorphic to @ReaderT a (State s) c@
-- ie, given an action "a", and a current state "s", return the new state "s"
-- and any commands "c" that need to be interpreted externally (eg. download file).
-- This is named Gadget instead of Update to avoid confusion with update from Data.Map
newtype GadgetT a s m c = GadgetT
    { runGadgetT :: ReaderT a (StateT s m) c
    } deriving ( MonadState s
               , MonadReader a
               , Monad
               , Applicative
               , Functor
               , Fail.MonadFail
               , Alternative
               , MonadPlus
               , MonadFix
               , MonadIO
               )

makeWrapped ''GadgetT

type Gadget a s = GadgetT a s Identity

_GadgetT :: Iso (GadgetT a s m c) (GadgetT a' s' m' c') (a -> s -> m (c, s)) (a' -> s' -> m' (c', s'))
_GadgetT = _Wrapping GadgetT . iso runReaderT ReaderT . iso (runStateT .) (StateT .)
{-# INLINABLE _GadgetT #-}

-- | Non polymorphic version of _Gadget
_GadgetT' :: Iso' (GadgetT a s m c) (a -> s -> m (c, s))
_GadgetT' = _GadgetT
{-# INLINABLE _GadgetT' #-}

mkGadgetT' :: (a -> s -> m (c, s)) -> GadgetT a s m c
mkGadgetT' = review _GadgetT
{-# INLINABLE mkGadgetT' #-}

runGadgetT' :: GadgetT a s m c -> (a -> s -> m (c, s))
runGadgetT' = view _GadgetT
{-# INLINABLE runGadgetT' #-}

belowGadgetT ::
  ((a -> s -> m (c, s)) -> a' -> s' -> m' (c', s'))
  -> GadgetT a s m c -> GadgetT a' s' m' c'
belowGadgetT f = _GadgetT %~ f
{-# INLINABLE belowGadgetT #-}

underGadgetT
    :: (ReaderT a (StateT s m) c -> ReaderT a' (StateT s' m') c')
    -> GadgetT a s m c
    -> GadgetT a' s' m' c'
underGadgetT f = _Wrapping GadgetT %~ f
{-# INLINABLE underGadgetT #-}

overGadgetT
    :: (GadgetT a s m c -> GadgetT a' s' m' c')
    -> ReaderT a (StateT s m) c
    -> ReaderT a' (StateT s' m') c'
overGadgetT f = _Unwrapping GadgetT %~ f
{-# INLINABLE overGadgetT #-}

aboveGadgetT ::
  (GadgetT a s m c -> GadgetT a' s' m' c')
  -> (a -> s -> m (c, s)) -> a' -> s' -> m' (c', s')
aboveGadgetT f = from _GadgetT %~ f
{-# INLINABLE aboveGadgetT #-}

instance MonadTrans (GadgetT a s) where
    lift = GadgetT . lift . lift

instance MFunctor (GadgetT a s) where
    hoist f (GadgetT m) = GadgetT (hoist (hoist f) m)

instance (Monad m, Semigroup c) => Semigroup (GadgetT a s m c) where
    (GadgetT f) <> (GadgetT g) = GadgetT $ (<>) <$> f <*> g
    {-# INLINABLE (<>) #-}

instance (Monad m, Monoid c) => Monoid (GadgetT a s m c) where
    mempty = GadgetT $ pure mempty
    {-# INLINABLE mempty #-}

    (GadgetT f) `mappend` (GadgetT g) = GadgetT $ mappend <$> f <*> g
    {-# INLINABLE mappend #-}

-- | zoom can be used to modify the state inside an Gadget
type instance Zoomed (GadgetT a s m) = Zoomed (ReaderT a (StateT s m))
instance Monad m => Zoom (GadgetT a s m) (GadgetT a t m) s t where
    zoom l = GadgetT . zoom l . runGadgetT
    {-# INLINABLE zoom #-}

-- | magnify can be used to modify the action inside an Gadget
type instance Magnified (GadgetT a s m) = Magnified (ReaderT a (StateT s m))
instance Monad m => Magnify (GadgetT a s m) (GadgetT b s m) a b where
    magnify l = GadgetT . magnify l . runGadgetT
    {-# INLINABLE magnify #-}