module Data.OrgMode.OrgDocView (
  OrgDocView(..), generateDocView, getRawElements,
  updateDoc, NodeUpdate(..)
  ) where

import Data.OrgMode.Doc
import Data.OrgMode.Text

import Data.Set (Set(..), member, lookupLE)
import Data.Maybe (mapMaybe, fromJust, catMaybes)

data OrgDocView a = OrgDocView
                    { ovElements :: [(a, Node)]
                    , ovDocument :: OrgDoc
                    }

-- | Generic visitor for updating a Node's values.  Intentionally, we
-- don't allow node deletion, just update.  Preferably, if you want to
-- delete a Node, you should control the parent.  We also have
-- findItemInNode which will construct an 'a' from the Node, which we
-- may then update against a list.
class (Eq a) => NodeUpdate a where
  findItemInNode :: Node -> Maybe a
  updateNodeLine :: a -> Node -> Maybe Node

-- Doesn't assume that xs or ys are individually sorted, which works
-- well for TextLines with no line number mid-stream.
mergeSorted :: (Ord a) => [a] -> [a] -> [a]
mergeSorted [] [] = []
mergeSorted xs [] = xs
mergeSorted [] ys = ys
mergeSorted xl@(x:xs) yl@(y:ys) =
  case compare x y of
    EQ -> x:(mergeSorted xs yl)
    LT -> x:(mergeSorted xs yl)
    GT -> y:(mergeSorted xl ys)

instance TextLineSource OrgDoc where
  getTextLines doc =
    let docLines = concatMap getTextLines $ odNodes doc
        propLines = concatMap getTextLines $ odProperties doc
    in mergeSorted docLines propLines

instance TextLineSource (OrgDocView a)  where
  getTextLines = getTextLines . ovDocument

-- * Constructors
generateDocView :: (NodeUpdate a) => OrgDoc -> OrgDocView a
generateDocView doc =
  let childNode (ChildNode n) = Just n
      childNode _ = Nothing
      childNodes n = mapMaybe childNode $ nChildren n
      scanNode :: (Node -> Maybe a) -> Node -> [(a, Node)]
      scanNode fn n = let hd = fn n
                          entry = maybe [] (\a -> [(a,n)]) hd
                          rest = (concatMap (scanNode fn) $ childNodes n)
                      in (entry++rest)
      scanOrgForest :: (NodeUpdate a) => [Node] -> [(a, Node)]
      scanOrgForest forest =
        concatMap (scanNode findItemInNode) forest

      forest = odNodes doc
      elements = scanOrgForest forest
  in OrgDocView elements doc

getRawElements :: OrgDocView a -> [a]
getRawElements docview =
  map fst $ ovElements docview

-- * Update an OrgMode doc

-- Algorithm: sort the issues by textline line #.  Then, get the
-- textlines of the entire tree, which shall be in ascending order.
-- Replace them as we match the node.

updateElementList :: (NodeUpdate a) => [Node] -> [(a, Node)]
updateElementList nodes =
  let nodeChildScan (ChildNode nd) = nodeScan nd
      nodeChildScan _ = []
      nodeScan nd =
        let children = concatMap nodeChildScan (nChildren nd)
        in case findItemInNode nd of
          Just item -> (item, nd):children
          Nothing -> children
  in concatMap nodeScan nodes

updateDoc :: (Ord a, NodeUpdate a) => Set a -> OrgDocView a -> OrgDocView a
updateDoc new_items doc =
  let nodeUpdater node =
        case findItemInNode node of
          Just item ->
            if member item new_items
            then let new_item = fromJust $ lookupLE item new_items
                 in updateNodeLine new_item node
            else Nothing
          Nothing -> Nothing
      new_nodes =
        map (updateNode nodeUpdater) $ odNodes $ ovDocument doc
      new_doc = (ovDocument doc) { odNodes = new_nodes }
  in doc { ovDocument = new_doc }