{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
module Keter.Prelude
    ( T.Text
    , String
    , P.Monad (..)
    , P.Maybe (..)
    , P.Bool (..)
    , (P.$)
    , (P..)
    , LogMessage (..)
    , log
    , logEx
    , KIO
    , toString
    , P.map
    , (A.***)
    , readFileLBS
    , P.Either (..)
    , P.either
    , E.SomeException
    , runKIO
    , void
    , liftIO
    , forkKIO
    , forkKIO'
    , (++)
    , P.minBound
    , P.succ
    , show
    , Control.Monad.when
    , fromText
    , P.flip
    , P.Show
    , KeterException (..)
    , E.toException
    , newStdGen
    , Default (..)
    , P.Int
    , (P.&&)
    , (P.==)
    , (P./=)
    , (P.*)
    , P.fromIntegral
    , P.reverse
    , P.otherwise
    , timeout
    , threadDelay
    , P.id
    , P.filter
    , P.mapM_
    , P.fmap
    , P.not
    , P.maybe
    , (P.>)
    , (P.<)
    , (P.<=)
    , (P.+)
    , (P.-)
    , getCurrentTime
      -- * Filepath
    , (F.</>)
    , (F.<.>)
    , F.FilePath
    , F.isDirectory
    , F.isFile
    , F.removeTree
    , F.createTree
    , F.directory
    , F.rename
    , F.basename
    , F.toText
    , F.hasExtension
    , F.listDirectory
    , F.decodeString
      -- * MVar
    , M.MVar
    , newMVar
    , newEmptyMVar
    , modifyMVar
    , modifyMVar_
    , swapMVar
    , takeMVar
    , tryTakeMVar
    , putMVar
      -- * IORef
    , I.IORef
    , newIORef
    , atomicModifyIORef
      -- * Chan
    , C.Chan
    , newChan
    , readChan
    , writeChan
    ) where

import qualified Filesystem.Path.CurrentOS as F
import qualified Filesystem as F
import qualified Data.Text as T
import qualified Prelude as P
import qualified Control.Arrow as A
import qualified Data.ByteString.Lazy as L
import Prelude (($), (.))
import qualified Control.Exception as E
import qualified Control.Monad
import qualified Control.Applicative
import qualified Control.Concurrent.MVar as M
import Control.Concurrent (forkIO, ThreadId)
import qualified Control.Concurrent
import qualified Data.IORef as I
import Data.Monoid (Monoid, mappend)
import qualified Data.Text.Lazy.Builder as B
import Data.Typeable (Typeable)
import qualified Control.Concurrent.Chan as C
import qualified System.Random as R
import Data.Default (Default (..))
import System.Exit (ExitCode)
import qualified Blaze.ByteString.Builder as Blaze
import qualified Blaze.ByteString.Builder.Char.Utf8
import qualified System.Timeout
import qualified Language.Haskell.TH.Syntax as TH
import qualified Data.Time

type String = T.Text

newtype KIO a = KIO { unKIO :: (LogMessage -> P.IO ()) -> P.IO a }

instance P.Monad KIO where
    return = KIO . P.const . P.return
    KIO x >>= y = KIO $ \f -> do
        x' <- x f
        let KIO mz = y x'
        mz f

instance P.Functor KIO where
    fmap = Control.Monad.liftM
instance Control.Applicative.Applicative KIO where
    (<*>) = Control.Monad.ap
    pure = P.return

log :: LogMessage -> KIO ()
log msg = do
    f <- getLogger
    void $ liftIO $ f msg
  where
    getLogger = KIO P.return

void :: P.Monad m => m a -> m ()
void f = f P.>> P.return ()

data LogMessage
    = ProcessCreated F.FilePath
    | InvalidBundle F.FilePath E.SomeException
    | ProcessDidNotStart F.FilePath
    | ExceptionThrown T.Text E.SomeException
    | RemovingPort P.Int
    | UnpackingBundle F.FilePath F.FilePath
    | TerminatingApp T.Text
    | FinishedReloading T.Text
    | TerminatingOldProcess T.Text
    | RemovingOldFolder F.FilePath
    | ReceivedInotifyEvent T.Text
    | ProcessWaiting F.FilePath

instance P.Show LogMessage where
    show (ProcessCreated f) = "Created process: " ++ F.encodeString f
    show (InvalidBundle f e) = P.concat
        [ "Unable to parse bundle file '"
        , F.encodeString f
        , "': "
        , P.show e
        ]
    show (ProcessDidNotStart fp) = P.concat
        [ "Could not start process within timeout period: "
        , F.encodeString fp
        ]
    show (ExceptionThrown t e) = P.concat
        [ T.unpack t
        , ": "
        , P.show e
        ]
    show (RemovingPort p) = "Port in use, removing from port pool: " ++ P.show p
    show (UnpackingBundle b dir) = P.concat
        [ "Unpacking bundle '"
        , F.encodeString b
        , "' into folder: "
        , F.encodeString dir
        ]
    show (TerminatingApp t) = "Shutting down app: " ++ T.unpack t
    show (FinishedReloading t) = "App finished reloading: " ++ T.unpack t
    show (TerminatingOldProcess t) = "Sending old process TERM signal: " ++ T.unpack t
    show (RemovingOldFolder fp) = "Removing unneeded folder: " ++ F.encodeString fp
    show (ReceivedInotifyEvent t) = "Received unknown INotify event: " ++ T.unpack t
    show (ProcessWaiting f) = "Process restarting too quickly, waiting before trying again: " ++ F.encodeString f

logEx :: TH.Q TH.Exp
logEx = do
    let showLoc TH.Loc { TH.loc_module = m, TH.loc_start = (l, c) } = P.concat
            [ m
            , ":"
            , P.show l
            , ":"
            , P.show c
            ]
    loc <- P.fmap showLoc TH.qLocation
    [|log P.. ExceptionThrown (T.pack $(TH.lift loc))|]

class ToString a where
    toString :: a -> P.String

instance ToString P.String where
    toString = P.id
instance ToString T.Text where
    toString = T.unpack
instance ToString F.FilePath where
    toString = F.encodeString

readFileLBS :: F.FilePath -> KIO (P.Either E.SomeException L.ByteString)
readFileLBS = liftIO . L.readFile P.. toString

liftIO :: P.IO a -> KIO (P.Either E.SomeException a)
liftIO = KIO . P.const . E.try

liftIO_ :: P.IO a -> KIO a
liftIO_ = KIO . P.const

runKIO :: (LogMessage -> P.IO ()) -> KIO a -> P.IO a
runKIO f (KIO g) = g f

newMVar :: a -> KIO (M.MVar a)
newMVar = liftIO_ . M.newMVar

newEmptyMVar :: KIO (M.MVar a)
newEmptyMVar = liftIO_ M.newEmptyMVar

modifyMVar :: M.MVar a -> (a -> KIO (a, b)) -> KIO b
modifyMVar m f = KIO $ \x -> M.modifyMVar m (\a -> unKIO (f a) x)

modifyMVar_ :: M.MVar a -> (a -> KIO a) -> KIO ()
modifyMVar_ m f = KIO $ \x -> M.modifyMVar_ m (\a -> unKIO (f a) x)

swapMVar :: M.MVar a -> a -> KIO a
swapMVar m = liftIO_ . M.swapMVar m

takeMVar :: M.MVar a -> KIO a
takeMVar = liftIO_ . M.takeMVar

tryTakeMVar :: M.MVar a -> KIO (P.Maybe a)
tryTakeMVar = liftIO_ . M.tryTakeMVar

putMVar :: M.MVar a -> a -> KIO ()
putMVar m = liftIO_ . M.putMVar m

forkKIO :: KIO () -> KIO ()
forkKIO = void . forkKIO'

forkKIO' :: KIO () -> KIO (P.Either E.SomeException ThreadId)
forkKIO' f = do
    x <- KIO P.return
    liftIO $ forkIO $ unKIO f x

newIORef :: a -> KIO (I.IORef a)
newIORef = liftIO_ . I.newIORef

atomicModifyIORef :: I.IORef a -> (a -> (a, b)) -> KIO b
atomicModifyIORef x = liftIO_ . I.atomicModifyIORef x

(++) :: Monoid m => m -> m -> m
(++) = mappend

show :: P.Show a => a -> T.Text
show = T.pack . P.show

class FromText a where
    fromText :: T.Text -> a
instance FromText T.Text where
    fromText = P.id
instance FromText F.FilePath where
    fromText = F.fromText
instance FromText B.Builder where
    fromText = B.fromText
instance FromText Blaze.Builder where
    fromText = Blaze.ByteString.Builder.Char.Utf8.fromText

data KeterException = CannotParsePostgres F.FilePath
                    | ExitCodeFailure F.FilePath ExitCode
                    | NoPortsAvailable
                    | InvalidConfigFile
    deriving (P.Show, Typeable)
instance E.Exception KeterException

newChan :: KIO (C.Chan a)
newChan = liftIO_ C.newChan

newStdGen :: KIO R.StdGen
newStdGen = liftIO_ R.newStdGen

readChan :: C.Chan a -> KIO a
readChan = liftIO_ . C.readChan

writeChan :: C.Chan a -> a -> KIO ()
writeChan c = liftIO_ . C.writeChan c

timeout :: P.Int -> KIO a -> KIO (P.Maybe a)
timeout seconds (KIO f) = KIO $ \x -> System.Timeout.timeout seconds $ f x

threadDelay :: P.Int -> KIO ()
threadDelay = liftIO_ . Control.Concurrent.threadDelay

getCurrentTime :: KIO Data.Time.UTCTime
getCurrentTime = liftIO_ Data.Time.getCurrentTime