{-# LANGUAGE CPP #-}
module Hakyll.Preview.Poll
    ( watchUpdates
    ) where
import           Control.Concurrent             (forkIO)
import           Control.Concurrent.MVar        (newEmptyMVar, takeMVar,
                                                 tryPutMVar)
import           Control.Exception              (AsyncException, fromException,
                                                 handle, throw)
import           Control.Monad                  (forever, void, when)
import           System.Directory               (canonicalizePath)
import           System.FilePath                (pathSeparators)
import qualified System.FSNotify                as FSNotify
#ifdef mingw32_HOST_OS
import           Control.Concurrent             (threadDelay)
import           Control.Exception              (IOException, throw, try)
import           System.Directory               (doesFileExist)
import           System.Exit                    (exitFailure)
import           System.FilePath                ((</>))
import           System.IO                      (Handle, IOMode (ReadMode),
                                                 hClose, openFile)
import           System.IO.Error                (isPermissionError)
#endif
import           Hakyll.Core.Configuration
import           Hakyll.Core.Identifier
import           Hakyll.Core.Identifier.Pattern
watchUpdates :: Configuration -> IO Pattern -> IO ()
watchUpdates conf update = do
    let providerDir = providerDirectory conf
    shouldBuild     <- newEmptyMVar
    pattern         <- update
    fullProviderDir <- canonicalizePath $ providerDirectory conf
    manager         <- FSNotify.startManager
    let allowed event = do
            
            
            let path       = FSNotify.eventPath event
                relative   = dropWhile (`elem` pathSeparators) $
                    drop (length fullProviderDir) path
                identifier = fromFilePath relative
            shouldIgnore <- shouldIgnoreFile conf path
            return $ not shouldIgnore && matches pattern identifier
    
    
    _ <- forkIO $ forever $ do
        event <- takeMVar shouldBuild
        handle
            (\e -> case fromException e of
                Nothing    -> putStrLn (show e)
                Just async -> throw (async :: AsyncException))
            (update' event providerDir)
    
    
    void $ FSNotify.watchTree manager providerDir (not . isRemove) $ \event -> do
        allowed' <- allowed event
        when allowed' $ void $ tryPutMVar shouldBuild event
  where
#ifndef mingw32_HOST_OS
    update' _     _        = void update
#else
    update' event provider = do
        let path = provider </> FSNotify.eventPath event
        
        fileExists <- doesFileExist path
        when fileExists . void $ waitOpen path ReadMode (\_ -> update) 10
    
    
    waitOpen :: FilePath -> IOMode -> (Handle -> IO r) -> Integer -> IO r
    waitOpen _    _    _       0 = do
        putStrLn "[ERROR] Failed to retrieve modified file for regeneration"
        exitFailure
    waitOpen path mode handler retries = do
        res <- try $ openFile path mode :: IO (Either IOException Handle)
        case res of
            Left ex -> if isPermissionError ex
                       then do
                           threadDelay 100000
                           waitOpen path mode handler (retries - 1)
                       else throw ex
            Right h -> do
                handled <- handler h
                hClose h
                return handled
#endif
isRemove :: FSNotify.Event -> Bool
isRemove (FSNotify.Removed {}) = True
isRemove _                     = False