-- | Parser for DDC build spec files.
module DDC.Build.Spec.Parser
        ( parseBuildSpec
        , Error(..) )
where
import DDC.Build.Spec.Base
import DDC.Base.Pretty
import Control.Monad
import Data.List
import Data.Char
import Data.Maybe
import qualified DDC.Core.Module        as C


---------------------------------------------------------------------------------------------------
-- | Problems that can arise when parsing a build spec file.
data Error 
        -- | Empty Spec file.
        = ErrorEmpty
        { errorFilePath :: FilePath }

        -- | Parse error in Spec file.
        | ErrorParse
        { errorFilePath :: FilePath 
        , errorLine     :: Int }

        -- | Required field is missing.
        | ErrorMissingField
        { errorFilePath :: FilePath
        , errorMissing  :: String }
        deriving Show


instance Pretty Error where
 ppr err
  = case err of
        ErrorEmpty filePath
         -> vcat [ text filePath
                 , text "Empty file" ]

        ErrorParse filePath n
         -> vcat [ text filePath <> text ":" <> int n
                 , text "Parse error" ]

        ErrorMissingField filePath field
         -> vcat [ text filePath
                 , text "Missing field '" <> text field <> text "'" ]


---------------------------------------------------------------------------------------------------
type LineNumber = Int
type StartCol   = Int

type Parser a   = [(LineNumber, StartCol, String)]  
                -> Either Error a


-- | Parse a build specification.
parseBuildSpec :: FilePath -> String -> Either Error Spec
parseBuildSpec path str
 = let  -- Attach line numbers and starting column to each line.
        ls              = lines str
        lsNum           = zip [1..]  ls
        lsNumCols       = attachCols lsNum
   in   pBuildSpec path lsNumCols


-- | Parse a build specification.
pBuildSpec :: FilePath -> Parser Spec
pBuildSpec path []
        = Left $ ErrorEmpty path

pBuildSpec path ((n, _s, str) : rest)
        -- Skip over blank lines
        | all (\c -> isSpace c || c == '\n') str
        = pBuildSpec path rest

        -- The build spec needs to start with the magic words and version number.
        | ["ddc", "build", version] <- words str
        = do    cs    <- pComponents path rest
                return  $ Spec
                        { specVersion           = version
                        , specComponents        = cs }

        | otherwise
        = Left $ ErrorParse path n


-- | Parse a build component specification.
pComponents :: FilePath -> Parser [Component]
pComponents _path []
        = return []

pComponents path ((n, start, str) : rest)
        -- skip over blank lines
        | all (\c -> isSpace c || c == '\n') str
        = pComponents path rest

        -- parse a library specification
        | str == "library"
        , (lsLibrary, lsMore) 
                <- span (\(_, start', _) -> start' == 0 || start' > start) rest
        = do    fs      <- pLibraryFields path lsLibrary
                more    <- pComponents    path lsMore
                return  $ fs : more

        -- parse an executable specification
        | str == "executable"
        , (lsExecutable, lsMore)
                <- span (\(_, start', _) -> start' == 0 || start' > start) rest
        = do    fs      <- pExecutableFields path lsExecutable
                more    <- pComponents       path lsMore
                return  $ fs : more

        | otherwise
        = Left $ ErrorParse path n


---------------------------------------------------------------------------------------------------
-- | Parse the fields of a library specification.
pLibraryFields :: FilePath -> Parser Component
pLibraryFields path str
 = do   fs                          <- pFields path str
        (sName,         fs_name)    <- takeField path "name"             fs
        (sVersion,      fs_version) <- takeField path "version"          fs_name
        (sTetraModules, fs_modules) <- takeField path "tetra-modules"    fs_version

        let Just msTetra
                = sequence
                $ map C.readModuleName
                $ words $ sTetraModules

        return  $ SpecLibrary 
                { specLibraryName          = sName
                , specLibraryVersion       = sVersion
                , specLibraryTetraModules  = msTetra
                , specLibraryMeta          = fs_modules }


---------------------------------------------------------------------------------------------------
-- | Parse the fields of an executable specification.
pExecutableFields :: FilePath -> Parser Component
pExecutableFields path str
 = do   fs                          <- pFields path str
        (sName,           fs_name)  <- takeField      path "name"        fs
        (sTetraMain,      fs_main)  <- takeField      path "tetra-main"  fs_name
        let (sTetraOther, fs_other) =  takeFieldMaybe path "tetra-other" fs_main

        let Just mTetraMain
                = C.readModuleName sTetraMain

        let Just msTetra
                = sequence $ map C.readModuleName
                $ concat   $ maybeToList $ liftM words sTetraOther 

        return  $ SpecExecutable
                { specExecutableName       = sName
                , specExecutableTetraMain  = mTetraMain
                , specExecutableTetraOther = msTetra
                , specExecutableMeta       = fs_other }


---------------------------------------------------------------------------------------------------
-- | Parse fields of a build specification.
pFields  :: FilePath -> Parser [(String, String)]
pFields _path []
        = return []

pFields path ((n, start, str) : rest)
        -- skip over blank lines
        | all (\c -> isSpace c || c == '\n') str
        = pFields path rest
        
        -- parse a single field.
        | (lsField, lsMore)      
                <- span (\(_, start', _) -> start' == 0 || start' > start) rest

        , (fieldName, ':' : fieldValue) 
                <- span (\c -> c /= ':') 
                $  str ++ concat [ s | (_, _, s) <- lsField]

        = do    let f   =  (chomp fieldName, chomp fieldValue)
                more    <- pFields path lsMore
                return  $ f : more

        | otherwise
        = Left $ ErrorParse path n


-- | Take a named field from this list of fields.
takeField :: FilePath
          -> String -> [(String, String)] 
          -> Either Error (String, [(String, String)])

takeField path name fs
 = case lookup name fs of
        Nothing -> Left $ ErrorMissingField path name
        Just s  -> return (s, delete (name, s) fs)


-- | Take a named field from this list of fields.
takeFieldMaybe 
        :: FilePath
        -> String -> [(String, String)] 
        -> (Maybe String, [(String, String)])

takeFieldMaybe _path name fs
 = case lookup name fs of
        Nothing -> (Nothing, fs)
        Just s  -> (Just s,  delete (name, s) fs)


---------------------------------------------------------------------------------------------------
-- | Attach starting column number to these lines.
attachCols 
        :: [(LineNumber, String)] 
        -> [(LineNumber, StartCol, String)]

attachCols lstrs
 = [ (ln, startCol 1 str, str) | (ln, str) <- lstrs ]
 where  startCol n ss  
         = case ss of
                []              -> 0
                ' '  : ss'      -> startCol (n + 1) ss'
                '\t' : ss'      -> startCol (n + 8) ss'
                _    : _        -> n


-- | Remove whitespace from the beginning and end of a string.
chomp :: String -> String
chomp str
        = reverse $ dropWhile isSpace $ reverse $ dropWhile isSpace str