    module Propellor.Property.ConfFile (
    	-- * Generic conffiles with sections
    	SectionStart,
    	SectionPast,
    	AdjustSection,
    	InsertSection,
    	adjustSection,
    	-- * Windows .ini files
    	IniSection,
    	IniKey,
    	containsIniSetting,
    	lacksIniSetting,
    	hasIniSection,
    	lacksIniSection,
    	iniFileContains,
    ) where

    import Propellor.Base
    import Propellor.Property.File

    import Data.List (isPrefixOf, foldl')

    -- | find the line that is the start of the wanted section (eg, == "<Foo>")
    type SectionStart  = Line -> Bool
    -- | find a line that indicates we are past the section
    -- (eg, a new section header)
    type SectionPast   = Line -> Bool
    -- | run on all lines in the section, including the SectionStart line;
    -- can add, delete, and modify lines, or even delete entire section
    type AdjustSection = [Line] -> [Line]
    -- | if SectionStart does not find the section in the file, this is used to
    -- insert the section somewhere within it
    type InsertSection = [Line] -> [Line]

    -- | Adjusts a section of conffile.
    adjustSection
    	:: Desc
    	-> SectionStart
    	-> SectionPast
    	-> AdjustSection
    	-> InsertSection
    	-> FilePath
    	-> Property UnixLike
    adjustSection desc start past adjust insert = fileProperty desc go
      where
    	go ls = let (pre, wanted, post) = foldl' find ([], [], []) ls
    		in if null wanted
    			then insert ls
    			else pre ++ adjust wanted ++ post
    	find (pre, wanted, post) l
    		| null wanted && null post && (not . start) l =
    			(pre ++ [l], wanted, post)
    		| (start l && null wanted && null post)
    		  || ((not . null) wanted && null post && (not . past) l) =
    			  (pre, wanted ++ [l], post)
    		| otherwise = (pre, wanted, post ++ [l])

    -- | Name of a section of an .ini file. This value is put
    -- in square braces to generate the section header.
    type IniSection = String

    -- | Name of a configuration setting within a .ini file.
    type IniKey = String

    iniHeader :: IniSection -> String
    iniHeader header = '[' : header ++ "]"

    adjustIniSection
    	:: Desc
    	-> IniSection
    	-> AdjustSection
    	-> InsertSection
    	-> FilePath
    	-> Property UnixLike
    adjustIniSection desc header =
    	adjustSection
    	desc
    	(== iniHeader header)
    	("[" `isPrefixOf`)

    -- | Ensures that a .ini file exists and contains a section
    -- with a key=value setting.
    containsIniSetting :: FilePath -> (IniSection, IniKey, String) -> Property UnixLike
    containsIniSetting f (header, key, value) = adjustIniSection
    	(f ++ " section [" ++ header ++ "] contains " ++ key ++ "=" ++ value)
    	header
    	go
    	(++ [confheader, confline]) -- add missing section at end
    	f
      where
    	confheader = iniHeader header
    	confline   = key ++ "=" ++ value
    	go []      = [confline]
    	go (l:ls)  = if isKeyVal l then confline : ls else l : go ls
    	isKeyVal x = (filter (/= ' ') . takeWhile (/= '=')) x `elem` [key, '#':key]

    -- | Removes a key=value setting from a section of an .ini file.
    -- Note that the section heading is left in the file, so this is not a
    -- perfect reversion of containsIniSetting.
    lacksIniSetting :: FilePath -> (IniSection, IniKey, String) -> Property UnixLike
    lacksIniSetting f (header, key, value) = adjustIniSection
    	(f ++ " section [" ++ header ++ "] lacks " ++ key ++ "=" ++ value)
    	header
    	(filter (/= confline))
    	id
    	f
      where
    	confline = key ++ "=" ++ value

    -- | Ensures that a .ini file exists and contains a section
    -- with a given key=value list of settings.
    hasIniSection :: FilePath -> IniSection -> [(IniKey, String)] -> Property UnixLike
    hasIniSection f header keyvalues = adjustIniSection
    	("set " ++ f ++ " section [" ++ header ++ "]")
    	header
    	go
    	(++ confheader : conflines) -- add missing section at end
    	f
      where
    	confheader = iniHeader header
    	conflines  = map (\(key, value) -> key ++ "=" ++ value) keyvalues
    	go _       = confheader : conflines

    -- | Ensures that a .ini file does not contain the specified section.
    lacksIniSection :: FilePath -> IniSection -> Property UnixLike
    lacksIniSection f header = adjustIniSection
    	(f ++ " lacks section [" ++ header ++ "]")
    	header
    	(const []) -- remove all lines of section
    	id -- add no lines if section is missing
    	f

    -- | Specifies the whole content of a .ini file.
    --
    -- Revertijg this causes the file not to exist.
    iniFileContains :: FilePath -> [(IniSection, [(IniKey, String)])] -> RevertableProperty UnixLike UnixLike
    iniFileContains f l = f `hasContent` content <!> notPresent f
      where
    	content = concatMap sectioncontent l
    	sectioncontent (section, keyvalues) = iniHeader section :
    		map (\(key, value) -> key ++ "=" ++ value) keyvalues