module Data.Ini.Config.Raw
( Ini(..)
, IniSection(..)
, IniValue(..)
, parseIni
) where

import           Control.Monad (void)
import           Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import           Data.Text (Text)
import qualified Data.Text as T
import           Text.Megaparsec
import           Text.Megaparsec.Text

-- | An 'Ini' value is a mapping from section names to
--   'IniSection' values.
newtype Ini
  = Ini { fromIni :: HashMap Text IniSection }
    deriving (Eq, Show)

-- | An 'IniSection' consists of a name, a mapping of key-value pairs,
--   and metadata about where the section starts and ends in the file.
data IniSection = IniSection
  { isName      :: Text
  , isVals      :: HashMap Text IniValue
  , isStartLine :: Int
  , isEndLine   :: Int
  } deriving (Eq, Show)

-- | An 'IniValue' represents a key-value mapping, and also stores the
--   line number where it appears.
data IniValue = IniValue
  { vLineNo :: Int
  , vName   :: Text
  , vValue  :: Text
  } deriving (Eq, Show)

-- | Parse a 'Text' value into an 'Ini' value.
parseIni :: Text -> Either String Ini
parseIni t = case runParser pIni "ini file" t of
  Left err -> Left (parseErrorPretty err)
  Right v  -> Right v

pIni :: Parser Ini
pIni = do
  sBlanks
  vs <- many (pSection <?> "section")
  void eof
  return $ Ini $ HM.fromList [ (T.toLower (isName v), v)
                             | v <- vs
                             ]

sBlanks :: Parser ()
sBlanks = skipMany (void eol <|> sComment)

sComment :: Parser ()
sComment = do
  void (oneOf ";#")
  void (manyTill anyChar eol)

pSection :: Parser IniSection
pSection = do
  start <- getCurrentLine
  void (char '[')
  name <- T.pack `fmap` some (noneOf "[]")
  void (char ']')
  sBlanks
  vals <- many (pPair <?> "key-value pair")
  end <- getCurrentLine
  sBlanks
  return IniSection
    { isName      = T.strip name
    , isVals      = HM.fromList [ (vName v, v) | v <- vals ]
    , isStartLine = start
    , isEndLine   = end
    }

pPair :: Parser IniValue
pPair = do
  pos <- getCurrentLine
  key <- T.pack `fmap` some (noneOf "[]=:")
  void (oneOf ":=")
  val <- T.pack `fmap` manyTill anyChar eol
  sBlanks
  return IniValue
    { vLineNo = pos
    , vName   = T.strip key
    , vValue  = T.strip val
    }

getCurrentLine :: Parser Int
getCurrentLine = (fromIntegral . unPos . sourceLine) `fmap` getPosition