{-|
Converts flow diagrams to the Graphviz (Dot) files for subsequent rendering
into nice pictures.
-}
module Text.FlowDiagram ( flow2dot
                        , parseFlow
                        , parseFlowFromFile
                        , showFlow
                        , Flow(..)
                        ) where

import qualified Text.Dot as D
import Control.Monad.State (StateT, evalStateT, gets, modify, lift, zipWithM, zipWithM_, foldM, when)
import qualified Data.Map as M (Map, empty, lookup, insert, union, fromList)
import Data.List (intercalate, unfoldr, splitAt, findIndex, nub, sort)
import System.IO (readFile)
import Data.Maybe (catMaybes, isJust, fromMaybe)
import Data.Char (isSpace)
import Text.ParserCombinators.Parsec hiding (State)

{-
Idea: In order to draw sequence (flow) diagram using graphviz we can use directed layout (dot) to
generate "skeleton" of the diagram and draw message lines and action boxes
-}


type Swimline = String

-- | Flow could include messages and actions, one item per source line
data Flow = Msg Swimline Swimline String
          -- ^ Message (from, to, message text). Syntax in the source file: @from -> to: message text@
          | Action Swimline String
          -- ^ Action (actor, message text). Syntax in the source file: @actor: message text@
          | Order [Swimline]
          -- ^ Tries to put swimlines in the specified order. Syntax: @order swimline1 swimline2 ...@
            deriving (Eq,Show)

isOrder (Order _) = True
isOrder _         = False

isMsg (Msg _ _ _) = True
isMsg _           = False

names flow = nub $ flip concatMap flow $ \elt ->
  case elt of
    Msg from to _ -> [from,to]
    Action a _ -> [a]
    Order _ -> []


-- | State of the diagram builder
data DiagS = DiagS { nodes :: M.Map (Int,Swimline) D.NodeId
                   -- ^ all the nodes of the graph, indexed by tier and swimline name
                   }

type Diagram = StateT DiagS D.Dot

-- | 'flow2dot' take a list of flow diagram items (`Flow') and converts them to Graphviz code
flow2dot :: [Flow] -> String
flow2dot flow =
  ("strict "++) $ D.showDot $ evalStateT (flow2dot' flow) (DiagS M.empty)
    -- NB: "strict" is VERY important here
    -- Without it, "dot" segfaults while rendering diagram (dot 2.12)

flow2dot' :: [Flow] -> Diagram ()
flow2dot' flow = do
  -- Avoid curved edges at all cost
  attribute ("splines","line")

  let order = case [ ns | Order ns <- flow ] of
                []     -> Nothing
                -- Only the first Order directive would be taken into account
                (ns:_) -> Just ns
      flow' = filter (not.isOrder) flow
      swimlines = names flow'

  -- check order
  case order of
    Just order' | sort order' /= sort swimlines -> do
      error "order statement must mention all swimlines"
    _ -> return ()

  -- Create nodes for flow elements
  flip mapM_ (zip [0..] flow') $ \(tier,elt) -> do
    namedNodes <- flowElement2dot tier order elt
    addNodes $ M.fromList $ [ ((tier,swimline),node) | (swimline,node)<-namedNodes ]

  -- Add nodes on all the remaining places on all the swimlines
  -- We don't add nodes on the same tier as Msg elements to avoid dot trying to route
  -- Msg edge around those invisible support nodes.
  flip mapM_ [ (tier, sline) | sline <- swimlines
                             , (tier,elt) <-zip [0..] flow'
                             , not (isMsg elt) ] $ \(tier,sline) -> do
    n <- findNode tier sline
    case n of
      Just _ -> return ()
      Nothing -> do
          id <- invisNode
          addNodes (M.fromList [((tier,sline),id)])

  -- create swimline headers
  headers <- flip mapM (fromMaybe swimlines order) $ \sline ->
    node [ ("label", mkHeader sline)
         , ("shape","box")
         ]
  same (headers)

  -- apply order, if any
  when (isJust order) $ do
    zipWithM_ (\h1 h2 -> edge h1 h2 [ ("style","invis")
                                    , ("weight","1") -- this edge is weak compared to verticals
                                    ])
      headers (drop 1 headers)

  -- connect all nodes on all swimlines
  -- first, lets group them by tier
  nodesByTier <- flip mapM [ tier | (tier,_) <-zip [0..] flow' ] $ \tier ->
    mapM (findNode tier) [ line | line <- swimlines ]

  -- then, we descend through the tiers connectin nodes to the previous node on
  -- the same swimline, starting from headers
  _ <- foldM (\prevNodes thisTier -> do
          same $ catMaybes thisTier
          zipWithM (\prevNode maybeThisNode ->
                      case maybeThisNode of
                        Just thisNode -> do
                          edge prevNode thisNode
                            [("style","dotted")
                            ,("arrowhead","none")
                            ,("weight","1000") -- these edges want to be
                                               -- very vertical
                            ]
                          return thisNode
                        Nothing -> return prevNode)
            prevNodes thisTier)
         headers nodesByTier
  return ()

flowElement2dot :: Int -> Maybe [String] -> Flow -> Diagram [(String,D.NodeId)]
-- Generate nodes for message/action on all required swimlines
flowElement2dot tier _ (Action actor message) = do
  l <- mkLabel tier message
  a <- node [("style","filled"),("shape","plaintext"),("label",l)]
  return [(actor,a)]

flowElement2dot tier order (Msg from to message) = do
  f    <- invisNode
  t    <- invisNode

  l <- mkLabel tier message

  let (f',t',attrs) =
        if order == Nothing
           then (f,t,[])
           else let (Just sls) = order
                    in case (findIndex (==from) sls, findIndex (==to) sls) of
                         (Just x, Just y) -> if x>y then (t,f,[("dir","back")]) else (f,t,[])
                         _                -> (f,t,[])

  edge f' t' $ [("label",l)
               ,("constraint","false") -- avoid pushing recipient node down
               ,("labelfloat","true") -- be a bit sloppy with label placement
               ] ++ attrs
  return [(from,f),(to,t)]

-- Order setting is done in Msg processing
flowElement2dot _ _ (Order _) = return []

mkLabel :: Int -> String -> Diagram String
mkLabel tier lbl = do
  return $ if null lbl then ""
                       else (show (tier+1) ++ ": " ++ reflow lbl)

invisNode :: Diagram D.NodeId
invisNode = node [("style","invis"),("shape","point")]

reflow :: String -> String
reflow str = intercalate "\n" $ map unwords $ splitInto words_in_row w
      where w = words str
            z = length w
            rows = z*height `div` (height+width)
            words_in_row = rows*width `div` height
            chunk _ []  = Nothing
            chunk 0 lst = Just (lst, [])
            chunk n lst = Just $ splitAt n lst
            splitInto n = unfoldr (chunk n)
            -- FIXME: for now, you have to hardcode desired width/height ratio
            width=3
            height=1

mkHeader :: String -> String
mkHeader = map remove_underscore
  where
    remove_underscore '_' = ' '
    remove_underscore x   = x

------------------------------
-- State access/modify helpers
------------------------------

addNodes :: M.Map (Int,String) D.NodeId -> Diagram ()
addNodes newNodes = do
  modify (\e -> e {nodes = M.union (nodes e) newNodes})

findNode :: Int -> String -> Diagram (Maybe D.NodeId)
findNode tier line = do
  ns <- gets nodes
  return $ M.lookup (tier, line) ns

------------------------------------------------
-- Lifting Text.Dot functions to the State monad
------------------------------------------------
same = lift . D.same
node = lift . D.node
edge f t args = lift $ D.edge f t args
attribute = lift . D.attribute

---------
-- Parser
---------
-- | Parse specified file and return Flow Diagram contained therein.
-- All syntax errors are thrown via 'error'
parseFlowFromFile :: FilePath -> IO [Flow]
parseFlowFromFile fname = do
  raw <- readFile fname
  return $ parseFlow fname raw

-- | Parse given string and return Flow Diagram contained therein.
-- All syntax errors are thrown via 'error'
parseFlow :: String -> String -> [Flow]
parseFlow _     ""  = []
parseFlow fname str =
  case parse document fname str of
       Left err   -> error $ unlines [ "Input:", str, "Error:", show err]
       Right flow -> flow

document :: GenParser Char st [Flow]
document = do
  whitespace
  fl <- many flowLine
  eof
  return $ catMaybes fl

flowLine, parseMsg, parseAction :: GenParser Char st (Maybe Flow)
flowLine = try parseOrder <|> try parseMsg <|> try parseAction <|> parseComment <|> parseBlank
parseOrder = do string "order"
                is <- identifier `manyTill` newline
                return $ Just $ Order is
parseMsg = do f <- identifier; string "->"; t <- identifier; m <- optionalMessage
              return $ Just $ Msg f t (trim m)
parseAction = do s <- identifier; string ":"; a <- anything
                 return $ Just $ Action s (trim a)
parseBlank = do whitespace; newline; return Nothing
parseComment = do whitespace; string "#"; anything; return Nothing

optionalMessage = do
  m <- option "" (try (do {string ":"; anything}))
  optional newline
  return m

identifier, whitespace, anything :: GenParser Char st String
identifier = do whitespace; i <- many1 (alphaNum <|> oneOf "_"); whitespace
                return i
whitespace = many $ oneOf " \t"
anything = try (anyChar `manyTill` newline) <|> many1 anyChar

trim :: String -> String
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace

-- | Print element of the flow diagram as String
showFlow :: Flow -> String
showFlow (Order sl)   = "order " ++ intercalate " " sl
showFlow (Msg f t m)  = unwords [ f, " -> ", t, ":", m ]
showFlow (Action s a) = unwords [ s, ":", a ]