{-# Language LambdaCase #-}
{-|
Description:
  Cursor-based navigation and modification of 'DirTree's.

This module should be imported qualified due to the very short names it exports.
-}
module System.Directory.Contents.Zipper where

import Control.Applicative
import Data.Map (Map)
import qualified Data.Map as Map
import System.FilePath

import System.Directory.Contents.Types

-- * Zipper

-- | A zipper for a 'DirTree'. As you navigate the tree, this keeps track of where
-- you are (which node is "focused" under your cursor) and how to reach parent, child,
-- and sibling nodes.
data DirZipper a = DirZipper
  { _dirZipper_cursor :: DirTree a -- ^ Cursor position
  , _dirZipper_siblings :: Map FilePath (DirTree a) -- ^ Siblings
  , _dirZipper_elders :: [(DirTree a, Map FilePath (DirTree a))]
  -- ^ Parents and aunts/uncles, in reverse order (i.e., immediate ancestors first)
  }
  deriving (Show, Read, Eq, Ord)

-- | Construct a zipper out of a 'DirTree'. Use 'focused' or 'unzipped' to get back
-- a 'DirTree'
zipped :: DirTree a -> DirZipper a
zipped a = DirZipper a Map.empty []

-- | The currently focused/selected node (and its children).  In other words,
-- where you are in the directory hierarchy.
focused :: DirZipper a -> DirTree a
focused = _dirZipper_cursor

-- | Throws away your current cursor information and returns the entire 'DirTree'
-- contained by the 'DirZipper'.
--
-- > unzipped . zipped == id
--
unzipped :: DirZipper a -> DirTree a
unzipped = focused . home

-- | Move down a level in the directory hierarchy. To move down to a specific child,
-- use 'downTo'.
down :: DirZipper a -> Maybe (DirZipper a)
down dz = case dz of
  DirZipper p@(DirTree_Dir _ xs) siblings parents ->
    withFirstChild xs $ \firstChild children ->
      DirZipper firstChild children $ (p, siblings) : parents
  DirZipper p@(DirTree_Symlink _ (Symlink_External _ xs)) siblings parents ->
    withFirstChild xs $ \firstChild children ->
      DirZipper firstChild children $ (p, siblings) : parents
  DirZipper (DirTree_Symlink _ (Symlink_Internal _ ref)) _ _ ->
    followRelative ref $ home dz
  _ -> Nothing

-- | Move up a level in the directory hierarchy, back to the parent that you
-- previously moved 'down' through.
up :: DirZipper a -> Maybe (DirZipper a)
up = \case
  DirZipper c s ((parent, uncles):ps) ->
    Just $ DirZipper (update c s parent) uncles ps
  _ -> Nothing
  where
    update :: DirTree a -> Map FilePath (DirTree a) -> DirTree a -> DirTree a
    update child siblings parent = case parent of
      DirTree_Dir f _ -> DirTree_Dir f $ insertSibling child siblings
      DirTree_Symlink f (Symlink_External s _) ->
        DirTree_Symlink f $ Symlink_External s $ insertSibling child siblings
      _ -> parent

-- | Go to the top of the directory hierarchy.
home :: DirZipper a -> DirZipper a
home dz =
  let upmost z = maybe z upmost $ up z
  in upmost dz

-- | Navigation directions for sibling nodes
data NavSibling = NavLeft | NavRight

-- | Move to the sibling next to the focused node
nextSibling :: NavSibling -> DirZipper a -> Maybe (DirZipper a)
nextSibling nav (DirZipper cursor siblings parents) =
  let kids = insertSibling cursor siblings
      next = case nav of
        NavRight -> Map.lookupGT (fileName cursor) kids
        NavLeft -> Map.lookupLT (fileName cursor) kids
  in case next of
      Nothing -> Nothing
      Just (_, sibling) -> Just $
        DirZipper sibling (removeSibling sibling kids) parents

-- | Move to the sibling to the left of the focused node
left :: DirZipper a -> Maybe (DirZipper a)
left = nextSibling NavLeft

-- | Move to the sibling to the right of the focused node
right :: DirZipper a -> Maybe (DirZipper a)
right = nextSibling NavRight

-- | Go to a particular sibling
toSibling :: FileName -> DirZipper a -> Maybe (DirZipper a)
toSibling name (DirZipper cursor siblings parents) =
  case Map.lookup name siblings of
    Nothing -> Nothing
    Just sibling ->
      let otherSiblings = insertSibling cursor $
            removeSibling sibling siblings
      in Just $ DirZipper sibling otherSiblings parents

-- | Move down in the directory hierarchy to a particular child
downTo :: FileName -> DirZipper a -> Maybe (DirZipper a)
downTo name z = do
  d <- down z
  if fileName (focused d) == name
    then pure d
    else toSibling name d

-- | Modify the focused node
mapCursor
  :: (DirTree a -> DirTree a)
  -> DirZipper a
  -> DirZipper a
mapCursor f (DirZipper cursor siblings parents) =
  DirZipper (f cursor) siblings parents

-- | Replace the focused node
replaceCursor
  :: DirTree a
  -> DirZipper a
  -> DirZipper a
replaceCursor = mapCursor . const

-- | Add a new sibling to the focused node's generation and focus on it
insert
  :: DirTree a
  -> DirZipper a
  -> DirZipper a
insert d (DirZipper cursor siblings parents) =
  DirZipper
    d
    (insertSibling cursor siblings)
    parents

-- | Remove the focused node
remove
  :: DirZipper a
  -> Maybe (DirZipper a)
remove z@(DirZipper cursor _ _) =
  let rm (DirZipper c s p) =
        DirZipper c (removeSibling cursor s) p
  in case rm <$> (left z <|> right z) of
    Just s -> Just s
    Nothing -> case up z of
      Nothing -> Nothing
      Just dz -> Just $ flip replaceCursor dz $
        case _dirZipper_cursor dz of
          DirTree_Dir f _ -> DirTree_Dir f Map.empty
          DirTree_Symlink f (Symlink_External s _) ->
            DirTree_Symlink f (Symlink_External s Map.empty)
          x -> x

-- | Try to navigate the provided (possibly relative) path.
followRelative
  :: FilePath
  -> DirZipper a
  -> Maybe (DirZipper a)
followRelative path dz =
  let follow r z = case r of
        "." -> Just z
        ".." -> up z
        _ -> downTo r z <|> toSibling r z
      go rs z = case rs of
        [] -> Just z
        (r:more) -> go more =<< follow r z
  in go (splitDirectories path) dz

-- | If the focused node is an internal symlink (see 'Symlink'), try to get
-- to the target.
followLink
  :: DirZipper a
  -> Maybe (DirZipper a)
followLink z = case z of
  DirZipper (DirTree_Symlink _ (Symlink_Internal s _)) _ _ -> followRelative s z
  _ -> Nothing