{- This file is part of settings.
 -
 - Written in 2015 by fr33domlover <fr33domlover@rel4tion.org>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

-- | This module provides functions work working with the 'Section' type, i.e.
-- option trees. The style is similar to the APIs for @HashMap@s and @Map@s.
-- You can use these functions to construct a custom settings tree UI. Before
-- you do that, try the "Data.Settings.Interface" module, which may already
-- offer what you need.
module Data.Settings.Section
    ( -- * Construction
      empty
    , singleton
      -- * Observation
    , hasOpts
    , hasSubs
    , null
    , member
    , memberOpt
    , memberSub
    , lookup
    , lookupOpt
    , lookupSub
      -- * Modification
    , insert
    , deleteOpt
    , deleteSub
    , delete
    )
where

import Data.Either (isRight)
import qualified Data.HashMap.Lazy as M
import Data.Settings.Types
import Prelude hiding (lookup, null)

-- | Construct an empty section, no options and no subsections.
empty :: Section m
empty = Section M.empty M.empty

-- | Construct a section with a single option.
singleton :: SecName -> Option m -> Section m
singleton name opt = Section (M.singleton name opt) M.empty

-- | Return 'True' if this section contains any options, 'False' otherwise.
hasOpts :: Section m -> Bool
hasOpts = not . M.null . secOpts

-- | Return 'True' if this section contains any subsections, 'False' otherwise.
hasSubs :: Section m -> Bool
hasSubs = not . M.null . secSubs

-- | Return 'True' if this section is empty (no options, no subsections),
-- 'False' otherwise.
null :: Section m -> Bool
null s = not $ hasOpts s || hasSubs s

-- | Return 'True' if an option or a subsection is present at the specified
-- path, 'False' otherwise.
member :: OptRoute -> Section m -> (Bool, Bool)
member route sec =
    case lookup route sec of
        Nothing -> (False, False)
        Just e  -> (True, isRight e)

-- | Return 'True' if an option is present at the specified path, 'False'
-- otherwise.
memberOpt :: OptRoute -> Section m -> Bool
memberOpt route sec =
    let (m, o) = member route sec
    in  m && o

-- | Return 'True' if a subsection is present at the specified path, 'False'
-- otherwise.
memberSub :: OptRoute -> Section m -> Bool
memberSub route sec =
    let (m, o) = member route sec
    in  m && not o

-- | Return 'Just' the section or option at the specified path, or 'Nothing' if
-- this section contains no such path.
lookup :: OptRoute -> Section m -> Maybe (Either (Section m) (Option m))
lookup []     sec = Just $ Left sec
lookup [name] sec =
    case (M.lookup name $ secOpts sec, M.lookup name $ secSubs sec) of
        (Just o,  _)       -> Just $ Right o
        (Nothing, Just s)  -> Just $ Left s
        (Nothing, Nothing) -> Nothing
lookup (n:ns) sec =
    case M.lookup n $ secSubs sec of
        Just s  -> lookup ns s
        Nothing -> Nothing

-- | Return 'Just' the option at the specified path, or 'Nothing' if this
-- section doesn't contain an option at this path.
lookupOpt :: [String] -> Section m -> Maybe (Option m)
lookupOpt path sec =
    case lookup path sec of
        Just (Right o) -> Just o
        _              -> Nothing

-- | Return 'Just' the section at the specified path, or 'Nothing' if this
-- section doesn't contain a subsection at this path.
lookupSub :: [String] -> Section m -> Maybe (Section m)
lookupSub path sec =
    case lookup path sec of
        Just (Left s) -> Just s
        _             -> Nothing

-- | Add the specified option at the specified path under this section. If the
-- section previously contained an option for this path, the old value is
-- replaced.
insert :: [String] -> Option m -> Section m -> Section m
insert path opt s@(Section opts subs) =
    case path of
        []     -> s
        [name] -> Section (M.insert name opt opts) subs
        (n:ns) ->
                -- Find the section or make a new one
            let sub   = M.lookupDefault empty n subs
                -- Insert the option there, applying recursively
                sub'  = insert ns opt sub
                -- Put the result back into the subsection map
                subs' = M.insert n sub' subs
            in  Section opts subs'

deleteImpl :: Bool -> Bool -> [String] -> Section m -> Section m
deleteImpl delO delS path sec = d path sec
    where
    d path sec@(Section opts subs) =
        case path of
            []     -> sec
            [name] -> Section
                        (if delO then M.delete name opts else opts)
                        (if delS then M.delete name subs else subs)
            (n:ns) -> Section opts $ M.adjust (d ns) n subs

-- | Remove the option at the specified path, if present. If there is a section
-- under this path, it won't be removed.
deleteOpt :: [String] -> Section m -> Section m
deleteOpt = deleteImpl True False

-- | Remove the section at the specified path, if present. If there is an
-- option under this path, it won't be removed.
deleteSub :: [String] -> Section m -> Section m
deleteSub = deleteImpl False True

-- | Remove the option or section at the specified path, if present.
delete :: [String] -> Section m -> Section m
delete = deleteImpl True True