module Propellor.Property.ConfFile (
        -- * Generic conffiles with sections
        SectionStart,
        SectionPast,
        AdjustSection,
        InsertSection,
        adjustSection,
        -- * Windows .ini files
        IniSection,
        IniKey,
        containsIniSetting,
        lacksIniSetting,
        hasIniSection,
        lacksIniSection,
        iniFileContains,
        -- * Conffiles that are actually shell scripts setting env vars
        ShellKey,
        containsShellSetting,
        lacksShellSetting,
) 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

-- | Key for a shell conffile property.  Conventionally uppercase letters and
-- numbers with underscores for separators.  See files in </etc/default>.
type ShellKey = String

-- | Ensures a shell conffile (like those in </etc/default>) exists and has a
-- key=value pair.
--
-- Comments out any further settings of that key further down the
-- file, to avoid those taking precedence.
containsShellSetting :: FilePath -> (ShellKey, String) -> Property UnixLike
containsShellSetting f (k, v) = adjust `before` dedup
  where
        adjust = adjustSection
                (f ++ " contains " ++ k ++ "=" ++ v)
                isline
                (not . isline)
                (const [line])
                (const [line])
                f
        dedup = fileProperty "" dedup' f
        dedup' ls = let (pre, wanted, post) = foldl' find ([], [], []) ls
                    in pre ++ wanted ++ map commentIfIsline post
        find (pre, wanted, post) l
                | null wanted && (not . isline) l = (pre ++ [l], wanted, post)
                | null wanted && isline l         = (pre, [l], post)
                | otherwise                       = (pre, wanted, post ++ [l])
        -- some /etc/default files comment settings lines with '# '
        -- and some use '#'; one advantage of just using '#' is that
        -- it distinguishes settings values from prose comments
        commentIfIsline l
                | isline l  = '#':l
                | otherwise = l

        isline s = (k ++ "=") `isPrefixOf` s
        line = k ++ "=" ++ shellEscape v

-- | Comments out a key=value pair in a shell conffile.
--
-- Does not delete the file if empty, and does not uncomment any
-- lines, so not a perfect reversion of 'containsShellSetting'.
lacksShellSetting :: FilePath -> (ShellKey, String) -> Property UnixLike
lacksShellSetting f (k, v) =
        fileProperty (f ++ "lacks shell setting " ++ k ++ "=" ++ v) go f
  where
        go ls = map commentOut ls
        commentOut l
                | (k ++ "=") `isPrefixOf` l = '#':l
                | otherwise                 = l