module Configuration.Dotenv.Parse (configParser) where

import Data.Maybe (catMaybes)

import Text.Parsec
  ((<|>), many, try, lookAhead, manyTill, char, anyChar, many1)

import Text.Parsec.String (Parser)

import Text.ParserCombinators.Parsec.Prim (GenParser)

import Text.ParserCombinators.Parsec.Char (space, newline, oneOf, noneOf)

import Control.Monad (liftM2)

import Text.Parsec.Combinator (eof)

import Control.Applicative ((<*), (*>), (<$>))

-- | Returns a parser for a Dotenv configuration file.
-- Accepts key and value arguments separated by "=".
-- Comments are allowed on lines by themselves and on
-- blank lines.
configParser :: Parser [(String, String)]
configParser = catMaybes <$> many lineWithArguments

lineWithArguments :: Parser (Maybe (String, String))
lineWithArguments =
  comment *> return Nothing
  <|> newline *> return Nothing
  <|> many1 (oneOf "\t ") *> return Nothing
  <|> Just <$> configurationOptionWithArguments

configurationOptionWithArguments :: Parser (String, String)
configurationOptionWithArguments = do
  _ <- many space

  keyword   <- manyTill1 (noneOf "\n ") keywordArgSeparator

  arguments <- argumentParser

  return (keyword, arguments)

argumentParser :: Parser String
argumentParser = try quotedArgument <|> try unquotedArgument

-- | Based on a commented-string parser in:
-- http://hub.darcs.net/navilan/XMonadTasks/raw/Data/Config/Lexer.hs
quotedWith :: Char -> Parser String
quotedWith c =
  char c
  *> many chr
  <* char c
  where chr = esc <|> noneOf [c]
        esc = escape *> char c

quotedArgument :: Parser String
quotedArgument = quotedWith '\'' <|> quotedWith '\"'

unquotedArgument :: Parser String
unquotedArgument =
  many (noneOf " \t\n#") <* (try comment <|> try verticalSpace *> return () <|>
                             lookAhead (try endOfLineOrInput))

comment :: Parser ()
comment =
  try (many verticalSpace *> char '#')
  *> manyTill anyChar endOfLineOrInput
  *> return ()

endOfLineOrInput :: Parser ()
endOfLineOrInput = newline *> return () <|> eof

manyTill1 :: GenParser tok st a -> GenParser tok st end -> GenParser tok st [a]
manyTill1 p end = liftM2 (:) p (manyTill p end)

keywordArgSeparator :: Parser ()
keywordArgSeparator =
  many verticalSpace *> char '=' *> many verticalSpace *> return ()

escape :: Parser Char
escape = char '\\'

verticalSpace :: Parser Char
verticalSpace = oneOf " \t"