{-# Language ScopedTypeVariables #-}
module Main where

import Prelude
import System.Directory
import System.FilePath
import System.IO
import Control.Monad (forM_, forever, when)
import Control.Exception
import Control.Concurrent
--import Data.Time

-- How many `openHandle` calls in the test
-- On a laptop:
-- * when set to 1k, it occasionally reproduces the failure
-- * when set to 10k, it occasionally fails to reproduce
n :: Int
n = 10000

main :: IO ()
main = test "."

test :: FilePath -> IO ()
test dir' = do
  let dir = dir' </> "repro"
  createDirectoryIfMissing True dir
  availableNames <- newChan :: IO (Chan FilePath)
  writeList2Chan availableNames [ dir </> "repro" ++ show (i :: Int) | i <- [1..30]]
  toClose <- newChan :: IO (Chan (Handle, FilePath))
  maybeDelete <- newChan :: IO (Chan FilePath)
  deleter <- forkIO (getChanContents maybeDelete >>= mapM_ (recycle availableNames))
  closer <- forkIO (getChanContents toClose >>= mapM_ (keepClosing availableNames))
  resultMVar <- newEmptyMVar
  openingThread <- keepOpening availableNames toClose maybeDelete `forkFinally`
                      putMVar resultMVar
  interrupter <- forkIO $ forever $ do
    threadDelay (10^3)
    throwTo openingThread Interrupt

  result <- readMVar resultMVar

  -- cleanup
  mapM_ killThread [interrupter, deleter, closer]
  removeDirectoryRecursive dir

  either throwIO (const $ putStrLn "No failures observed - success") result


keepOpening :: Chan FilePath -> Chan (Handle, FilePath) -> Chan FilePath -> IO ()
keepOpening availableNames toClose maybeDelete =
  uninterruptibleMask $ \ restore -> do
    filepaths <- take n <$> getChanContents availableNames
    forM_ filepaths $ \filepath -> do
      --now <- getCurrentTime
      h <- (Just <$> restore (openFile filepath WriteMode)) `catch` \(_ :: Interrupt) -> do
            writeChan maybeDelete filepath
            pure Nothing
      --elapsed <- (`diffUTCTime` now) <$> getCurrentTime
      --print elapsed
      case h of
        Nothing -> pure ()
        Just h -> writeChan toClose (h, filepath)

data Interrupt = Interrupt deriving (Show)
instance Exception Interrupt

recycle :: Chan FilePath -> FilePath -> IO ()
recycle availableNames name = do
  exist <- doesFileExist name
  when exist $ removeFile name
  writeChan availableNames name

keepClosing :: Chan FilePath -> (Handle, FilePath) -> IO ()
keepClosing availableNames (handle, name) = do
  hClose handle
  removeFile name
  writeChan availableNames name
