{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}
module Data.NDBL.Parse (Document, Group, Pair, pNDBL) where
import Data.Char (isPrint, isSpace)
type Document = [Group]
type Group    = [Pair]
type Pair     = (String,String)
type Result a = Either String (String, a)
type Parse a = String -> Result a
throw = Left
over :: (a -> b) -> Result a -> Result b
over _ (Left err)     = Left err
over f (Right (s, x)) = Right (s, f x)
bind :: Result a -> ((String, a) -> Result b) -> Result b
bind (Left err) _ = Left err
bind (Right a)  f = f a
isSep :: Char -> Bool
isSep c = c `elem` " \t\r\n="
pQString :: Parse String
pQString = go
  where go ('\\':x:xs)
          | x == '\\' ||
            x == '"'  = (x:) `over` go xs
          | otherwise = throw $ "Unrecognized escape: \"\\" ++ [x] ++ "\""
        go ('"':xs) = return (xs, "")
        go (x:xs)
          | isPrint x || isSpace x = (x:) `over` go xs
          | otherwise = throw $ "Non-printable character: " ++ show x
        go [] = throw $ "End of document while still inside string"
pWord :: Parse String
pWord s@(x:xs)
  | isPrint x && not (isSep x) = (x:) `over` pWord xs
  | isSep x = return (s, "")
  | not (isPrint x) = throw $ "Non-printable character: " ++ show x
pWord s = return (s, "")
pWord1 :: Parse String
pWord1 (x:xs)
  | isPrint x && not (isSep x) = (x:) `over` pWord xs
  | isSep x = throw $ "`=` without previous key"
  | not (isPrint x) = throw $ "Non-printable character: " ++ show x
pWord1 s = throw $ "Expected word; found " ++ show s
pPair :: Parse (String, String)
pPair s = bind (pWord1 s) $ \case
  ('=':'"':s', r) -> (r,) `over` pQString s'
  ('=':s',     r) -> (r,) `over` pWord s'
  _               -> throw "Expected '=' after pair name"
isHSpace :: Char -> Bool
isHSpace c = c == ' ' || c == '\t'
isVSpace :: Char -> Bool
isVSpace c = c == '\n' || c == '\r'
pComment :: Parse Bool
pComment s@(x:xs)
  | isVSpace x = pSkip s
  | otherwise  = pComment xs
pSkip :: Parse Bool
pSkip ""     = return ("", False)
pSkip (y:"") = return ("", False)
pSkip ('#':xs) = pComment xs
pSkip (y:s@(x:xs))
  | isVSpace y && isHSpace x = pSkip xs
  | isVSpace y               = return (s, False)
pSkip s@(x:xs)
  | isHSpace x = pSkip xs
  | otherwise  = return (s, True)
pGroup :: Parse [(String, String)]
pGroup s = bind (pPair s) $ \case
  (s', p) -> bind (pSkip s') $ \case
    (s'', True)  -> (p:) `over` pGroup s''
    (s'', False) -> return (s'', [p])
pDocument :: Parse Document
pDocument s = bind (pGroup s) $ \case
  ("", g) -> return ("", [g])
  (xs, g) -> (g:) `over` pDocument xs
pNDBL :: String -> Either String Document
pNDBL s = case pDocument s of
  Right (_, d) -> Right d
  Left err     -> Left err