{-# LANGUAGE DeriveAnyClass        #-}
{-# LANGUAGE TemplateHaskell        #-}
{-# LANGUAGE FunctionalDependencies        #-}
{-# LANGUAGE TypeFamilies        #-}
{-# LANGUAGE DeriveFunctor         #-}
{-# LANGUAGE ViewPatterns         #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DeriveTraversable     #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TupleSections         #-}
{-|
Module      : System.DirTree
Copyright   : (c) Christian Gram Kalhauge, 2019
License     : MIT
Maintainer  : kalhauge@cs.ucla.edu

A directory tree, with helper functions to do different cool stuff. Contrary to
`directory-tree`, this package does try to add as many accessors and handlers as
possible. This is alos the reason that it depends on the Lens library.

-}
module System.DirTree
  (
   -- * 'DirTreeNode'
   -- $DirTreeNode
    DirTreeNode(..)
  , RelativeFile(..)

   -- ** Helpers
  , FileType
  , fileTypeOfNode
  , AsDirTreeNode(..)
  , AsRelativeFile(..)

   -- ** IO
  , getFileType
  , checkFileType
  , readPath

   -- * 'FileMap'
   -- $FileMap
  , FileMap(..)

 -- ** Constructors
  , emptyFileMap
  , singletonFileMap
  , toFileList
  , fromFileList
  , (.*)
  , (./)
  , (.*>)
  , (.*.)

 -- ** Accessors
  , lookupFileMap

   -- * 'DirTree'
   -- $DirTree
  , DirTree(..)
  , RelativeDirTree
  , asRelativeDirTree

   -- ** Constructors
  , file
  , realfile
  , symlink
  , directory
  , directory'
  , emptyDirectory
  , createDeepFile
  , createDeepTree

   -- ** Accessors
  , FileKey
  , fileKeyFromPath
  , fileKeyToPath
  , diffFileKey
  , diffPath
  , alterFile

   -- ** Iterators
   -- Most of the iterators can be done with the 'FunctorWithIndex',
   -- 'FoldableWithIndex', and 'TraversableWithIndex', but some accumilations
   -- are easier.
  , iflattenDirTree
  , flattenDirTree
  , depthfirst
  , findNode
  , listNodes

   -- ** IO
  , readDirTree
  , writeDirTree
  , Link(..)
  , toLink
  , readRelativeDirTree
  , followLinks
  , writeRelativeDirTree

 -- * 'DirForest'
 -- $DirForest
  , DirForest(..)
  , RelativeDirForest
  , ForestFileKey
  , fromForestFileKey
  , toForestFileKey

 -- ** Constructors
  , asRelativeDirForest
  , emptyForest
  , singletonForest
  , createDeepForest

 -- ** Iterators
  , alterForest
  )
where

-- containers
import qualified Data.Map                      as Map

-- deepseq
import           Control.DeepSeq

-- directory
import           System.Directory        hiding ( findFile )

-- filepath
import           System.FilePath

-- lens
import           Control.Lens.Combinators
import           Control.Lens

-- base
import           Data.Functor
import           Data.Foldable
import           Data.Bifunctor
import           Data.Maybe
import           Data.List.NonEmpty             ( NonEmpty(..)
                                                , nonEmpty
                                                )
import           Data.Semigroup                 ( sconcat )
import           Data.Monoid
import           Data.Bitraversable
import           Data.Bifoldable
import           System.IO.Error
import           Control.Monad
import           Text.Show
import           GHC.Generics

-- $DirTreeNode
-- The basic item of this library is a DirTreeNode.

-- | A directory tree node. Everything is either a file, or a
-- directory.
data DirTreeNode r a
  = Directory r
  | File a
  deriving (Show, Eq, Ord, Functor, Foldable, Traversable, NFData, Generic)

instance Bifunctor DirTreeNode where
  bimap fr fa = \case
    Directory r -> Directory (fr r)
    File      a -> File (fa a)

instance Bifoldable DirTreeNode where
  bifoldMap fr fa = \case
    Directory r -> fr r
    File      a -> fa a

instance Bitraversable DirTreeNode where
  bitraverse fr fa = \case
    Directory r -> Directory <$> fr r
    File      a -> File <$> fa a

makeClassyPrisms ''DirTreeNode

-- | A DirTree can contain relativeFile files. This means that some files might be
-- symlinks.
data RelativeFile s a
  = Symlink s
  | Real a
  deriving (Show, Eq, Ord, Functor, Foldable, Traversable, NFData, Generic)

instance Bifunctor RelativeFile where
  bimap fr fa = \case
    Symlink r -> Symlink (fr r)
    Real    a -> Real (fa a)

instance Bifoldable RelativeFile where
  bifoldMap fr fa = \case
    Symlink r -> fr r
    Real    a -> fa a

instance Bitraversable RelativeFile where
  bitraverse fr fa = \case
    Symlink r -> Symlink <$> fr r
    Real    a -> Real <$> fa a

makeClassyPrisms ''RelativeFile

-- | It is quite offten that a node will be used as a relative file.
instance AsRelativeFile (DirTreeNode a (RelativeFile b c)) b c where
  _RelativeFile = _File

-- | A `FileType` is just a `DirTreeNode` with no contents.
type FileType = DirTreeNode () (RelativeFile () ())

-- | Gets the `FileType` of a `DirTreeNode`
fileTypeOfNode :: DirTreeNode a (RelativeFile b c) -> FileType
fileTypeOfNode = bimap (const ()) (bimap (const ()) (const ()))

-- | Check a filepath for Type, throws an IOException if path does not exist.
getFileType :: FilePath -> IO FileType
getFileType fp =
  -- TODO: Throw a resonable exception if the file does not exist.
                 pathIsSymbolicLink fp >>= \case
  True  -> return $ File (Symlink ())
  False -> doesDirectoryExist fp >>= \case
    True  -> return $ Directory ()
    False -> return $ File (Real ())

-- | Check a filepath for Type, return Nothing if the path does not
-- exist.
checkFileType :: FilePath -> IO (Maybe FileType)
checkFileType fp =
  catchIOError (Just <$> getFileType fp) (const . return $ Nothing)

-- | Reads the structure of the filepath
readPath :: FilePath -> IO (DirTreeNode [String] (RelativeFile FilePath ()))
readPath fp =
  bitraverse (const $ listDirectory fp)
             (bitraverse (const $ getSymbolicLinkTarget fp) return)
    =<< getFileType fp


-- $FileMap
-- The 'FileMap' is used to represent the content of a directory.

-- | A map from file names to
newtype FileMap a =
  FileMap { fileMapAsMap :: Map.Map String a }
  deriving (Eq, Ord, NFData, Generic, Functor, Foldable, Traversable)

-- | Single File
singletonFileMap :: String -> a -> FileMap a
singletonFileMap s a = FileMap (Map.singleton s a)

-- | An empty filemap
emptyFileMap :: FileMap a
emptyFileMap = FileMap Map.empty

-- | The 'FileMap' is a semigroup if the contnent is. It tries
-- to union the content under each item.
instance Semigroup a => Semigroup (FileMap a) where
  FileMap as <> FileMap bs = FileMap (Map.unionWith (<>) as bs)

-- | The empty monoid is the emptyFileMap
instance Semigroup a => Monoid (FileMap a) where
  mempty = emptyFileMap

instance FunctorWithIndex String FileMap
instance FoldableWithIndex String FileMap
instance TraversableWithIndex String FileMap where
  itraverse f (FileMap fs) = FileMap <$> itraverse f fs
  {-# INLINE itraverse #-}

instance Show a => Show (DirForest a) where
  showsPrec d m =
    showParen (d > 9) $ showString "DirForest . fromFileList " . showFileList m
   where
    showFileList =
      showListWith (\(s, x) -> f s $ dirTreeNode x)
        . toFileList
        . getInternalFileMap

    f s (Directory x) =
      showsPrec (dir_prec + 1) s . showString " ./ " . showFileList x

    f s (File x) =
      showsPrec (dir_prec + 1) s
        . showString " .* "
        . showsPrec (dir_prec + 1) x
    dir_prec = 5


-- | Create a list of pairs of filenames and file values.
toFileList :: FileMap a -> [(String, a)]
toFileList (FileMap a) = Map.toList a

-- | Create a `FileMap` from a list of pairs of filenames a file values.
fromFileList :: [(String, a)] -> FileMap a
fromFileList = FileMap . Map.fromList

-- | Find a list of names used in the FileMap
toFileNames :: FileMap a -> [String]
toFileNames = map fst . toFileList

-- | Lookup a file using a filename
lookupFileMap :: String -> FileMap a -> Maybe a
lookupFileMap s (FileMap a) = Map.lookup s a

-- | The 'Map.alterF' version to the FileMap.
alterFileMap
  :: Functor f
  => (Maybe a -> f (Maybe a))
  -> String
  -> FileMap a
  -> f (FileMap a)
alterFileMap fn key (FileMap fm) = FileMap <$> Map.alterF fn key fm

type instance Index (FileMap a) = String
type instance IxValue (FileMap a) = a

instance Ixed (FileMap a) where
  ix k f m = FileMap <$> ix k f (fileMapAsMap m)
  {-# INLINE ix #-}

instance At (FileMap a) where
  at = flip alterFileMap
  {-# INLINE at #-}

-- $DirTree
-- A 'DirTree' is a recursive difined tree.
--

-- | A 'FileKey' is a list of filenames to get to the final file
type FileKey = [String]

-- | A 'DirTreeN' represents a single level in the DirTree.
type DirTreeN a = DirTreeNode (DirForest a) a

-- | A specialized traversal of the DirTreeNode
itraverseDirTreeN
  :: Applicative f => (FileKey -> a -> f b) -> DirTreeN a -> f (DirTreeN b)
itraverseDirTreeN fia = \case
  Directory m -> Directory <$> itraverse (fia . fromForestFileKey) m
  File      a -> File <$> fia [] a

-- | A dir tree is a tree of nodes.
newtype DirTree a = DirTree
  { dirTreeNode :: DirTreeNode (DirForest a) a
  }
  deriving (Eq, Ord, NFData, Generic)

instance Functor DirTree where
  fmap f (DirTree a) = DirTree $ bimap (fmap f) f a

instance Foldable DirTree where
  foldMap f (DirTree e) = bifoldMap (foldMap f) f e

instance Traversable DirTree where
  traverse f (DirTree e) = DirTree <$> bitraverse (traverse f) f e

instance FunctorWithIndex FileKey DirTree
instance FoldableWithIndex FileKey DirTree
instance TraversableWithIndex FileKey DirTree where
  itraverse f (DirTree fs) = DirTree <$> itraverseDirTreeN f fs
  {-# INLINE itraverse #-}

-- | A relative dir tree also exists.
type RelativeDirTree s a = DirTree (RelativeFile s a)

-- | All 'DirTree's are also relative.
asRelativeDirTree :: DirTree a -> RelativeDirTree s a
asRelativeDirTree = fmap Real

instance (Show a) => Show (DirTree a) where
  showsPrec d c = showParen (d > 9) (f $ dirTreeNode c)
   where
    f = \case
      Directory a -> showString "directory " . showsPrec 11 a
      File      a -> showString "file " . showsPrec 11 a

-- | A DirTree is a semigroup, where it merges directories and take the last
-- entry if there files.
--
-- >>> file 'a' <> file 'b'
-- file 'b'
--
-- >>> directory' [ "a" .* 'a', "b" .* 'b'] <> directory' [ "b" .* 'd', "c" .* 'c']
-- directory (fromFileList ["a" .* 'a',"b" .* 'd',"c" .* 'c'])
instance Semigroup (DirTree a) where
  DirTree (Directory as) <> DirTree (Directory bs) =
    DirTree (Directory (as <> bs))
  _ <> a = a

-- | Constructs a dirtree with only a file
file :: a -> DirTree a
file = DirTree . File
{-# INLINE file #-}

-- | Constructs a relative dirtree with only a real file
realfile :: a -> RelativeDirTree s a
realfile = file . Real
{-# INLINE realfile #-}

-- | Constructs a dirtree with a symlink
symlink :: s -> RelativeDirTree s a
symlink = file . Symlink
{-# INLINE symlink #-}

-- | Constructs a dirtree with a directory
directory :: DirForest a -> DirTree a
directory = DirTree . Directory
{-# INLINE directory #-}

-- | Constructs a dirtree with a file list
directory' :: [(String, DirTree a)] -> DirTree a
directory' = DirTree . Directory . DirForest . fromFileList
{-# INLINE directory' #-}

-- | Constructs a dirtree with a empty directory
emptyDirectory :: DirTree a
emptyDirectory = directory' []
{-# INLINE emptyDirectory #-}

-- | Create a file
(.*) :: String -> a -> (String, DirTree a)
(.*) s a = (s, file a)

-- | Create a symbolic link
(.*>) :: String -> s -> (String, RelativeDirTree s a)
(.*>) s a = (s, symlink a)

-- | Create a real file
(.*.) :: String -> a -> (String, RelativeDirTree s a)
(.*.) s a = (s, realfile a)

-- | Create a directory
(./) :: String -> [(String, DirTree a)] -> (String, DirTree a)
(./) s a = (s, directory' a)

-- | Get a `FileKey` from a `FilePath`
fileKeyFromPath :: FilePath -> FileKey
fileKeyFromPath = splitDirectories

-- | Get a `FilePath` from a `FileKey`
fileKeyToPath :: FileKey -> FilePath
fileKeyToPath = joinPath

-- | 'diffFileKey' produces a filepath which is needed to
-- navigate from one FileKey to a other.
--
-- >>> diffFileKey ["hello", "world"] ["hello"]
-- ".."
--
-- >>> diffFileKey ["hello"] ["hello", "world", "test"]
-- "world/test"
--
-- >>> diffFileKey ["world", "test"] ["hello"]
-- "../../hello"
diffFileKey :: FileKey -> FileKey -> FilePath
diffFileKey f to' =
  let (n, bs) = prefix f to' in fileKeyToPath (replicate n ".." ++ bs)
 where
  prefix al@(a : as) bl@(b : bs) | a == b    = prefix as bs
                                 | otherwise = (length al, bl)
  prefix (_ : as) [] = (1 + length as, [])
  prefix []       bs = (0, bs)

-- | 'diffPath' produces a the filekey at the end of
-- a relative filepath, from one filekey.
--
-- >>> diffPath ["hello", "world"] ".."
-- Just ["hello"]
--
-- >>> diffPath ["hello"] "world/test"
-- Just ["hello","world","test"]
--
-- >>> diffPath ["world", "test"] "../../hello"
-- Just ["hello"]
--
-- >>> diffPath ["world", "test"] "/hello"
-- Nothing
--
-- >>> diffPath ["world", "test"] "../../.."
-- Nothing
diffPath :: FileKey -> FilePath -> Maybe FileKey
diffPath f path | isAbsolute path = Nothing
                | otherwise       = go (fileKeyFromPath path) (reverse f)
 where
  go = \case
    ".." : rest -> \case
      _ : as -> go rest as
      []     -> Nothing
    rest -> \m -> Just (reverse m ++ rest)

-- | Alter File is the 'DirTree' version of 'Map.alterF'.
--
-- >>> alterFile (\x -> [Nothing, x, Just (file 'b')]) [] (Just (file 'a'))
-- [Nothing,Just (file 'a'),Just (file 'b')]
alterFile
  :: forall f a
   . Functor f
  => (Maybe (DirTree a) -> f (Maybe (DirTree a)))
  -> FileKey
  -> Maybe (DirTree a)
  -> f (Maybe (DirTree a))
alterFile fn key = maybe (newFile key) (go key) where
  go key' tree@(DirTree node) = case key' of
    []       -> fn (Just tree)
    k : rest -> case node of
      Directory a -> Just . directory <$> alterForest fn (k :| rest) a
      File      _ -> newFile rest

  newFile :: FileKey -> f (Maybe (DirTree a))
  newFile key' = fmap (createDeepTree key') <$> fn Nothing
{-# INLINE alterFile #-}

-- | Create a recursive `DirTree` from a FileKey and a value.
createDeepFile :: FileKey -> a -> DirTree a
createDeepFile key a = createDeepTree key (file a)
{-# INLINE createDeepFile #-}

-- | Create a recursive `DirTree` from a FileKey and a value.
createDeepTree :: FileKey -> DirTree a -> DirTree a
createDeepTree key a = foldr (\s f -> directory (singletonForest s f)) a key
{-# INLINE createDeepTree #-}

type instance Index (DirTree a) = FileKey
type instance IxValue (DirTree a) = DirTree a

instance Ixed (DirTree a) where
  ix key fn = go key   where
    go key' tree@(DirTree node) = case nonEmpty key' of
      Nothing -> fn tree
      Just fk -> case node of
        Directory a -> directory <$> ix fk fn a
        File      _ -> pure tree
  {-# INLINE ix #-}

-- | Not a completly correct Lens, since it is implossible to
-- delete the current DirTree. To use a correct Lens, see
-- 'alterFile'.
--
-- >>> emptyDirectory & at ["file", "path"] ?~ file 'x'
-- directory (fromFileList ["file" ./ ["path" .* 'x']])
instance At (DirTree a) where
  at k f m = fromMaybe m <$> alterFile f k (Just m)
  {-# INLINE at #-}

-- | This method enables eta reduction of a DirTree a with an index.
iflattenDirTree :: (FileKey -> DirTreeNode (FileMap m) a -> m) -> DirTree a -> m
iflattenDirTree f = go id where
  go fk =
    f (fk [])
      . first (imap (\k -> go (fk . (k :))) . getInternalFileMap)
      . dirTreeNode
{-# INLINE iflattenDirTree #-}

-- | This method enables eta reduction of a DirTree a.
flattenDirTree :: (DirTreeNode (FileMap m) a -> m) -> DirTree a -> m
flattenDirTree f = go
  where go = f . first (fmap go . getInternalFileMap) . dirTreeNode
{-# INLINE flattenDirTree #-}

-- | Uses a semigroup to join together the results, This is slightly
-- less powerfull than 'iflattenDirTree', but more convinient for
-- summations.
depthfirst
  :: (Semigroup m) => (FileKey -> DirTreeNode [String] a -> m) -> DirTree a -> m
depthfirst f = iflattenDirTree $ \k -> \case
  Directory fm ->
    sconcat $ f k (Directory . toFileNames $ fm) :| Data.Foldable.toList fm
  File a -> f k (File a)
{-# INLINE depthfirst #-}

-- | Find a file given a predicate that takes a `FileKey` and `DirTreeNode`.
findNode
  :: (FileKey -> DirTreeNode [String] a -> Bool)
  -> DirTree a
  -> Maybe (FileKey, DirTreeNode [String] a)
findNode f = getFirst . depthfirst (\k a -> First $ guard (f k a) $> (k, a))
{-# INLINE findNode #-}

-- List all the nodes in the dirtree
listNodes :: DirTree a -> [(FileKey, DirTreeNode [String] a)]
listNodes = (`appEndo` []) . depthfirst (\k a -> Endo ((k, a) :))
{-# INLINE listNodes #-}


-- ** IO Methods

-- | A `Link` can either be `Internal`, pointing to something in the `DirTree` or
-- `External` pointing to an absolute `FilePath`.
data Link
  = Internal !FileKey
  | External !FilePath
  deriving (Show, Eq, Generic, NFData)

-- | Figure out a link from the FileKey and FilePath of Link
toLink :: FileKey -> FilePath -> Link
toLink key f = maybe (External f) Internal (diffPath (Prelude.init key) f)


-- | Reads a DirTree. All file paths are absolute to the filepath
readRelativeDirTree
  :: (FilePath -> IO a) -> FilePath -> IO (RelativeDirTree Link a)
readRelativeDirTree reader' fp = do
  from' <- canonicalizePath fp
  go from' fp
 where
  go from' fp' = do
    node <- readPath fp'
    DirTree
      <$> bimapM
            ( fmap (DirForest . fromFileList)
            . mapM (\k -> (k, ) <$> go from' (fp' </> k))
            )
            (bimapM absolute (const $ reader' fp'))
            node
   where
    absolute a
      | isAbsolute a = return $ External a
      | otherwise = do
        a' <- canonicalizePath (takeDirectory fp' </> a)
        let a'' = makeRelative from' a'
        return
          $ if a'' /= a' then Internal (fileKeyFromPath a'') else External a'

-- | Reads a DirTree and follow all the relative links. Might recurse forever.
readDirTree :: (FilePath -> IO a) -> FilePath -> IO (DirTree a)
readDirTree fn fp = readRelativeDirTree fn fp >>= followLinks fn

-- | Follow the links to create the tree. This function might recurse forever.
followLinks
  :: forall a . (FilePath -> IO a) -> RelativeDirTree Link a -> IO (DirTree a)
followLinks fn tree = go tree where
  go = flattenDirTree $ \case
    File (Symlink a) -> case a of
      Internal s -> case tree ^? ix s of
        Just a' -> go a'
        Nothing ->
          error $ "Could not find " ++ show s ++ " in the dirtree " ++ show
            (void tree)
      External s -> readDirTree fn s

    File      (Real a) -> return $ file a

    Directory a        -> directory . DirForest <$> sequence a

-- | Writes a Relative DirTree to a file
writeRelativeDirTree
  :: (FilePath -> a -> IO ()) -> FilePath -> RelativeDirTree Link a -> IO ()
writeRelativeDirTree writer fp = depthfirst go where
  go key = \case
    Directory _ -> createDirectory fp'
    File      a -> case a of
      Symlink (External target) -> createFileLink target fp'
      Symlink (Internal key'  ) -> createFileLink
        (case (key, key') of
          (_ : fk', _ ) -> diffFileKey fk' key'
          ([]     , []) -> "."
          ([]     , _ ) -> error "Fail"
        )
        fp'
      Real a' -> writer fp' a'
    where fp' = fp </> fileKeyToPath key
{-# INLINE writeRelativeDirTree #-}

-- | Writes a Relative DirTree to a file
writeDirTree :: (FilePath -> a -> IO ()) -> FilePath -> DirTree a -> IO ()
writeDirTree writer fp = writeRelativeDirTree writer fp . asRelativeDirTree
{-# INLINE writeDirTree #-}


-- $DirForest
-- A 'DirForest' is the content of a directory. A 'DirForest' is more
-- useful in some cases

newtype DirForest a = DirForest
  { getInternalFileMap :: FileMap (DirTree a)
  } deriving (Eq, Ord, NFData, Generic)

instance Functor DirForest where
  fmap f (DirForest a) = DirForest $ fmap (fmap f) a

instance Foldable DirForest where
  foldMap f (DirForest e) = foldMap (foldMap f) e

instance Traversable DirForest where
  traverse f (DirForest e) = DirForest <$> traverse (traverse f) e

instance FunctorWithIndex ForestFileKey DirForest
instance FoldableWithIndex ForestFileKey DirForest
instance TraversableWithIndex ForestFileKey DirForest where
  itraverse f (DirForest fs) =
    DirForest <$> itraverse (\k -> itraverse (f . (k :|))) fs
  {-# INLINE itraverse #-}

instance Semigroup (DirForest a) where
  (DirForest a) <> (DirForest b) = DirForest (a <> b)

instance Monoid (DirForest a) where
  mempty = DirForest mempty

-- | All entries in a DirForest has to be non-empty
type ForestFileKey = NonEmpty String

-- | Convert a 'ForestFileKey' to a 'FileKey'
fromForestFileKey :: ForestFileKey -> FileKey
fromForestFileKey = toList

-- | Convert a 'FileKey' to a 'ForestFileKey'
toForestFileKey :: FileKey -> Maybe ForestFileKey
toForestFileKey = nonEmpty

-- | Creates an empty forest
emptyForest :: DirForest a
emptyForest = mempty

-- | Creates an singleton forest
singletonForest :: String -> DirTree a -> DirForest a
singletonForest k f = DirForest $ singletonFileMap k f

-- | Creates an deep file in a forest
createDeepForest :: ForestFileKey -> DirTree a -> DirForest a
createDeepForest (k :| rest) f = singletonForest k (createDeepTree rest f)

-- | A relative dir forest also exists.
type RelativeDirForest s a = DirForest (RelativeFile s a)

-- | All 'DirTree's are also relative.
asRelativeDirForest :: DirForest a -> RelativeDirForest s a
asRelativeDirForest = fmap Real

type instance Index (DirForest a) = ForestFileKey
type instance IxValue (DirForest a) = DirTree a

instance Ixed (DirForest a) where
  ix (k :| key) fn a = DirForest <$> ix k (ix key fn) (getInternalFileMap a)
  {-# INLINE ix #-}

alterForest
  :: forall f a
   . Functor f
  => (Maybe (DirTree a) -> f (Maybe (DirTree a)))
  -> ForestFileKey
  -> DirForest a
  -> f (DirForest a)
alterForest fn (k :| key) a =
  DirForest <$> alterFileMap (alterFile fn key) k (getInternalFileMap a)

-- >>> emptyDirForest & at ("file" :| ["path"]) ?~ file 'x'
-- fromFileList ["file" ./ ["path" .* 'x']]
instance At (DirForest a) where
  at k f = alterForest f k
  {-# INLINE at #-}

makeWrapped ''DirTree
makeWrapped ''DirForest

instance AsDirTreeNode (DirTree a) (DirForest a) a where
  _DirTreeNode = _Wrapped
  {-# INLINE _DirTreeNode #-}