module Database.Persist.Quasi
    ( parse
    ) where

import Database.Persist.Base
import Data.Char
import Data.Maybe (mapMaybe)

-- | Parses a quasi-quoted syntax into a list of entity definitions.
parse :: String -> [EntityDef]
parse = parse'
      . removeSpaces
      . filter (not . empty)
      . map tokenize
      . lines

-- | A token used by the parser.
data Token = Spaces !Int   -- ^ @Spaces n@ are @n@ consecutive spaces.
           | Token String  -- ^ @Token tok@ is token @tok@ already unquoted.

-- | Tokenize a string.
tokenize :: String -> [Token]
tokenize [] = []
tokenize ('-':'-':_) = [] -- Comment until the end of the line.
tokenize ('"':xs) = go xs ""
    where
      go ('\"':rest) acc = Token (reverse acc) : tokenize rest
      go ('\\':y:ys) acc = go ys (y:acc)
      go (y:ys)      acc = go ys (y:acc)
      go []          acc = error $ "Unterminated quoted (\") string starting with " ++
                                   show (reverse acc) ++ "."
tokenize (x:xs)
    | isSpace x = let (spaces, rest) = span isSpace xs
                  in Spaces (1 + length spaces) : tokenize rest
tokenize xs = let (token, rest) = break isSpace xs
              in Token token : tokenize rest

-- | A string of tokens is empty when it has only spaces.  There
-- can't be two consecutive 'Spaces', so this takes /O(1)/ time.
empty :: [Token] -> Bool
empty []         = True
empty [Spaces _] = True
empty _          = False

-- | A line.  We don't care about spaces in the middle of the
-- line.  Also, we don't care about the ammount of indentation.
data Line = Line { lineType :: LineType
                 , tokens   :: [String] }

-- | A line may be part of a header or body.
data LineType = Header | Body
                deriving (Eq)

-- | Remove leading spaces and remove spaces in the middle of the
-- tokens.
removeSpaces :: [[Token]] -> [Line]
removeSpaces xs = map (makeLine . subtractSpace) xs
    where
      -- | Ammount of leading spaces.
      s = minimum $ map headSpace xs

      -- | Ammount of leading space in a single token string.
      headSpace (Spaces n : _) = n
      headSpace _              = 0

      -- | Subtract the leading space.
      subtractSpace ys | s == 0 = ys
      subtractSpace (Spaces n : rest)
          | n == s    = rest
          | otherwise = Spaces (n - s) : rest
      subtractSpace _ = error "Database.Persist.Quasi: never here"

      -- | Get all tokens while ignoring spaces.
      getTokens (Token tok : rest) = tok : getTokens rest
      getTokens (Spaces _  : rest) =       getTokens rest
      getTokens []                 = []

      -- | Make a 'Line' from a @[Token]@.
      makeLine (Spaces _ : rest) = Line Body   (getTokens rest)
      makeLine rest              = Line Header (getTokens rest)

-- | Divide lines into blocks and make entity definitions.
parse' :: [Line] -> [EntityDef]
parse' (Line Header (name:entattribs) : rest) =
    let (x, y) = span ((== Body) . lineType) rest
    in mkEntityDef name entattribs (map tokens x) : parse' y
parse' ((Line Header []) : _) = error "Indented line must contain at least name."
parse' ((Line Body _)    : _) = error "Blocks must begin with non-indented lines."
parse' [] = []

-- | Construct an entity definition.
mkEntityDef :: String -> [String] -> [[String]] -> EntityDef
mkEntityDef name entattribs attribs =
    EntityDef name entattribs cols uniqs derives
  where
    cols = mapMaybe takeCols attribs
    uniqs = mapMaybe takeUniqs attribs
    derives = case mapMaybe takeDerives attribs of
                [] -> ["Show", "Read", "Eq"]
                x -> concat x

takeCols :: [String] -> Maybe ColumnDef
takeCols ("deriving":_) = Nothing
takeCols (n@(f:_):ty:rest)
    | isLower f = Just $ ColumnDef n ty rest
takeCols _ = Nothing

takeUniqs :: [String] -> Maybe UniqueDef
takeUniqs (n@(f:_):rest)
    | isUpper f = Just $ UniqueDef n rest
takeUniqs _ = Nothing

takeDerives :: [String] -> Maybe [String]
takeDerives ("deriving":rest) = Just rest
takeDerives _ = Nothing