module Model.ChemnoDict
    ( getDict
    , getDictAuto
    ) where

import Data.List ( foldl' )
import qualified Model.Types as T

---------------------------------------------------------------------
-- Types
---------------------------------------------------------------------

-- |Describes the result of parsing a line in the key file.
data ParsedLine =
      KeyLine Int String String     -- Line number key value
    | ErrorLine Int String          -- Line number error message
    | EmptyLine Int                 -- Line number
    deriving ( Show )

---------------------------------------------------------------------
-- Interface functions
---------------------------------------------------------------------

getDict :: String -> Either String T.ChemKeys
-- ^Read a file input string to parse the chemno key-value pairs. If
-- there are no errors, a T.ChemnoKeys associative list is returned
-- in the Right value. If there are errors, a formatted error message
-- string is returned in the Left value.
getDict str = fmtDict . foldl' chkMultDef [] $ parsed
    where parsed = map parseLine $ zip [1..] ( lines str )

getDictAuto :: String -> Either String T.ChemKeys
-- ^Similar to getDict but automatically numbers the keys in the
-- order they appear in the key file. Ignores any text on the line
-- after the first key string.
getDictAuto str = fmtDictAuto . foldl' chkMultDef [] $ parsed
    where parsed = map parseLineAuto $ zip [1..] ( lines str )

---------------------------------------------------------------------
-- Functions
---------------------------------------------------------------------

---------------------------------------
-- Formatting functions.

fmtDict :: [ParsedLine] -> Either String T.ChemKeys
-- ^Formats a list of parsed lines for user-specified numbering. If
-- there are no errors in the key file, then a T.ChemKeys value is
-- returned as a Right value. Otherwise, the errors are formatted
-- into a single string and returned as a Left value.
fmtDict xs = case getErrs xs of
                  Nothing -> Right . getAllKeys $ xs
                  Just es -> Left . concat $ es

fmtDictAuto :: [ParsedLine] -> Either String T.ChemKeys
-- ^Formats a list of parsed lines for automatic key numbering. If
-- there are no errors in the key file, then a T.ChemKeys value is
-- returned as a Right value. Otherwise, the errors are formatted
-- into a single string and returned as a Left value.
fmtDictAuto xs = case getErrs xs of
                      Nothing -> Right . getAllKeysAuto $ xs
                      Just es -> Left . concat $ es

fmtErr :: (Int, String) -> String
-- ^Formats a line-number and error string pair to printable error.
fmtErr (n, e) = "line " ++ show n ++ ": " ++ e ++ "\n"

---------------------------------------
-- Utility parsing functions

parseLine :: (Int, String) -> ParsedLine
-- ^Reads a line number and string pair, check for errors and parse.
parseLine (lineNo, str) =
    case words str of
         [] -> EmptyLine lineNo
         ws -> let noComment = takeWhile ( (/=) '#' ) $ unwords ws
               in  chkLine lineNo $ words noComment

parseLineAuto :: (Int, String) -> ParsedLine
-- ^Reads a line number and string pair, check for errors and parse
-- ignoring anything after the key.
parseLineAuto (lineNo, str) =
    case words str of
         []          -> EmptyLine lineNo
         ('#':cs):ws -> EmptyLine lineNo
         ws          -> let noComment = takeWhile ( (/=) '#' ) $ unwords ws
                        in case words noComment of
                                []   -> ErrorLine lineNo "Missing key"
                                k:ks -> KeyLine lineNo k ""

chkLine :: Int -> [String] -> ParsedLine
-- ^Checks for valid formatting of input line in the chemno key file
-- when the user is responsible for providing the chemical numbers.
chkLine n [] = EmptyLine n
chkLine n (k:ks)
    | length ks < 1  = ErrorLine n $ noValErr k
    | length ks > 1  = ErrorLine n $ tooManyErr k
    | length ks == 1 = KeyLine n k ( head ks )
    where noValErr x   = "No value for key: " ++ "'" ++ x ++ "'"
          tooManyErr x = "Too many values for key: " ++ "'" ++ x ++ "'"

chkMultDef :: [ParsedLine] -> ParsedLine -> [ParsedLine]
-- ^Accumulator for converting multiply defined keys to errors.
chkMultDef xs kl@(KeyLine n k v) =
    if any ( \ x -> getKey x == Just k ) xs
       then xs ++ [ e ]
       else xs ++ [ kl ]
    where e = ErrorLine n $ "Multiply defined key: " ++ "'" ++ k ++ "'"
chkMultDef xs x = xs ++ [x]

getErrs :: [ParsedLine] -> Maybe [String]
-- ^Extracts all errors from a list of parsed lines and formats them.
-- Returns Nothing if there are no errors.
getErrs xs = case [ (n, e) | ( ErrorLine n e ) <- xs ] of
                  [] -> Nothing
                  es -> Just . map fmtErr $ es

getAllKeys :: [ParsedLine] -> T.ChemKeys
-- ^Extracts all keys from a list of parsed lines and formats them as
-- T.ChemKeys where the user is responsible for the chemical numbers.
getAllKeys xs = [ (k, v) | ( KeyLine _ k v ) <- xs ]

getAllKeysAuto :: [ParsedLine] -> T.ChemKeys
-- ^Extracts all keys from a list of parsed lines and formats them as
-- T.ChemKeys where the keys are to be autonumbered.
getAllKeysAuto xs = zip [ k | ( KeyLine _ k _ ) <- xs ] ( map show [1..] )

getKey :: ParsedLine -> Maybe String
-- ^Accessor function to get the key fields from a parsed line.
getKey (ErrorLine _ _) = Nothing
getKey (EmptyLine _)   = Nothing
getKey (KeyLine n k v) = Just k
