{-# LANGUAGE OverloadedStrings #-}
-- | Incomplete parser for SQL CREATE TABLE statements.
--   Needed to figure out whether any given column is auto-incrementing
--   or not. It's super inefficient, but doesn't really matter since it'll
--   only ever be invoked during validation.
module Database.Selda.SQLite.Parser (colsFromQuery) where
import Control.Applicative
import Control.Monad (void, msum, MonadPlus (..))
import Data.Char (isSpace, isAlpha, isAlphaNum)
import Data.Maybe (isJust, catMaybes)
import Data.Text (Text)
import qualified Data.Text as Text

colsFromQuery :: Text -> [(Text, (Text, Bool))]
colsFromQuery = parse' parseCreateQueryCols

newtype Parser a = P { unP :: (Text -> Maybe (Text, a)) }

instance Functor Parser where
  fmap f (P g) = P (fmap (fmap f) . g)

instance Applicative Parser where
  pure x = P $ \t -> Just (t, x)
  f <*> x = f >>= \f' -> fmap f' x

instance Alternative Parser where
  empty = P $ const Nothing
  P f <|> P g = P $ \s ->
    case f s of
      res@(Just _) -> res
      _            -> g s

instance Monad Parser where
  return = pure
  P m >>= f = P $ \s -> do
    case m s of
      Just (rest, x) -> unP (f x) rest
      _              -> Nothing

instance MonadPlus Parser where
  mzero = empty
  mplus = (<|>)

parse :: Parser a -> Text -> Maybe a
parse (P f) t = snd <$> f t

parse' :: Parser a -> Text -> a
parse' f t = maybe (error $ "no parse: '" ++ show t ++ "'") id $ parse f t

lowerText :: Text -> Parser ()
lowerText prefix = P $ \s ->
  case Text.splitAt (Text.length prefix) s of
    (prefix', rest) | prefix == Text.toLower prefix' -> Just (rest, ())
    _                                                -> Nothing

charP :: (Char -> Bool) -> Parser Char
charP p = P $ \s ->
  case Text.splitAt 1 s of
    (prefix, rest) | Text.any p prefix -> Just (rest, Text.head prefix)
    _                                  -> Nothing

char :: Char -> Parser Char
char c = charP (== c)

space :: Parser ()
space = void $ charP isSpace

spaces :: Parser ()
spaces = void $ some space

sepBy1 :: Parser s -> Parser a -> Parser [a]
sepBy1 sep p = do
  x <- p
  xs <- optional $ sep *> sepBy1 sep p
  case xs of
    Just xs' -> pure (x:xs')
    _        -> pure [x]

commaSeparated :: Parser a -> Parser [a]
commaSeparated = sepBy1 (many space >> char ',' >> many space)

keywords :: [Text]
keywords = ["constraint", "unique", "primary key"]

parseCreateQueryCols :: Parser [(Text, (Text, Bool))]
parseCreateQueryCols = do
  lowerText "create table"
  spaces
  void $ sqliteIdentifier
  void $ many space
  void $ char '('
  cols <- commaSeparated parseCol <* many space
  void $ char ')'
  pure $ catMaybes cols

parseCol :: Parser (Maybe (Text, (Text, Bool)))
parseCol = do
    decl <- constraint <|> column
    pure $ case decl of
      Right col -> Just col
      _         -> Nothing
  where
    column = do
      name <- sqliteIdentifier
      spaces
      ty <- sqliteIdentifier
      void $ optional $ spaces *> lowerText "primary key"
      isAuto <- optional $ spaces *> lowerText "autoincrement"
      void $ many $ charP (\c -> isAlphaNum c || isSpace c)
      void $ optional $ do
        void $ char '('
        void $ commaSeparated sqliteIdentifier
        void $ char ')'
      pure $ Right $ (name, (ty, isJust isAuto))
    constraint = do
      msum (map lowerText keywords)
      void $ many $ msum
        [ void sqliteIdentifier
        , void $ do
            void $ char '('
            void $ commaSeparated sqliteIdentifier
            void $ char ')'
        , spaces
        ]
      pure $ Left ()

sqliteIdentifier :: Parser Text
sqliteIdentifier = Text.pack <$> (quoted <|> unquoted)
  where
    unquoted = do
      x <- charP $ \c -> isAlpha c || c == '_'
      xs <- many $ charP $ \c -> isAlphaNum c || c == '_' || c == '$'
      pure $ (x:xs)
    quoted = char '"' *> many quotedChar <* char '"'
    quotedChar = (char '"' >> char '"') <|> charP (/= '"')