------------------------------------------------------------------------------
-- | A zipper for navigating and modifying XML trees.  This is nearly the
-- same exposed interface as the @xml@ package in @Text.XML.Light.Cursor@,
-- with modifications as needed to adapt to different types.
module Text.XmlHtml.Cursor (
    -- * Cursor type
    Cursor,

    -- * Conversion to and from cursors
    fromNode,
    fromNodes,
    topNode,
    topNodes,
    current,
    siblings,

    -- * Cursor navigation
    parent,
    root,
    getChild,
    firstChild,
    lastChild,
    left,
    right,
    nextDF,

    -- * Search
    findChild,
    findLeft,
    findRight,
    findRec,

    -- * Node classification
    isRoot,
    isFirst,
    isLast,
    isLeaf,
    isChild,
    hasChildren,
    getNodeIndex,

    -- * Updates
    setNode,
    modifyNode,
    modifyNodeM,

    -- * Insertions
    insertLeft,
    insertRight,
    insertManyLeft,
    insertManyRight,
    insertFirstChild,
    insertLastChild,
    insertManyFirstChild,
    insertManyLastChild,
    insertGoLeft,
    insertGoRight,

    -- * Deletions
    removeLeft,
    removeRight,
    removeGoLeft,
    removeGoRight,
    removeGoUp
    ) where

import           Control.Monad
import           Data.Maybe
import           Data.Text (Text)
import           Text.XmlHtml

------------------------------------------------------------------------------
-- | Just the tag of an element
type Tag = (Text, [(Text, Text)])


------------------------------------------------------------------------------
-- | Reconstructs an element from a tag and a list of its children.
fromTag :: Tag -> [Node] -> Node
fromTag (t,a) c = Element t a c


------------------------------------------------------------------------------
-- | A zipper for XML document forests.
data Cursor = Cursor {
    current :: !Node,   -- ^ Retrieves the current node of a 'Cursor'
    lefts   :: ![Node],                 -- right to left
    rights  :: ![Node],                 -- left to right
    parents :: ![([Node], Tag, [Node])] -- parent's tag and siblings
    }
    deriving (Eq)


------------------------------------------------------------------------------
-- | Builds a 'Cursor' for navigating a tree. That is, a forest with a single
-- root 'Node'.
fromNode :: Node -> Cursor
fromNode n = Cursor n [] [] []


------------------------------------------------------------------------------
-- | Builds a 'Cursor' for navigating a forest with the given list of roots.
-- The cursor is initially positioned at the left-most node.  Gives 'Nothing'
-- if the list is empty.
fromNodes :: [Node] -> Maybe Cursor
fromNodes (n:ns) = Just (Cursor n [] ns [])
fromNodes []     = Nothing


------------------------------------------------------------------------------
-- | Retrieves the root node containing the current cursor position.
topNode :: Cursor -> Node
topNode cur  = current (root cur)


------------------------------------------------------------------------------
-- | Retrieves the entire forest of 'Node's corresponding to a 'Cursor'.
topNodes :: Cursor -> [Node]
topNodes cur = siblings (root cur)


------------------------------------------------------------------------------
-- | Retrieves a list of the 'Node's at the same level as the current position
-- of a cursor, including the current node.
siblings :: Cursor -> [Node]
siblings (Cursor cur ls rs _) = foldl (flip (:)) (cur:rs) ls


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' to its parent in the document.
parent :: Cursor -> Maybe Cursor
parent c@(Cursor _ _ _ ((ls,t,rs):ps))
            = Just (Cursor (fromTag t (siblings c)) ls rs ps)
parent _    = Nothing


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' up through parents to reach the root level.
root :: Cursor -> Cursor
root = until isRoot (fromJust . parent)


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' down to the indicated child index.
getChild :: Int -> Cursor -> Maybe Cursor
getChild i (Cursor n ls rs ps) =
    case n of
      Element t a cs -> let (lls, rest) = splitAt i cs in
          if i >= length cs || i < 0
            then Nothing
            else Just $ Cursor (head rest)
                               (reverse lls)
                               (tail rest)
                               ((ls, (t,a), rs):ps)
      _              -> Nothing


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' down to its first child.
firstChild :: Cursor -> Maybe Cursor
firstChild = getChild 0


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' down to its last child.
lastChild :: Cursor -> Maybe Cursor
lastChild (Cursor (Element t a c) ls rs ps) | not (null c)
    = let rc = reverse c
      in  Just $ Cursor (head rc) (tail rc) [] ((ls, (t,a), rs):ps)
lastChild _
    = Nothing


------------------------------------------------------------------------------
-- | Moves a 'Cursor' to its left sibling.
left :: Cursor -> Maybe Cursor
left (Cursor c (l:ls) rs ps) = Just (Cursor l ls (c:rs) ps)
left _                       = Nothing


------------------------------------------------------------------------------
-- | Moves a 'Cursor' to its right sibling.
right :: Cursor -> Maybe Cursor
right (Cursor c ls (r:rs) ps) = Just (Cursor r (c:ls) rs ps)
right _                       = Nothing


------------------------------------------------------------------------------
-- | Moves a 'Cursor' to the next node encountered in a depth-first search.
-- If it has children, this is equivalent to 'firstChild'.  Otherwise, if it
-- has a right sibling, then this is equivalent to 'right'.  Otherwise, the
-- cursor moves to the first right sibling of one of its parents.
nextDF :: Cursor -> Maybe Cursor
nextDF c = firstChild c `mplus` up c
  where up x = right x `mplus` (up =<< parent x)


------------------------------------------------------------------------------
-- | Repeats the given move until a 'Cursor' is obtained that matches the
-- predicate.
search :: (Cursor -> Bool)         -- ^ predicate
       -> (Cursor -> Maybe Cursor) -- ^ move
       -> Cursor                   -- ^ starting point
       -> Maybe Cursor
search p move c | p c       = return c
                | otherwise = search p move =<< move c


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' to the first child that matches the predicate.
findChild :: (Cursor -> Bool) -> Cursor -> Maybe Cursor
findChild p cur = search p right =<< firstChild cur


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' to the nearest left sibling that matches a
-- predicate.
findLeft :: (Cursor -> Bool) -> Cursor -> Maybe Cursor
findLeft p cur = search p left =<< left cur


------------------------------------------------------------------------------
-- | Navigates a 'Cursor' to the nearest right sibling that matches a
-- predicate.
findRight :: (Cursor -> Bool) -> Cursor -> Maybe Cursor
findRight p cur = search p right =<< right cur


------------------------------------------------------------------------------
-- | Does a depth-first search for a descendant matching the predicate.  This
-- can match the current cursor position.
findRec :: (Cursor -> Bool) -> Cursor -> Maybe Cursor
findRec p = search p nextDF


------------------------------------------------------------------------------
-- | Determines if the 'Cursor' is at a root node.
isRoot :: Cursor -> Bool
isRoot cur = null (parents cur)


------------------------------------------------------------------------------
-- | Determines if the 'Cursor' is at a first child.
isFirst :: Cursor -> Bool
isFirst cur = null (lefts cur)


------------------------------------------------------------------------------
-- | Determines if the 'Cursor' is at a last child.
isLast :: Cursor -> Bool
isLast cur = null (rights cur)


------------------------------------------------------------------------------
-- | Determines if the 'Cursor' is at a leaf node.
isLeaf :: Cursor -> Bool
isLeaf (Cursor (Element _ _ c) _ _ _) = null c
isLeaf _                              = True


------------------------------------------------------------------------------
-- | Determines if the 'Cursor' is at a child node (i.e., if it has a parent).
isChild :: Cursor -> Bool
isChild = not . isRoot


------------------------------------------------------------------------------
-- | Determines if the 'Cursor' is at a non-leaf node (i.e., if it has
-- children).
hasChildren :: Cursor -> Bool
hasChildren = not . isLeaf


------------------------------------------------------------------------------
-- | Gets the index of the 'Cursor' among its siblings.
getNodeIndex :: Cursor -> Int
getNodeIndex cur = length (lefts cur)


------------------------------------------------------------------------------
-- | Replaces the current node.
setNode :: Node -> Cursor -> Cursor
setNode n cur = cur { current = n }


------------------------------------------------------------------------------
-- | Modifies the current node by applying a function.
modifyNode :: (Node -> Node) -> Cursor -> Cursor
modifyNode f cur = setNode (f (current cur)) cur


------------------------------------------------------------------------------
-- | Modifies the current node by applying an action in some functor.
modifyNodeM :: Functor m => (Node -> m Node) -> Cursor -> m Cursor
modifyNodeM f cur = flip setNode cur `fmap` f (current cur)


------------------------------------------------------------------------------
-- | Inserts a new 'Node' to the left of the current position.
insertLeft :: Node -> Cursor -> Cursor
insertLeft n (Cursor nn ls rs ps) = Cursor nn (n:ls) rs ps


------------------------------------------------------------------------------
-- | Inserts a new 'Node' to the right of the current position.
insertRight :: Node -> Cursor -> Cursor
insertRight n (Cursor nn ls rs ps) = Cursor nn ls (n:rs) ps


------------------------------------------------------------------------------
-- | Inserts a list of new 'Node's to the left of the current position.
insertManyLeft :: [Node] -> Cursor -> Cursor
insertManyLeft ns (Cursor nn ls rs ps) = Cursor nn (reverse ns ++ ls) rs ps


------------------------------------------------------------------------------
-- | Inserts a list of new 'Node's to the right of the current position.
insertManyRight :: [Node] -> Cursor -> Cursor
insertManyRight ns (Cursor nn ls rs ps) = Cursor nn ls (ns ++ rs) ps


------------------------------------------------------------------------------
-- | Inserts a 'Node' as the first child of the current element.
insertFirstChild :: Node -> Cursor -> Maybe Cursor
insertFirstChild n (Cursor (Element t a c) ls rs ps)
    = Just (Cursor (Element t a (n:c)) ls rs ps)
insertFirstChild _ _
    = Nothing


------------------------------------------------------------------------------
-- | Inserts a 'Node' as the last child of the current element.
insertLastChild :: Node -> Cursor -> Maybe Cursor
insertLastChild n (Cursor (Element t a c) ls rs ps)
    = Just (Cursor (Element t a (c ++ [n])) ls rs ps)
insertLastChild _ _
    = Nothing


------------------------------------------------------------------------------
-- | Inserts a list of 'Node's as the first children of the current element.
insertManyFirstChild :: [Node] -> Cursor -> Maybe Cursor
insertManyFirstChild ns (Cursor (Element t a c) ls rs ps)
    = Just (Cursor (Element t a (ns ++ c)) ls rs ps)
insertManyFirstChild _ _
    = Nothing


------------------------------------------------------------------------------
-- | Inserts a list of 'Node's as the last children of the current element.
insertManyLastChild :: [Node] -> Cursor -> Maybe Cursor
insertManyLastChild ns (Cursor (Element t a c) ls rs ps)
    = Just (Cursor (Element t a (c ++ ns)) ls rs ps)
insertManyLastChild _ _
    = Nothing


------------------------------------------------------------------------------
-- | Inserts a new 'Node' to the left of the current position, and moves
-- left to the new node.
insertGoLeft :: Node -> Cursor -> Cursor
insertGoLeft n (Cursor nn ls rs ps) = Cursor n ls (nn:rs) ps


------------------------------------------------------------------------------
-- | Inserts a new 'Node' to the right of the current position, and moves
-- right to the new node.
insertGoRight :: Node -> Cursor -> Cursor
insertGoRight n (Cursor nn ls rs ps) = Cursor n (nn:ls) rs ps


------------------------------------------------------------------------------
-- | Removes the 'Node' to the left of the current position, if any.
removeLeft :: Cursor -> Maybe (Node, Cursor)
removeLeft (Cursor n (l:ls) rs ps) = Just (l, Cursor n ls rs ps)
removeLeft _                       = Nothing


------------------------------------------------------------------------------
-- | Removes the 'Node' to the right of the current position, if any.
removeRight :: Cursor -> Maybe (Node, Cursor)
removeRight (Cursor n ls (r:rs) ps) = Just (r, Cursor n ls rs ps)
removeRight _                       = Nothing


------------------------------------------------------------------------------
-- | Removes the current 'Node', and moves the Cursor to its left sibling,
-- if any.
removeGoLeft :: Cursor -> Maybe Cursor
removeGoLeft (Cursor _ (l:ls) rs ps) = Just (Cursor l ls rs ps)
removeGoLeft _                       = Nothing


------------------------------------------------------------------------------
-- | Removes the current 'Node', and moves the Cursor to its right sibling,
-- if any.
removeGoRight :: Cursor -> Maybe Cursor
removeGoRight (Cursor _ ls (r:rs) ps) = Just (Cursor r ls rs ps)
removeGoRight _                       = Nothing


------------------------------------------------------------------------------
-- | Removes the current 'Node', and moves the Cursor to its parent, if any.
removeGoUp :: Cursor -> Maybe Cursor
removeGoUp (Cursor _ ls rs ((lls, (t,a), rrs):ps))
    = Just (Cursor (Element t a children) lls rrs ps)
  where
    children = foldl (flip (:)) (rs) ls
removeGoUp _                       = Nothing

