{-# LANGUAGE Arrows #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTSyntax #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
-- {-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
-- {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoMonoLocalBinds #-}

module Midair.Core (

   -- * Fundamental units
     SFlow
   -- , (-->)

   , sMap
   , sFold
   , sFoldNoDefault
   , sFoldAccum
   , sCompose
   , sZip
   , sFilter

   -- * Hot-swapping
   , mkNodeRef
   , SFNodeRef
   , nRef
   , hotSwap
   , hotSwapSTM

   -- * Graph firing
   , fireGraph
   , fireGraphIO
   ) where

import Control.Arrow
import qualified Control.Category as Cat
-- import Control.Concurrent.Async (race)
import Control.Concurrent.STM
-- import Data.Proxy
-- import Data.Type.Equality
-- import Data.Unique

-- type (-->) a b = SFlow a b

-- | Signal flow
-- 
--   A little like a function:
--   takes a value of type 'a' and returns one of type 'c'
data SFlow a c where
   -- Transformation:
   SF_Map :: (a -> c) -> SFlow a c
   -- Composition:
   SF_Compose :: forall a b c. SFlow b c -> SFlow a b -> SFlow a c
   -- Accumulation:
   SF_FoldP :: c -> (a -> c -> c) -> SFlow a c
      -- Could we written in terms of other combinators:
   SF_FoldAccum :: Maybe c -> (a -> Maybe c -> c) -> SFlow a c
   -- SF_FoldAccum :: forall a b c d
   -- Recombination:
   SF_Zip :: forall a b c. SFlow a b -> SFlow a c -> SFlow a (b, c)
   -- Filtration/selection:
   SF_Filter :: Maybe a -> (a -> Bool) -> SFlow a (Maybe a)
   -- Reference to a part of the graph:
   SF_NodeRef :: TVar (Maybe c) -> TVar (SFlow a c) -> SFlow a c

data SFNodeRef a c
   = SFNodeRef_Internal (TVar (Maybe c)) (TVar (SFlow a c))

-- | Turn the result of 'mkNodeRef' into something you can use in
--   an 'SFlow' graph
nRef :: SFNodeRef a c -> SFlow a c
nRef (SFNodeRef_Internal x y) = SF_NodeRef x y

mkNodeRefSTM :: SFlow i o -> STM (SFNodeRef i o)
mkNodeRefSTM sigNode = do
   sigNodeTVar <- newTVar sigNode
   foo <- newTVar Nothing
   return $ SFNodeRef_Internal foo sigNodeTVar

-- | Pass in a signal flow graph and get back a reference you can
--   use to hot-swap with
mkNodeRef :: SFlow i o -> IO (SFNodeRef i o)
mkNodeRef g = atomically $ mkNodeRefSTM g

-- | Given a node in the graph, and an input to that node, return the output of
--   that node and the \"new node\" with updated state
fireGraph :: TVar (SFlow a c) -> a -> STM c
fireGraph graphTVar inVal = do
   (newGraph, retVal) <- (flip fireGraph') inVal =<< readTVar graphTVar
   writeTVar graphTVar newGraph
   return retVal


-- The only action we do in STM here is to call fireGraph (no prime)
-- whenever we encounter a 'SF_NodeRef':
fireGraph' :: SFlow a c -> a -> STM (SFlow a c, c)
fireGraph' m@(SF_Map f) a =
   return (m, f a)
fireGraph' (SF_Compose bToCOld aToBOld) a = do
   (aToBNew, rightVal) <- fireGraph' aToBOld a
   (bToCNew, returnVal) <- fireGraph' bToCOld rightVal
   return (SF_Compose bToCNew aToBNew, returnVal)
fireGraph' (SF_FoldP theLastVal f) a =
   let newVal = f a theLastVal
   in return (SF_FoldP newVal f, newVal)
fireGraph' (SF_FoldAccum theLastVal f) a =
   let newVal = f a theLastVal
   in return (SF_FoldAccum (Just newVal) f, newVal)
fireGraph' (SF_Zip aToBOld aToCOld) a = do
   (aToBNew, b) <- fireGraph' aToBOld a
   (aToCNew, c) <- fireGraph' aToCOld a
   return (SF_Zip aToBNew aToCNew, (b, c))
fireGraph' (SF_Filter previousVal filterF) newVal = return $
   if filterF newVal
      then (SF_Filter (Just newVal) filterF, Just newVal)
      else (SF_Filter previousVal filterF, previousVal)
fireGraph' (SF_NodeRef prevOutVar graphRef) newIn = do
   newOut <- fireGraph graphRef newIn
   writeTVar prevOutVar $ Just newOut
   return (SF_NodeRef prevOutVar graphRef, newOut)

fireGraphIO :: TVar (SFlow a c) -> a -> IO c
fireGraphIO graphTVar inVal =
   atomically $ fireGraph graphTVar inVal

-- | Apply a function to the input signal
sMap :: (a -> c) -> SFlow a c
sMap = SF_Map

-- | Compose two signal flows into one. The equivalent of '(.)'
sCompose :: SFlow b c -> SFlow a b -> SFlow a c
sCompose = SF_Compose

-- | Accumulate a value. \"Folding over the past\".
sFold :: c -> (a -> c -> c) -> SFlow a c
sFold = SF_FoldP

-- | Like 'sFold' but with no default. Useful for functions like
--   @min@ which don't have a semantics of a result from one
--   argument
sFoldNoDefault :: (a -> Maybe c -> c) -> SFlow a c
sFoldNoDefault f = SF_FoldAccum Nothing f

-- | This name may change
sFoldAccum :: Maybe c -> (a -> Maybe c -> c) -> SFlow a c
sFoldAccum maybeV f = SF_FoldAccum maybeV f

-- | Zip two signal flows together into one which returns a
--   signal of two-tuples
sZip :: SFlow a b -> SFlow a c -> SFlow a (b, c)
sZip = SF_Zip

-- | Filter out the incoming signal by a predicate. Also check out
--   'Midair.Handy.sFilterWDefault'
sFilter :: (b -> Bool) -> SFlow b (Maybe b)
sFilter f = SF_Filter Nothing f

instance Functor (SFlow a) where
   fmap f0 (SF_Map f1) = SF_Map (f0 . f1)
   fmap f0 sf@(SF_FoldP _ _) = SF_Compose (SF_Map f0) sf
   fmap f0 sf@(SF_FoldAccum _ _) = SF_Compose (SF_Map f0) sf
   fmap f0 sf@(SF_Compose _ _) = SF_Compose (SF_Map f0) sf
   fmap f0 sf@(SF_Zip _ _) = SF_Compose (SF_Map f0) sf
   fmap f0 sf@(SF_Filter _ _) = SF_Compose (SF_Map f0) sf
   fmap f0 sf@(SF_NodeRef _ _) = SF_Compose (SF_Map f0) sf

instance Applicative (SFlow a) where
   pure x = SF_Map $ \_ -> x
   (<*>) a b = SF_Compose (SF_Map (\(f, x) -> f x)) (SF_Zip a b)

instance Cat.Category SFlow where
   id = SF_Map $ \x -> x
   (.) a b = SF_Compose a b

instance Arrow SFlow where
   arr = SF_Map
   first :: SFlow b c -> SFlow (b, d) (c, d)
   first bToC =
      sZip (bToC `sCompose` arr fst) (arr snd)

{-
-- | Rationale: why do we do this instead of recomputing from the beginning?
-- 
--   1) Practicality: it would be expensive to save all input values
--   2) Meaning: if you e.g. have a video game that's depermining a player's
--      position based on their keypresses, if you were to e.g. recompute all steps
--      (let's say the keypresses move 3 pixels now instead of 2), your player
--      would end up in a totally new position
-}


hotSwapSTM :: SFNodeRef a c -> (Maybe c -> SFlow a c) -> STM ()
hotSwapSTM (SFNodeRef_Internal oldValMaybeTVar graphTVar) newGraphFromVal = do
   oldValMaybe <- readTVar oldValMaybeTVar
   writeTVar graphTVar $ newGraphFromVal oldValMaybe

-- | Swap out part or whole of a signal flow graph with a new one
--   of the same type.
hotSwap :: SFNodeRef a c -> (Maybe c -> SFlow a c) -> IO ()
hotSwap a b = atomically $ hotSwapSTM a b