-- | Reads .flow syntax.
module DataFlow.Reader (
  document,
  readDiagram,
  readDiagramFile
) where

import           Control.Applicative           ((*>), (<*), (<*>))
import           Control.Monad
import           Data.Char
import           Data.Functor                  ((<$>))
import           Data.List
import qualified Data.Map                      as M
import           Text.ParserCombinators.Parsec

import           DataFlow.Core

commentsAndSpace :: Parser ()
commentsAndSpace = do
  spaces
  skipMany comment
  spaces
  where
  comment = do
    _ <- string "/*"
    _ <- manyTill anyChar (try $ string "*/")
    return ()

identifier :: Parser ID
identifier = do
  first <- letter
  rest <-  many (letter <|> digit <|> char '_')
  return $ first : rest

str :: Parser Value
str = do
  -- TODO: Handle escaped characters.
  _ <- char '"'
  s <- many (noneOf "\"\r\n")
  _ <- char '"'
  return $ String s

textBlock :: Parser Value
textBlock = do
  _ <- char '`'
  s <- anyToken `manyTill` try (char '`')
  return $ String $ intercalate "\n" $ map (dropWhile isSpace) $ lines s

inside :: Parser x -> Parser y -> Parser t -> Parser t
inside before after p = do
  commentsAndSpace
  _ <- before
  commentsAndSpace
  c <- p
  commentsAndSpace
  _ <- after
  commentsAndSpace
  return c

inBraces :: Parser t -> Parser t
inBraces = inside (char '{') (char '}')

inSquareBrackets :: Parser t -> Parser t
inSquareBrackets = inside (char '[') (char ']')

array :: Parser Value
array =
  let sep = do _ <- char ','
               commentsAndSpace
  in Array <$>
    inSquareBrackets (value `sepBy` sep) <* commentsAndSpace

value :: Parser Value
value = try textBlock <|> try str <|> array

attr :: Parser (String, Value)
attr = do
  key <- identifier
  skipMany1 $ char ' '
  _ <- char '='
  skipMany1 $ char ' '
  v <- value
  commentsAndSpace
  return (key, v)

attrs :: Parser Attributes
attrs = liftM M.fromList $ many (try attr)

-- | Construct a parser for an node with an ID:
--
-- @
-- \<keyword\> \<id\> {
--   ...
-- }
-- @
idAndAttrsNode :: String -> (ID -> Attributes -> t) -> Parser t
idAndAttrsNode keyword f = do
  _ <- string keyword
  skipMany1 space
  id' <- identifier
  f id' <$> option M.empty (try (inBraces attrs))

function :: Parser Node
function = idAndAttrsNode "function" Function

database :: Parser Node
database = idAndAttrsNode "database" Database

io :: Parser Node
io = idAndAttrsNode "io" InputOutput

data FlowType = Back | Forward

arrow :: Parser FlowType
arrow = do
  s <- string "->" <|> string "--" <|> string "<-"
  case s of
    "->" -> return Forward
    "<-" -> return Back
    _    -> fail "Invalid flow statement"

flow :: Parser Flow
flow = do
  i1 <- identifier
  skipMany1 space
  arr <- arrow
  skipMany1 space
  i2 <- identifier
  a <- option M.empty $ try (inBraces attrs)
  case arr of
    Back    -> return $ Flow i2 i1 a
    Forward -> return $ Flow i1 i2 a

node :: Parser Node
node = do
  n <- try function
       <|> try database
       <|> io
  commentsAndSpace
  return n

boundary :: Parser RootNode
boundary = do
  _ <- string "boundary"
  skipMany1 space
  id' <- identifier
  inBraces (TrustBoundary id' <$> attrs <*> many node)

rootNode :: Parser RootNode
rootNode = try (Node <$> node)
           <|> boundary

diagram :: Parser Diagram
diagram =
  string "diagram" *> inBraces (Diagram <$> attrs <*> many (try rootNode) <*> many flow)

document :: Parser Diagram
document = commentsAndSpace *> diagram <* commentsAndSpace

-- Read the string, as named by the second argument, as a 'Diagram'.
readDiagram :: String -> String -> Either ParseError Diagram
readDiagram = parse document

-- Read the file at the given file path as a 'Diagram'.
readDiagramFile :: FilePath -> IO (Either ParseError Diagram)
readDiagramFile = parseFromFile document
