doYxStuff
    :: IORef (Maybe ProjectConfig)
    -> FilePath
    -- ^ Absolute path to YX executable.
    -> ProjectRoot
    -- ^ Absolute path to project root directory. Everthing initiation does is
    -- relative to this directory.
    -> FilePath
    -- ^ Absolute path to YX project configuration, which is used when new one
    -- has to be created.
    -> Maybe FilePath
    -- ^ Possibly absolute path to existing YX project configuration. 'Nothing'
    -- if there is no such file.
    -> IO ()
doYxStuff cfgRef yxExe root defaultYxConfig possibleYxConfig = shake opts $ do
    getProjectCfg' <- Shake.newCache $ \(yxConfigChanged, cacheFile) -> liftIO
        $ let memo r = r <$ atomicWriteIORef cfgRef (Just r)
              getMemo = readIORef cfgRef
        in
            -- When configuration file has been changed then the cache must be
            -- invalidated. To avoid reading cache file (efficiently stored
            -- version of parsed configuration file) multiple times, we memoize
            -- the parsing result.
            if yxConfigChanged
                then parseAndCacheYxConfig yxConfig cacheFile >>= memo
                else getMemo >>= \case
                    Just r -> pure r
                    Nothing ->
                        -- We don't need to memoize cache file reads, because,
                        -- it is done for us by Shake. See 'Shake.newCache' for
                        -- more details.
                        readCachedYxConfig cacheFile
                            `catch` \(CacheDecodingException _ _) ->
                                -- This should happen only when cache file uses
                                -- old data format and we need to reparse
                                -- project config. As a result it may force
                                -- some additional rules to be executed, but
                                -- the result should always be consistent.
                                parseAndCacheYxConfig yxConfig cacheFile

    let compileProjectCfg = getProjectCfg' . (True, )
        getProjectCfg = getProjectCfg' . (False, )

    Shake.want ["yx-initialization"]

    -- We need to create YX project file only if there isn't one. To correctly
    -- track the dependencies we need this rule to be a pattern rule instead of
    -- phony rule.
    yxConfig %> \out -> liftIO $ do
        -- Even though this is not a phony rule, we need to check for existence
        -- of the file, otherwise we would overwrite any user defined
        -- configuration.
        haveConfig <- doesFileExist out
        unless haveConfig $ liftIO $ do
            IO.putStrLn $ "Generating YX configuration file for this project: "
                <> FilePath.makeRelative root out
            createProjectConfig root >>= Text.writeFile out
            IO.putStrLn ""

    cfgCacheFile %> \out -> do
        -- We expect yxConfig to be already present. See "yx-initialization" rule
        -- for details.
        Shake.need [yxConfig]

        -- (BIG) TODO: Delete old artifacts in .yx-stuff dir. Currently we
        -- would keep e.g. old links in "bin" dir.
        () <$ compileProjectCfg out

    yxShellStuff root "*" Bash </> "bashrc" %> \out -> do
        Shake.need [cfgCacheFile]
        cfg <- getProjectCfg cfgCacheFile
        printGeneratingBashrc out
        compileBashrc cfg (FilePath.makeRelative yxEnvStuff out) out
        liftIO $ IO.putStrLn ""

    yxShellStuff root "*" Bash </> "completion" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    yxShellStuff root "*" Bash </> "environment" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    yxShellStuff root "*" Bash </> "aliases" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    -- Combinator 'Shake.alternatives' allows us to use overlapping patterns.
    Shake.alternatives $ do
        -- Symbolic link to "yx" is a special case. We want to handle it
        -- separately to make that fact explicit.
        yxExeStuff root "*" </> "yx" %> \out ->
            createExecutableLink root yxExe out

        yxExeStuff root "*" </> "*" %> \out -> do
            Shake.need [cfgCacheFile]
            (envName, exeName) <- parseBinFileName yxEnvStuff out
            cfg <- getProjectCfg cfgCacheFile
            lookupExe cfg envName exeName >>= \exe -> case _type exe of
                Alias -> error $ out
                    <> ": Trying to create binary, but shell alias was found."
                Command -> createExecutableLink root yxExe out
                Symlink ->
                    createExecutableLink root (Text.unpack $ _command exe) out

    -- Top level rule:
    "yx-initialization" ~> do
        -- No matter what, we need to parse YX project configuration file, or
        -- read the cached version if its available.
        Shake.need [cfgCacheFile]

        -- We need to traverse the configuration file to know what tasks need
        -- to be done executed.
        getProjectCfg cfgCacheFile >>= mapHM_ envDependencies . _environments
  where
    yxConfig = fromMaybe defaultYxConfig possibleYxConfig
    cfgCacheFile = yxStuffFile root CachedStuff "config.bin"
    yxEnvStuff = yxStuffPath (Just root) (EnvironmentStuff Nothing)

    opts = shakeOptions
        { shakeFiles = yxStuffPath (Just root) CachedStuff
--      , shakeLint = Just Shake.LintFSATrace
        }

    mapHM_ :: Applicative f => (k -> v -> f ()) -> HashMap k v -> f ()
    mapHM_ f = HashMap.foldrWithKey (\k -> (*>) . f k) $ pure ()

    forHM_ = flip mapHM_

    envDependencies name Environment{_bin = bins} = do
        needBin name "yx"
        Shake.need [yxShellStuff root (Text.unpack name) Bash </> "bashrc"]
        forHM_ bins $ \exeName Executable{_type = t} -> case t of
            Alias   -> pure ()  -- Aliases are handled by shell.
            Command -> needBin name exeName
            Symlink -> needBin name exeName

    needBin name bin = do
        Shake.need [yxExeStuff root (Text.unpack name) </> Text.unpack bin]

    parseBinFileName dir file
      | haveParseError = error $ file <> ": Unexpected path when parsing\
        \'yxStuff </> envName </> \"bin\" </> exeName'"
      | otherwise      = pure (envName, exeName)
      where
        haveParseError = List.null envName || List.null exeName

        envName = FilePath.dropTrailingPathSeparator envDir

        (envDir, _) = FilePath.splitFileName
            $ FilePath.dropTrailingPathSeparator binDir

        (binDir, exeName) = FilePath.splitFileName
            $ FilePath.makeRelative dir file

    lookupExe :: ProjectConfig -> String -> String -> Shake.Action Executable
    lookupExe ProjectConfig{_environments = envs} envName exeName =
        case HashMap.lookup envName' envs >>= HashMap.lookup exeName' . _bin of
            Nothing -> error $ exeName <> ": Unable to find configuration for\
                \ executable in environment '" <> envName <> "'"
            Just r -> pure r
      where
        exeName' = fromString exeName
        envName' = fromString envName

    printGeneratingBashrc out = liftIO . IO.putStrLn
        $ "Generating Bash *rc script:\n  " <> FilePath.makeRelative root out
doYxStuff
    :: IORef (Maybe ProjectConfig)
    -> FilePath
    -- ^ Absolute path to YX executable.
    -> ProjectRoot
    -- ^ Absolute path to project root directory. Everthing initiation does is
    -- relative to this directory.
    -> FilePath
    -- ^ Absolute path to YX project configuration, which is used when new one
    -- has to be created.
    -> Maybe FilePath
    -- ^ Possibly absolute path to existing YX project configuration. 'Nothing'
    -- if there is no such file.
    -> IO ()
doYxStuff cfgRef yxExe root defaultYxConfig possibleYxConfig = shake opts $ do
    getProjectCfg' <- Shake.newCache $ \(yxConfigChanged, cacheFile) -> liftIO
        $ let memo r = r <$ atomicWriteIORef cfgRef (Just r)
              getMemo = readIORef cfgRef
        in
            -- When configuration file has been changed then the cache must be
            -- invalidated. To avoid reading cache file (efficiently stored
            -- version of parsed configuration file) multiple times, we memoize
            -- the parsing result.
            if yxConfigChanged
                then parseAndCacheYxConfig yxConfig cacheFile >>= memo
                else getMemo >>= \case
                    Just r -> pure r
                    Nothing ->
                        -- We don't need to memoize cache file reads, because,
                        -- it is done for us by Shake. See 'Shake.newCache' for
                        -- more details.
                        readCachedYxConfig cacheFile
                            `catch` \(CacheDecodingException _ _) ->
                                -- This should happen only when cache file uses
                                -- old data format and we need to reparse
                                -- project config. As a result it may force
                                -- some additional rules to be executed, but
                                -- the result should always be consistent.
                                parseAndCacheYxConfig yxConfig cacheFile

    let compileProjectCfg = getProjectCfg' . (True, )
        getProjectCfg = getProjectCfg' . (False, )

    Shake.want ["yx-initialization"]

    -- We need to create YX project file only if there isn't one. To correctly
    -- track the dependencies we need this rule to be a pattern rule instead of
    -- phony rule.
    yxConfig %> \out -> liftIO $ do
        -- Even though this is not a phony rule, we need to check for existence
        -- of the file, otherwise we would overwrite any user defined
        -- configuration.
        haveConfig <- doesFileExist out
        unless haveConfig $ liftIO $ do
            IO.putStrLn $ "Generating YX configuration file for this project: "
                <> FilePath.makeRelative root out
            createProjectConfig root >>= Text.writeFile out
            IO.putStrLn ""

    cfgCacheFile %> \out -> do
        -- We expect yxConfig to be already present. See "yx-initialization" rule
        -- for details.
        Shake.need [yxConfig]

        -- (BIG) TODO: Delete old artifacts in .yx-stuff dir. Currently we
        -- would keep e.g. old links in "bin" dir.
        () <$ compileProjectCfg out

    yxShellStuff root "*" Bash </> "bashrc" %> \out -> do
        Shake.need [cfgCacheFile]
        cfg <- getProjectCfg cfgCacheFile
        printGeneratingBashrc out
        compileBashrc cfg (FilePath.makeRelative yxEnvStuff out) out
        liftIO $ IO.putStrLn ""

    yxShellStuff root "*" Bash </> "completion" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    yxShellStuff root "*" Bash </> "environment" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    yxShellStuff root "*" Bash </> "aliases" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    -- Combinator 'Shake.alternatives' allows us to use overlapping patterns.
    Shake.alternatives $ do
        -- Symbolic link to "yx" is a special case. We want to handle it
        -- separately to make that fact explicit.
        yxExeStuff root "*" </> "yx" %> \out ->
            createExecutableLink root yxExe out

        yxExeStuff root "*" </> "*" %> \out -> do
            Shake.need [cfgCacheFile]
            (envName, exeName) <- parseBinFileName yxEnvStuff out
            cfg <- getProjectCfg cfgCacheFile
            lookupExe cfg envName exeName >>= \exe -> case _type exe of
                Alias -> error $ out
                    <> ": Trying to create binary, but shell alias was found."
                Command -> createExecutableLink root yxExe out
                Symlink ->
                    createExecutableLink root (Text.unpack $ _command exe) out

    -- Top level rule:
    "yx-initialization" ~> do
        -- No matter what, we need to parse YX project configuration file, or
        -- read the cached version if its available.
        Shake.need [cfgCacheFile]

        -- We need to traverse the configuration file to know what tasks need
        -- to be done executed.
        getProjectCfg cfgCacheFile >>= mapHM_ envDependencies . _environments
  where
    yxConfig = fromMaybe defaultYxConfig possibleYxConfig
    cfgCacheFile = yxStuffFile root CachedStuff "config.bin"
    yxEnvStuff = yxStuffPath (Just root) (EnvironmentStuff Nothing)

    opts = shakeOptions
        { shakeFiles = yxStuffPath (Just root) CachedStuff
--      , shakeLint = Just Shake.LintFSATrace
        }

    mapHM_ :: Applicative f => (k -> v -> f ()) -> HashMap k v -> f ()
    mapHM_ f = HashMap.foldrWithKey (\k -> (*>) . f k) $ pure ()

    forHM_ = flip mapHM_

    envDependencies name Environment{_bin = bins} = do
        needBin name "yx"
        Shake.need [yxShellStuff root (Text.unpack name) Bash </> "bashrc"]
        forHM_ bins $ \exeName Executable{_type = t} -> case t of
            Alias   -> pure ()  -- Aliases are handled by shell.
            Command -> needBin name exeName
            Symlink -> needBin name exeName

    needBin name bin = do
        Shake.need [yxExeStuff root (Text.unpack name) </> Text.unpack bin]

    parseBinFileName dir file
      | haveParseError = error $ file <> ": Unexpected path when parsing\
        \'yxStuff </> envName </> \"bin\" </> exeName'"
      | otherwise      = pure (envName, exeName)
      where
        haveParseError = List.null envName || List.null exeName

        envName = FilePath.dropTrailingPathSeparator envDir

        (envDir, _) = FilePath.splitFileName
            $ FilePath.dropTrailingPathSeparator binDir

        (binDir, exeName) = FilePath.splitFileName
            $ FilePath.makeRelative dir file

    lookupExe :: ProjectConfig -> String -> String -> Shake.Action Executable
    lookupExe ProjectConfig{_environments = envs} envName exeName =
        case HashMap.lookup envName' envs >>= HashMap.lookup exeName' . _bin of
            Nothing -> error $ exeName <> ": Unable to find configuration for\
                \ executable in environment '" <> envName <> "'"
            Just r -> pure r
      where
        exeName' = fromString exeName
        envName' = fromString envName

    printGeneratingBashrc out = liftIO . IO.putStrLn
        $ "Generating Bash *rc script:\n  " <> FilePath.makeRelative root out
doYxStuff cfgRef yxExe root defaultYxConfig possibleYxConfig = shake opts $ do
    getProjectCfg' <- Shake.newCache $ \(yxConfigChanged, cacheFile) -> liftIO
        $ let memo r = r <$ atomicWriteIORef cfgRef (Just r)
              getMemo = readIORef cfgRef
        in
            -- When configuration file has been changed then the cache must be
            -- invalidated. To avoid reading cache file (efficiently stored
            -- version of parsed configuration file) multiple times, we memoize
            -- the parsing result.
            if yxConfigChanged
                then parseAndCacheYxConfig yxConfig cacheFile >>= memo
                else getMemo >>= \case
                    Just r -> pure r
                    Nothing ->
                        -- We don't need to memoize cache file reads, because,
                        -- it is done for us by Shake. See 'Shake.newCache' for
                        -- more details.
                        readCachedYxConfig cacheFile
                            `catch` \(CacheDecodingException _ _) ->
                                -- This should happen only when cache file uses
                                -- old data format and we need to reparse
                                -- project config. As a result it may force
                                -- some additional rules to be executed, but
                                -- the result should always be consistent.
                                parseAndCacheYxConfig yxConfig cacheFile

    let compileProjectCfg = getProjectCfg' . (True, )
        getProjectCfg = getProjectCfg' . (False, )

    Shake.want ["yx-initialization"]

    -- We need to create YX project file only if there isn't one. To correctly
    -- track the dependencies we need this rule to be a pattern rule instead of
    -- phony rule.
    yxConfig %> \out -> liftIO $ do
        -- Even though this is not a phony rule, we need to check for existence
        -- of the file, otherwise we would overwrite any user defined
        -- configuration.
        haveConfig <- doesFileExist out
        unless haveConfig $ liftIO $ do
            IO.putStrLn $ "Generating YX configuration file for this project: "
                <> FilePath.makeRelative root out
            createProjectConfig root >>= Text.writeFile out
            IO.putStrLn ""

    cfgCacheFile %> \out -> do
        -- We expect yxConfig to be already present. See "yx-initialization" rule
        -- for details.
        Shake.need [yxConfig]

        -- (BIG) TODO: Delete old artifacts in .yx-stuff dir. Currently we
        -- would keep e.g. old links in "bin" dir.
        () <$ compileProjectCfg out

    yxShellStuff root "*" Bash </> "bashrc" %> \out -> do
        Shake.need [cfgCacheFile]
        cfg <- getProjectCfg cfgCacheFile
        printGeneratingBashrc out
        compileBashrc cfg (FilePath.makeRelative yxEnvStuff out) out
        liftIO $ IO.putStrLn ""

    yxShellStuff root "*" Bash </> "completion" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    yxShellStuff root "*" Bash </> "environment" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    yxShellStuff root "*" Bash </> "aliases" %> \out -> do
        printGeneratingBashrc out
        -- TODO

    -- Combinator 'Shake.alternatives' allows us to use overlapping patterns.
    Shake.alternatives $ do
        -- Symbolic link to "yx" is a special case. We want to handle it
        -- separately to make that fact explicit.
        yxExeStuff root "*" </> "yx" %> \out ->
            createExecutableLink root yxExe out

        yxExeStuff root "*" </> "*" %> \out -> do
            Shake.need [cfgCacheFile]
            (envName, exeName) <- parseBinFileName yxEnvStuff out
            cfg <- getProjectCfg cfgCacheFile
            lookupExe cfg envName exeName >>= \exe -> case _type exe of
                Alias -> error $ out
                    <> ": Trying to create binary, but shell alias was found."
                Command -> createExecutableLink root yxExe out
                Symlink ->
                    createExecutableLink root (Text.unpack $ _command exe) out

    -- Top level rule:
    "yx-initialization" ~> do
        -- No matter what, we need to parse YX project configuration file, or
        -- read the cached version if its available.
        Shake.need [cfgCacheFile]

        -- We need to traverse the configuration file to know what tasks need
        -- to be done executed.
        getProjectCfg cfgCacheFile >>= mapHM_ envDependencies . _environments
  where
    yxConfig = fromMaybe defaultYxConfig possibleYxConfig
    cfgCacheFile = yxStuffFile root CachedStuff "config.bin"
    yxEnvStuff = yxStuffPath (Just root) (EnvironmentStuff Nothing)

    opts = shakeOptions
        { shakeFiles = yxStuffPath (Just root) CachedStuff
--      , shakeLint = Just Shake.LintFSATrace
        }

    mapHM_ :: Applicative f => (k -> v -> f ()) -> HashMap k v -> f ()
    mapHM_ f = HashMap.foldrWithKey (\k -> (*>) . f k) $ pure ()

    forHM_ = flip mapHM_

    envDependencies name Environment{_bin = bins} = do
        needBin name "yx"
        Shake.need [yxShellStuff root (Text.unpack name) Bash </> "bashrc"]
        forHM_ bins $ \exeName Executable{_type = t} -> case t of
            Alias   -> pure ()  -- Aliases are handled by shell.
            Command -> needBin name exeName
            Symlink -> needBin name exeName

    needBin name bin = do
        Shake.need [yxExeStuff root (Text.unpack name) </> Text.unpack bin]

    parseBinFileName dir file
      | haveParseError = error $ file <> ": Unexpected path when parsing\
        \'yxStuff </> envName </> \"bin\" </> exeName'"
      | otherwise      = pure (envName, exeName)
      where
        haveParseError = List.null envName || List.null exeName

        envName = FilePath.dropTrailingPathSeparator envDir

        (envDir, _) = FilePath.splitFileName
            $ FilePath.dropTrailingPathSeparator binDir

        (binDir, exeName) = FilePath.splitFileName
            $ FilePath.makeRelative dir file

    lookupExe :: ProjectConfig -> String -> String -> Shake.Action Executable
    lookupExe ProjectConfig{_environments = envs} envName exeName =
        case HashMap.lookup envName' envs >>= HashMap.lookup exeName' . _bin of
            Nothing -> error $ exeName <> ": Unable to find configuration for\
                \ executable in environment '" <> envName <> "'"
            Just r -> pure r
      where
        exeName' = fromString exeName
        envName' = fromString envName

    printGeneratingBashrc out = liftIO . IO.putStrLn
        $ "Generating Bash *rc script:\n  " <> FilePath.makeRelative root out