{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE TemplateHaskell       #-}

module Network.Craze.Internal where

import           Control.Exception (SomeException)
import           Control.Monad     (forM, when)
import           Data.Map.Lazy     (Map)
import qualified Data.Map.Lazy     as M
import           Data.Monoid       ((<>))

import           Control.Concurrent.Async.Lifted (Async, async, asyncThreadId,
                                                  cancel, waitAnyCatch)
import           Control.Concurrent.Lifted       (threadDelay)
import           Control.Lens                    (at, makeLenses, use, (&),
                                                  (.~), (?=))
import           Control.Monad.State             (MonadState)
import           Control.Monad.Trans             (MonadIO, liftIO)
import           Data.Text                       (Text)
import qualified Data.Text                       as T
import qualified Data.Text.IO                    as TIO
import           Network.Curl                    (CurlBuffer, CurlHeader,
                                                  CurlResponse_,
                                                  curlGetResponse_)

import Network.Craze.Types

type ClientMap ht bt a = Map (Async (CurlResponse_ ht bt)) (ClientState a)

data ClientState a = ClientState
  { _csOptions :: ProviderOptions
  , _csStatus  :: ClientStatus a
  }

data RaceState ht bt a = RaceState
  { _rsClientMap  :: ClientMap ht bt a
  , _rsChecker    :: RacerChecker a
  , _rsHandler    :: RacerHandler ht bt a
  , _rsDebug      :: Bool
  , _rsReturnLast :: Bool
  }

makeLenses ''ClientState
makeLenses ''RaceState

extractStatuses :: RaceState ht bt a -> [(Text, ClientStatus a)]
extractStatuses RaceState{..} = M.elems $ makeTuple <$>  _rsClientMap
  where
    makeTuple :: ClientState a -> (Text, ClientStatus a)
    makeTuple ClientState{..} = (poTag _csOptions, _csStatus)

makeRaceState
  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)
  => Text
  -> Racer ht bt a
  -> m (RaceState ht bt a)
makeRaceState url Racer{..} = do
  providerMap <- makeClientMap url racerProviders

  pure $ RaceState
    providerMap
    racerChecker
    racerHandler
    racerDebug
    racerReturnLast

makeClientMap
  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)
  => Text
  -> [RacerProvider]
  -> m (ClientMap ht bt a)
makeClientMap url providers = M.fromList <$> forM providers (makeClient url)

makeClient
  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)
  => Text
  -> RacerProvider
  -> m (Async (CurlResponse_ ht bt), ClientState a)
makeClient url provider = liftIO $ do
  options <- provider
  future <- async $ performGet url options

  pure (future, ClientState options Pending)

performGet
  :: (CurlHeader ht, CurlBuffer bt)
  => Text
  -> ProviderOptions
  -> IO (CurlResponse_ ht bt)
performGet url ProviderOptions{..} = do
  case poDelay of
    Nothing -> pure ()
    Just delay -> threadDelay delay

  curlGetResponse_ (T.unpack url) poOptions

cancelAll :: MonadIO m => [Async a] -> m ()
cancelAll = liftIO . mapM_ (async . cancel)

cancelRemaining
  :: (MonadIO m, MonadState (RaceState ht bt a) m)
  => m ()
cancelRemaining = do
  remaining <- onlyPending <$> use rsClientMap

  cancelAll $ M.keys remaining

identifier :: Async (CurlResponse_ ht bt) -> ProviderOptions -> Text
identifier a o = poTag o <> ":" <> (T.pack . show . asyncThreadId $ a)

onlyPending :: ClientMap ht bt a -> ClientMap ht bt a
onlyPending = M.filter (isPending . _csStatus)

isPending :: ClientStatus a -> Bool
isPending Pending = True
isPending _ = False

markAsSuccessful
  :: (MonadState (RaceState ht bt a) m)
  => Async (CurlResponse_ ht bt)
  -> a
  -> m ()
markAsSuccessful key result = do
  maybePrevious <- use $ rsClientMap . at key

  case maybePrevious of
    Just previous -> (rsClientMap . at key)
      ?= (previous & csStatus .~ Successful result)
    Nothing -> pure ()

markAsFailure
  :: (MonadState (RaceState ht bt a) m)
  => Async (CurlResponse_ ht bt)
  -> a
  -> m ()
markAsFailure key result = do
  maybePrevious <- use $ rsClientMap . at key

  case maybePrevious of
    Just previous -> (rsClientMap . at key)
      ?= (previous & csStatus .~ Failed result)
    Nothing -> pure ()

markAsErrored
  :: (MonadState (RaceState ht bt a) m)
  => Async (CurlResponse_ ht bt)
  -> SomeException
  -> m ()
markAsErrored key result = do
  maybePrevious <- use $ rsClientMap . at key

  case maybePrevious of
    Just previous -> (rsClientMap . at key)
      ?= (previous & csStatus .~ Errored result)
    Nothing -> pure ()

waitForOne
  :: (Eq a, MonadIO m, MonadState (RaceState ht bt a) m)
  => m (Maybe (Async (CurlResponse_ ht bt), a))
waitForOne = do
  debug <- use rsDebug
  providerMap <- use rsClientMap

  let asyncs = _csOptions <$> onlyPending providerMap

  if null asyncs then pure Nothing else do
    winner <- liftIO $ waitAnyCatch (M.keys asyncs)

    case winner of
      (as, Right a) -> do
        handler <- use rsHandler
        check <- use rsChecker
        returnLast <- use rsReturnLast
        result <- liftIO $ handler a

        if check result then do
          markAsSuccessful as result
          cancelRemaining

          when debug . liftIO $ do
            TIO.putStr "[racer] Winner: "
            print (asyncThreadId as)

          pure $ Just (as, result)
          else do
            markAsFailure as result

            remaining <- M.keys . onlyPending <$> use rsClientMap

            if returnLast && null remaining
              then do
                when debug . liftIO $ do
                  TIO.putStr "[racer] Reached last. Returning: "
                  print (asyncThreadId as)

                pure $ Just (as, result)
              else waitForOne
      (as, Left ex) -> markAsErrored as ex >> waitForOne
