    {-# LANGUAGE FlexibleContexts #-}
    -- |
    -- Module    : Data.Ini.Reader.Internals
    -- Copyright : 2011-2014 Magnus Therning
    -- License   : BSD3
    --
    -- Internal functions used in 'Data.Ini.Reader'.
    module Data.Ini.Reader.Internals where

    import Control.Monad.Except
    import Control.Monad.State
    import qualified Data.ByteString as BS
    import Text.Parsec as P
    import Text.Parsec.String

    import Data.Ini
    import Data.Ini.Types

    data IniReaderError
        = IniParserError String
        | IniSyntaxError String
        | IniOtherError String
        deriving (Eq, Show)

    type IniParseResult = Either IniReaderError

    -- | The type used to represent a line of a config file.
    data IniFile
        = SectionL String
        | OptionL String String
        | OptionContL String
        | CommentL
        deriving (Show, Eq)

    -- | Build a configuration from a list of 'IniFile' items.
    buildConfig :: [IniFile] -> IniParseResult Config
    buildConfig ifs = let
            isComment CommentL = True
            isComment _ = False

            fIfs = filter (not . isComment) ifs

            -- merge together OptionL and subsequent OptionContL items
            mergeOptions [] = return []
            mergeOptions (s@(SectionL _) : ifs) = (s :) `liftM` mergeOptions ifs
            mergeOptions (CommentL : ifs ) = (CommentL :) `liftM` mergeOptions ifs
            mergeOptions (OptionL on ov : OptionContL ov2 : ifs) = mergeOptions $ (OptionL on (ov ++ ov2)) : ifs
            mergeOptions (o@(OptionL on ov) : ifs) = (o :) `liftM` mergeOptions ifs
            mergeOptions _ = throwError $ IniSyntaxError "Syntax error in INI file."

            -- build the configuration from a [IniFile]
            buildit a [] = return a
            buildit a (SectionL sn : is) = put sn >> buildit a is
            buildit a (OptionL on ov : is) = do
                sn <- get
                let na = setOption sn on ov a
                buildit na is

        in mergeOptions fIfs >>= (\ is -> return . fst $ runState (buildit emptyConfig is) "default")

    -- | Consumer of whitespace \"@ \t@\".
    eatWhiteSpace :: Parser String
    eatWhiteSpace = many $ oneOf " \t"

    -- | Parser for the start-of-section line.  It expects the line to start with a
    -- @[@ then find the section name, and finally a @]@.  The section name may be
    -- surrounded by any number of white space characters (see 'eatWhiteSpace').
    secParser :: Parser IniFile
    secParser = let
            validSecNameChrs = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "._-/@\" "
        in do
            char '['
            eatWhiteSpace
            sn <- many1 $ oneOf validSecNameChrs
            eatWhiteSpace
            char ']'
            manyTill anyChar newline
            return $ SectionL sn

    -- | Parser for a single line of an option.  The line must start with an option
    -- name, then a @=@ must be found, and finally the rest of the line is taken as
    -- the option value.  The equal sign may be surrounded by any number of white
    -- space characters (see 'eatWhiteSpace').
    optLineParser :: Parser IniFile
    optLineParser = let
            validOptNameChrs = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "_-/@"
        in do
            on <- many1 $ oneOf validOptNameChrs
            eatWhiteSpace
            char '='
            eatWhiteSpace
            ov <- manyTill anyChar newline
            return $ OptionL on ov

    -- | Parser for an option-value continuation line.  The line must start with
    -- either a space or a tab character (\"@ \t@\").  Everything else on the line,
    -- until the newline character, is taken as the continuation of an option
    -- value.
    optContParser :: Parser IniFile
    optContParser = do
        oneOf " \t"
        eatWhiteSpace
        oc <- noneOf " \t"
        ov <- manyTill anyChar newline
        return $ OptionContL $ oc:ov

    -- | Parser for "noise" in the configuration file, such as comments and empty
    -- lines.  (Note that lines containing only space characters will be
    -- successfully parsed by 'optContParser'.)
    noiseParser :: Parser IniFile
    noiseParser = let
            commentP = do
                oneOf "#;"
                manyTill anyChar newline
            emptyL = newline >> return ""
        in choice [commentP, emptyL] >> return CommentL

    iniParser :: Parser [IniFile]
    iniParser =
        many $ choice [secParser, optLineParser, optContParser, noiseParser]