chase :: [Identifier] -> Identifier -> Runtime ()
chase trail id'
    | id' `elem` trail = throwError $ "Hakyll.Core.Runtime.chase: " ++
        "Dependency cycle detected: " ++ intercalate " depends on "
            (map show $ dropWhile (/= id') (reverse trail) ++ [id'])
    | otherwise        = do
        logger   <- runtimeLogger        <$> ask
        todo     <- runtimeTodo          <$> get
        provider <- runtimeProvider      <$> ask
        universe <- runtimeUniverse      <$> ask
        routes   <- runtimeRoutes        <$> ask
        store    <- runtimeStore         <$> ask
        config   <- runtimeConfiguration <$> ask
        Logger.debug logger $ "Processing " ++ show id'

        let compiler = todo M.! id'
            read' = CompilerRead
                { compilerConfig     = config
                , compilerUnderlying = id'
                , compilerProvider   = provider
                , compilerUniverse   = M.keysSet universe
                , compilerRoutes     = routes
                , compilerStore      = store
                , compilerLogger     = logger
                }

        result <- liftIO $ runCompiler compiler read'
        case result of
            -- Rethrow error
            CompilerError [] -> throwError
                "Compiler failed but no info given, try running with -v?"
            CompilerError es -> throwError $ intercalate "; " es

            -- Signal that a snapshot was saved ->
            CompilerSnapshot snapshot c -> do
                -- Update info. The next 'chase' will pick us again at some
                -- point so we can continue then.
                modify $ \s -> s
                    { runtimeSnapshots =
                        S.insert (id', snapshot) (runtimeSnapshots s)
                    , runtimeTodo      = M.insert id' c (runtimeTodo s)
                    }

            -- Huge success
            CompilerDone (SomeItem item) cwrite -> do
                -- Print some info
                let facts = compilerDependencies cwrite
                    cacheHits
                        | compilerCacheHits cwrite <= 0 = "updated"
                        | otherwise                     = "cached "
                Logger.message logger $ cacheHits ++ " " ++ show id'

                -- Sanity check
                unless (itemIdentifier item == id') $ throwError $
                    "The compiler yielded an Item with Identifier " ++
                    show (itemIdentifier item) ++ ", but we were expecting " ++
                    "an Item with Identifier " ++ show id' ++ " " ++
                    "(you probably want to call makeItem to solve this problem)"

                -- Write if necessary
                (mroute, _) <- liftIO $ runRoutes routes provider id'
                case mroute of
                    Nothing    -> return ()
                    Just route -> do
                        let path = destinationDirectory config </> route
                        liftIO $ makeDirectories path
                        liftIO $ write path item
                        Logger.debug logger $ "Routed to " ++ path

                -- Save! (For load)
                liftIO $ save store item

                -- Update state
                modify $ \s -> s
                    { runtimeDone  = S.insert id' (runtimeDone s)
                    , runtimeTodo  = M.delete id' (runtimeTodo s)
                    , runtimeFacts = M.insert id' facts (runtimeFacts s)
                    }

            -- Try something else first
            CompilerRequire dep c -> do
                -- Update the compiler so we don't execute it twice
                let (depId, depSnapshot) = dep
                done      <- runtimeDone <$> get
                snapshots <- runtimeSnapshots <$> get

                -- Done if we either completed the entire item (runtimeDone) or
                -- if we previously saved the snapshot (runtimeSnapshots).
                let depDone =
                        depId `S.member` done ||
                        (depId, depSnapshot) `S.member` snapshots

                modify $ \s -> s
                    { runtimeTodo = M.insert id'
                        (if depDone then c else compilerResult result)
                        (runtimeTodo s)
                    }

                -- If the required item is already compiled, continue, or, start
                -- chasing that
                Logger.debug logger $ "Require " ++ show depId ++
                    " (snapshot " ++ depSnapshot ++ "): " ++
                    (if depDone then "OK" else "chasing")
                if depDone then chase trail id' else chase (id' : trail) depId
chase :: [Identifier] -> Identifier -> Runtime ()
chase trail id'
    | id' `elem` trail = throwError $ "Hakyll.Core.Runtime.chase: " ++
        "Dependency cycle detected: " ++ intercalate " depends on "
            (map show $ dropWhile (/= id') (reverse trail) ++ [id'])
    | otherwise        = do
        logger   <- runtimeLogger        <$> ask
        todo     <- runtimeTodo          <$> get
        provider <- runtimeProvider      <$> ask
        universe <- runtimeUniverse      <$> ask
        routes   <- runtimeRoutes        <$> ask
        store    <- runtimeStore         <$> ask
        config   <- runtimeConfiguration <$> ask
        Logger.debug logger $ "Processing " ++ show id'

        let compiler = todo M.! id'
            read' = CompilerRead
                { compilerConfig     = config
                , compilerUnderlying = id'
                , compilerProvider   = provider
                , compilerUniverse   = M.keysSet universe
                , compilerRoutes     = routes
                , compilerStore      = store
                , compilerLogger     = logger
                }

        result <- liftIO $ runCompiler compiler read'
        case result of
            -- Rethrow error
            CompilerError [] -> throwError
                "Compiler failed but no info given, try running with -v?"
            CompilerError es -> throwError $ intercalate "; " es

            -- Signal that a snapshot was saved ->
            CompilerSnapshot snapshot c -> do
                -- Update info. The next 'chase' will pick us again at some
                -- point so we can continue then.
                modify $ \s -> s
                    { runtimeSnapshots =
                        S.insert (id', snapshot) (runtimeSnapshots s)
                    , runtimeTodo      = M.insert id' c (runtimeTodo s)
                    }

            -- Huge success
            CompilerDone (SomeItem item) cwrite -> do
                -- Print some info
                let facts = compilerDependencies cwrite
                    cacheHits
                        | compilerCacheHits cwrite <= 0 = "updated"
                        | otherwise                     = "cached "
                Logger.message logger $ cacheHits ++ " " ++ show id'

                -- Sanity check
                unless (itemIdentifier item == id') $ throwError $
                    "The compiler yielded an Item with Identifier " ++
                    show (itemIdentifier item) ++ ", but we were expecting " ++
                    "an Item with Identifier " ++ show id' ++ " " ++
                    "(you probably want to call makeItem to solve this problem)"

                -- Write if necessary
                (mroute, _) <- liftIO $ runRoutes routes provider id'
                case mroute of
                    Nothing    -> return ()
                    Just route -> do
                        let path = destinationDirectory config </> route
                        liftIO $ makeDirectories path
                        liftIO $ write path item
                        Logger.debug logger $ "Routed to " ++ path

                -- Save! (For load)
                liftIO $ save store item

                -- Update state
                modify $ \s -> s
                    { runtimeDone  = S.insert id' (runtimeDone s)
                    , runtimeTodo  = M.delete id' (runtimeTodo s)
                    , runtimeFacts = M.insert id' facts (runtimeFacts s)
                    }

            -- Try something else first
            CompilerRequire dep c -> do
                -- Update the compiler so we don't execute it twice
                let (depId, depSnapshot) = dep
                done      <- runtimeDone <$> get
                snapshots <- runtimeSnapshots <$> get

                -- Done if we either completed the entire item (runtimeDone) or
                -- if we previously saved the snapshot (runtimeSnapshots).
                let depDone =
                        depId `S.member` done ||
                        (depId, depSnapshot) `S.member` snapshots

                modify $ \s -> s
                    { runtimeTodo = M.insert id'
                        (if depDone then c else compilerResult result)
                        (runtimeTodo s)
                    }

                -- If the required item is already compiled, continue, or, start
                -- chasing that
                Logger.debug logger $ "Require " ++ show depId ++
                    " (snapshot " ++ depSnapshot ++ "): " ++
                    (if depDone then "OK" else "chasing")
                if depDone then chase trail id' else chase (id' : trail) depId
chase trail id'
    | id' `elem` trail = throwError $ "Hakyll.Core.Runtime.chase: " ++
        "Dependency cycle detected: " ++ intercalate " depends on "
            (map show $ dropWhile (/= id') (reverse trail) ++ [id'])
    | otherwise        = do
        logger   <- runtimeLogger        <$> ask
        todo     <- runtimeTodo          <$> get
        provider <- runtimeProvider      <$> ask
        universe <- runtimeUniverse      <$> ask
        routes   <- runtimeRoutes        <$> ask
        store    <- runtimeStore         <$> ask
        config   <- runtimeConfiguration <$> ask
        Logger.debug logger $ "Processing " ++ show id'

        let compiler = todo M.! id'
            read' = CompilerRead
                { compilerConfig     = config
                , compilerUnderlying = id'
                , compilerProvider   = provider
                , compilerUniverse   = M.keysSet universe
                , compilerRoutes     = routes
                , compilerStore      = store
                , compilerLogger     = logger
                }

        result <- liftIO $ runCompiler compiler read'
        case result of
            -- Rethrow error
            CompilerError [] -> throwError
                "Compiler failed but no info given, try running with -v?"
            CompilerError es -> throwError $ intercalate "; " es

            -- Signal that a snapshot was saved ->
            CompilerSnapshot snapshot c -> do
                -- Update info. The next 'chase' will pick us again at some
                -- point so we can continue then.
                modify $ \s -> s
                    { runtimeSnapshots =
                        S.insert (id', snapshot) (runtimeSnapshots s)
                    , runtimeTodo      = M.insert id' c (runtimeTodo s)
                    }

            -- Huge success
            CompilerDone (SomeItem item) cwrite -> do
                -- Print some info
                let facts = compilerDependencies cwrite
                    cacheHits
                        | compilerCacheHits cwrite <= 0 = "updated"
                        | otherwise                     = "cached "
                Logger.message logger $ cacheHits ++ " " ++ show id'

                -- Sanity check
                unless (itemIdentifier item == id') $ throwError $
                    "The compiler yielded an Item with Identifier " ++
                    show (itemIdentifier item) ++ ", but we were expecting " ++
                    "an Item with Identifier " ++ show id' ++ " " ++
                    "(you probably want to call makeItem to solve this problem)"

                -- Write if necessary
                (mroute, _) <- liftIO $ runRoutes routes provider id'
                case mroute of
                    Nothing    -> return ()
                    Just route -> do
                        let path = destinationDirectory config </> route
                        liftIO $ makeDirectories path
                        liftIO $ write path item
                        Logger.debug logger $ "Routed to " ++ path

                -- Save! (For load)
                liftIO $ save store item

                -- Update state
                modify $ \s -> s
                    { runtimeDone  = S.insert id' (runtimeDone s)
                    , runtimeTodo  = M.delete id' (runtimeTodo s)
                    , runtimeFacts = M.insert id' facts (runtimeFacts s)
                    }

            -- Try something else first
            CompilerRequire dep c -> do
                -- Update the compiler so we don't execute it twice
                let (depId, depSnapshot) = dep
                done      <- runtimeDone <$> get
                snapshots <- runtimeSnapshots <$> get

                -- Done if we either completed the entire item (runtimeDone) or
                -- if we previously saved the snapshot (runtimeSnapshots).
                let depDone =
                        depId `S.member` done ||
                        (depId, depSnapshot) `S.member` snapshots

                modify $ \s -> s
                    { runtimeTodo = M.insert id'
                        (if depDone then c else compilerResult result)
                        (runtimeTodo s)
                    }

                -- If the required item is already compiled, continue, or, start
                -- chasing that
                Logger.debug logger $ "Require " ++ show depId ++
                    " (snapshot " ++ depSnapshot ++ "): " ++
                    (if depDone then "OK" else "chasing")
                if depDone then chase trail id' else chase (id' : trail) depId