{-# 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
 , 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
-- import           Control.Lens.Indexed

-- 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           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 =
  pathIsSymbolicLink fp >>= \case
  True ->
    return $ File (Symlink ())
  False ->
    doesDirectoryExist fp >>= \case
    True ->
      return $ Directory ()
    False ->
      return $ File (Real ())

-- | 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)

-- | 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)
  --   -- ix key fn . getInternalFileMap
  --   go key' tree@(DirTree node) =
  --     case key' of
  --       [] -> fn tree
  --       k : rest ->
  --         case node of
  --           Directory a -> directory <$> ix k (ix rest fn) a
  --           File _ -> pure tree
  {-# 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 #-}