module Data.Quantities.DefinitionParser where

import Control.Applicative ((<$>), (<*))
import System.Environment
import Text.ParserCombinators.Parsec

import Data.Quantities.Data (Definition (..), Symbol, units, magnitude)
import Data.Quantities.ExprParser (parseExpr)


main :: IO ()
main = do
  defs <- readDefinitions <$> head <$> getArgs
  print defs

-- | Parse multiline string of definitions (say, from a file) into a
-- list of definitions.
readDefinitions :: String -> [Definition]
readDefinitions input = case parse readDefinitions' "Input File Parser" input of
  Left err -> error (show err) >> []
  Right val -> val

readDefinitions' :: Parser [Definition]
readDefinitions' = many parseDef <* eof

-- | Parse any definition line
parseDef :: Parser Definition
parseDef  = do
  _ <- spaces
  -- skipMany comment
  optional $ many $ char '\n'
  line <- try parseDefLine <|> try parseBaseLine <|> parsePrefixLine
  spaces
  -- skipMany comment
  optional $ many $ char '\n'
  return line

-- comment :: Parser String
-- comment = char '#' >> many (noneOf "\n")

eol :: Parser Char
eol = char '\n'

-- | Parse a line containing unit definition
-- Ex: minute = 60 s = min
parseDefLine :: Parser Definition
parseDefLine = do
  (UnitDefinition s e []) <- parseUnitDef
  syns <- many (try parseSynonym)
  return $ UnitDefinition s e syns

parseUnitDef :: Parser Definition
parseUnitDef = do
  sym   <- parseSymbol <* spaces <* char '='
  quant <- parseExpr
  spaces
  -- skipMany comment
  return $ UnitDefinition sym quant []

parseSynonym :: Parser Symbol
parseSynonym = spaces >> char '=' >> spaces >> parseSymbol <* spaces


-- | Parse line containing base definition
-- Ex: meter = [length] = m
parseBaseLine :: Parser Definition
parseBaseLine = do
  (sym, f) <- parseBase
  syns <- many (try parseSynonym)
  return $ BaseDefinition sym f syns

parseBase :: Parser (Symbol, Symbol)
parseBase = do
  sym <- parseSymbol <* spaces <* char '='
  b <- spaces >> char '[' >> option "" parseSymbol <* char ']'
  spaces
  -- skipMany comment
  return (sym, b)

-- | Parse line containing prefix definition
-- Ex: milli- = 1e-3 = m-
parsePrefixLine :: Parser Definition
parsePrefixLine = do
  (p, f) <- parsePrefix
  syns <- many (try parsePrefixSynonym)
  return $ PrefixDefinition p f syns

parsePrefix :: Parser (Symbol, Double)
parsePrefix = do
  pre <- many1 letter <* char '-' <* spaces <* char '='
  facQuant <- spaces >> parseExpr
  spaces
  if null (units facQuant) then
    return (pre, magnitude facQuant)
    else fail "No units allowed in prefix definitions"


parsePrefixSynonym :: Parser Symbol
parsePrefixSynonym = spaces >> char '=' >> spaces >> parseSymbol <* char '-' <* spaces


-- | Parse a symbol for a unit
parseSymbol :: Parser Symbol
parseSymbol = do
  letter' <- letter
  rest    <- many (alphaNum <|> char '_')
  return $ letter' : rest