{-# LANGUAGE DeriveDataTypeable #-}
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
module Data.Serialization (
	Conf,
	SettingInfo(..),
	readConfigFile,
	writeConfigFile,
	ParseException) where

import System.IO
import qualified Data.Text.IO as T
import qualified Data.Map as M
import Text.ParserCombinators.Parsec
import Text.Parsec.Text as T
import Control.Monad (unless, when)
import Control.Exception (throwIO, Exception)
import Data.Typeable (Typeable)
import System.Directory (doesFileExist, copyFile)
import Control.Applicative ((<$>))

data SettingInfo = SettingInfo { value :: String, userSet :: Bool } deriving (Show, Eq)

-- | The in-memory configuration data.
type Conf = M.Map String SettingInfo

-- | The configuration file is in an invalid format.
data ParseException = ParseException FilePath String
	deriving (Show, Typeable)
instance Exception ParseException

readConfigFile :: FilePath -> IO Conf
readConfigFile path = do
	contents <- T.readFile path
	case parse parseConfigFile "" contents of
		Left pe -> throwIO (ParseException path
			$ "Invalid configuration file " ++ show (errorPos pe))
		Right v -> return v

data ConfigElement = ConfigEntry String String
	| Comment

isConfigEntry :: ConfigElement -> Bool
isConfigEntry (ConfigEntry _ _) = True
isConfigEntry _ = False

parseConfigFile :: T.GenParser st Conf
parseConfigFile = do
	elements <- many $ comment <|> configEntry <|> emptyLine
	let configEntries = filter isConfigEntry elements
	return $ M.fromList $ map (\(ConfigEntry a b) ->
		(a, SettingInfo {value=b, userSet=True})) configEntries

comment :: T.GenParser st ConfigElement
comment = char '#' >> finishLine >> return Comment

configEntry :: T.GenParser st ConfigElement
configEntry = do
	key <- many1 $ noneOf " \t=\r\n"
	char '='
	val <- finishLine
	-- so now we finished the parsing of the classical
	-- key=value, however we support a little bit more,
	-- you can do something like that:
	-- key=[1,2,
	--      3,4,
	--      5, 6]
	--  In that case the value will be
	--  "[1,2,3,4,5, 6]"
	blanks <- many $ oneOf " \t"
	fullVal <- if null blanks
		then return val
		else do
			firstExtraLine <- finishLine
			rest <- concat <$> many (lineSkipSpaces $ length blanks)
			return $ val ++ firstExtraLine ++ rest
	return $ ConfigEntry key fullVal

finishLine :: T.GenParser st String
finishLine = do
	result <- many $ noneOf "\r\n"
	many1 $ oneOf "\r\n"
	return result

lineSkipSpaces :: Int -> T.GenParser st String
lineSkipSpaces spaceCount = do
	count spaceCount (oneOf "\t ")
	result <- finishLine
	return result

emptyLine :: T.GenParser st ConfigElement
emptyLine = do
	many $ oneOf " \t"
	many1 $ oneOf "\r\n"
	return Comment

writeConfigFile :: FilePath -> Conf -> IO ()
writeConfigFile path config = do
	whenM (doesFileExist path) $
		copyFile path $ path ++ ".bak"
	withFile path WriteMode $ \handle -> do
		hPutStrLn handle "# This file is autogenerated. You can change, comment and uncomment settings but text comments you may add will be lost."
		mapM_ (uncurry $ writeConfigEntry handle) $ M.toList config
	where whenM s r = s >>= flip when r

writeConfigEntry :: Handle -> String -> SettingInfo -> IO ()
writeConfigEntry handle key (SettingInfo sValue sUserSet) = do
	unless sUserSet $ hPutStr handle "# "
	hPutStrLn handle $ key ++ "=" ++ (wrap sValue)

wrap :: String -> String
wrap str = if null rest
		then str
		else first ++ "\n  " ++ wrap rest
	where (first, rest) = splitAt 80 str