{-|
Module      : Toml.Schema.ParseTable
Description : A type for matching keys out of a table
Copyright   : (c) Eric Mertens, 2023
License     : ISC
Maintainer  : emertens@gmail.com

This module provides utilities for matching key-value pairs
out of tables while building up application-specific values.

It will help generate warnings for unused keys, help select
between multiple possible keys, and emit location-specific
error messages when keys are unavailable.

-}
module Toml.Schema.ParseTable (
    -- * Base interface
    ParseTable,
    KeyAlt(..),
    pickKey,
    parseTable,

    -- * Primitives
    liftMatcher,
    warnTable,
    warnTableAt,
    failTableAt,
    setTable,
    getTable,
    ) where

import Control.Applicative (Alternative, empty)
import Control.Monad (MonadPlus)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ReaderT(..), ask)
import Control.Monad.Trans.State.Strict (StateT(..), get, put)
import Data.Foldable (for_)
import Data.List (intercalate)
import Data.Map qualified as Map
import Data.Text (Text)
import Toml.Schema.Matcher (Matcher, inKey, failAt, warn, warnAt)
import Toml.Semantics (Table'(..), Value')
import Toml.Pretty

-- | Parser that tracks a current set of unmatched key-value
-- pairs from a table.
--
-- Use 'Toml.Schema.optKey' and 'Toml.Schema.reqKey' to extract keys.
--
-- Use 'getTable' and 'setTable' to override the table and implement
-- other primitives.
newtype ParseTable l a = ParseTable (ReaderT l (StateT (Table' l) (Matcher l)) a)
    deriving (Functor, Applicative, Monad, Alternative, MonadPlus)

-- | Implemented in terms of 'fail' on 'Matcher'
instance MonadFail (ParseTable l) where
    fail = ParseTable . fail

-- | Lift a matcher into the current table parsing context.
liftMatcher :: Matcher l a -> ParseTable l a
liftMatcher = ParseTable . lift . lift

-- | Run a 'ParseTable' computation with a given starting 'Table''.
-- Unused tables will generate a warning. To change this behavior
-- 'getTable' and 'setTable' can be used to discard or generate
-- error messages.
parseTable :: ParseTable l a -> l -> Table' l -> Matcher l a
parseTable (ParseTable p) l t =
 do (x, MkTable t') <- runStateT (runReaderT p l) t
    for_ (Map.assocs t') \(k, (a, _)) ->
        warnAt a ("unexpected key: " ++ show (prettySimpleKey k))
    pure x

-- | Return the remaining portion of the table being matched.
getTable :: ParseTable l (Table' l)
getTable = ParseTable (lift get)

-- | Replace the remaining portion of the table being matched.
setTable :: Table' l -> ParseTable l ()
setTable = ParseTable . lift . put

-- | Emit a warning without an annotation.
warnTable :: String -> ParseTable l ()
warnTable = liftMatcher . warn

-- | Emit a warning with the given annotation.
warnTableAt :: l -> String -> ParseTable l ()
warnTableAt l = liftMatcher . warnAt l

-- | Abort the current table matching with an error message at the given annotation.
failTableAt :: l -> String -> ParseTable l a
failTableAt l = liftMatcher . failAt l

-- | Key and value matching function
data KeyAlt l a
    = Key Text (Value' l -> Matcher l a) -- ^ pick alternative based on key match
    | Else (Matcher l a) -- ^ default case when no previous cases matched

-- | Take the first option from a list of table keys and matcher functions.
-- This operation will commit to the first table key that matches. If the
-- associated matcher fails, only that error will be propagated and the
-- other alternatives will not be matched.
--
-- If no keys match, an error message is generated explaining which keys
-- would have been accepted.
--
-- This is provided as an alternative to chaining multiple
-- 'Toml.Schema.reqKey' cases together with 'Control.Applicative.Alternative'
-- which will fall-through as a result of any failure to the next case.
pickKey :: [KeyAlt l a] -> ParseTable l a
pickKey xs =
 do MkTable t <- getTable
    foldr (f t) errCase xs
    where
        f _ (Else m) _ = liftMatcher m
        f t (Key k c) continue =
            case Map.lookup k t of
                Nothing -> continue
                Just (_, v) ->
                 do setTable $! MkTable (Map.delete k t)
                    liftMatcher (inKey k (c v))

        errCase =
         do l <- ParseTable ask
            case xs of
                []        -> empty -- there's nothing a user can do here
                [Key k _] -> failTableAt l ("missing key: " ++ show (prettySimpleKey k))
                _         -> failTableAt l ("possible keys: " ++ intercalate ", " [show (prettySimpleKey k) | Key k _ <- xs])
