{-# Language GADTs #-}
{-|
Module      : Config.Schema.Load
Description : Operations to extract a value from a configuration.
Copyright   : (c) Eric Mertens, 2017
License     : ISC
Maintainer  : emertens@gmail.com

This module automates the extraction of a decoded value from a configuration
value according to a specification as built using "Config.Schema.Spec".

-}
module Config.Schema.Load
  ( loadValue
  , loadValueFromFile

  -- * Errors
  , ValueSpecMismatch(..)
  , PrimMismatch(..)
  , Problem(..)
  ) where

import           Control.Exception                (throwIO)
import           Control.Monad                    (zipWithM)
import           Control.Monad.Trans.Class        (lift)
import           Control.Monad.Trans.State        (StateT(..), runStateT, state)
import           Control.Monad.Trans.Except       (Except, runExcept, throwE, withExcept)
import           Data.List.NonEmpty               (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import           Data.Text                        (Text)
import qualified Data.Text.IO as Text

import           Config
import           Config.Schema.Types
import           Config.Schema.Load.Error


-- | Match a 'Value' against a 'ValueSpec' and return either
-- the interpretation of that value or the list of errors
-- encountered.
loadValue ::
  ValueSpec a                       {- ^ specification           -} ->
  Value p                           {- ^ value                   -} ->
  Either (ValueSpecMismatch p) a {- ^ errors or decoded value -}
loadValue spec val = runExcept (getValue spec val)


-- | Read a configuration file, parse it, and validate it according
-- to the given specification.
--
-- Throws 'IOError', 'ParseError', or 'ValueSpecMismatch'
loadValueFromFile ::
  ValueSpec a {- ^ specification -} ->
  FilePath    {- ^ filename      -} ->
  IO a
loadValueFromFile spec path =
  do txt <- Text.readFile path
     let exceptIO m = either throwIO return m
     val <- exceptIO (parse txt)
     exceptIO (loadValue spec val)

getSection :: PrimSectionSpec a -> StateT [Section p] (Except (Problem p)) a
getSection (ReqSection k _ w) =
  do mb <- state (lookupSection k)
     lift $ case mb of
       Just v -> getValue' (SubkeyProblem k) w v
       Nothing -> throwE (MissingSection k)
getSection (OptSection k _ w) =
  do mb <- state (lookupSection k)
     lift (traverse (getValue' (SubkeyProblem k) w) mb)


getSections :: SectionsSpec a -> [Section p] -> Except (Problem p) a
getSections spec xs =
  do (a,leftovers) <- runStateT (runSections getSection spec) xs
     case NonEmpty.nonEmpty leftovers of
       Nothing -> return a
       Just ss -> throwE (UnusedSections (fmap sectionName ss))


getValue :: ValueSpec a -> Value p -> Except (ValueSpecMismatch p) a
getValue s v = withExcept (ValueSpecMismatch (valueAnn v) (describeValue v)) (runValueSpec (getValue1 v) s)

-- | Match a 'Value' against a 'ValueSpec' given a wrapper for any nested
-- mismatch errors that might occur.
getValue' ::
  (ValueSpecMismatch p -> Problem p) ->
  ValueSpec a ->
  Value p ->
  Except (Problem p) a
getValue' p s v = withExcept (p . ValueSpecMismatch (valueAnn v) (describeValue v)) (runValueSpec (getValue1 v) s)

getValue1 :: Value p -> PrimValueSpec a -> Except (NonEmpty (PrimMismatch p)) a
getValue1 v prim = withExcept (pure . PrimMismatch (describeSpec prim))
                              (getValue2 v prim)

-- | Match a primitive value specification against a single value.
getValue2 :: Value p -> PrimValueSpec a -> Except (Problem p) a
getValue2 (Text _ t)       TextSpec           = pure t
getValue2 (Number _ n)     NumberSpec         = pure n
getValue2 (List _ xs)      (ListSpec w)       = getList w xs
getValue2 (Atom _ b)       AnyAtomSpec        = pure (atomName b)
getValue2 (Atom _ b)       (AtomSpec a)
  | a == atomName b = pure ()
  | otherwise       = throwE WrongAtom
getValue2 (Sections _ s)   (SectionsSpec _ w) = getSections w s
getValue2 (Sections _ s)   (AssocSpec w)      = getAssoc w s
getValue2 v                (NamedSpec _ w)    = getValue' NestedProblem w v
getValue2 v                (CustomSpec _ w)   = getCustom w v
getValue2 _                _                  = throwE TypeMismatch


-- | This operation processes all of the values in a list with the given
-- value specification and updates the scope with a one-based list index.
getList :: ValueSpec a -> [Value p] -> Except (Problem p) [a]
getList w = zipWithM (\i -> getValue' (ListElementProblem i) w) [1::Int ..]


-- | This operation processes all of the values in a section list
-- against the given specification and associates them with the
-- section name.
getAssoc :: ValueSpec a -> [Section p] -> Except (Problem p) [(Text,a)]
getAssoc w = traverse $ \(Section _ k v) ->
                 (,) k <$> getValue' (SubkeyProblem k) w v

-- | Match a value against its specification. If 'Just' is matched
-- return the value. If 'Nothing is matched, report an error.
getCustom ::
  ValueSpec (Either Text a) {- ^ specification -} ->
  Value p                   {- ^ value         -} ->
  Except (Problem p) a
getCustom w v = either (throwE . CustomProblem) pure =<< getValue' NestedProblem w v


-- | Extract a section from a list of sections by name.
lookupSection ::
  Text                         {- ^ section name                       -} ->
  [Section p]                  {- ^ available sections                 -} ->
  (Maybe (Value p), [Section p]) {- ^ found value and remaining sections -}
lookupSection _ [] = (Nothing, [])
lookupSection key (s@(Section _ k v):xs)
  | key == k  = (Just v, xs)
  | otherwise = case lookupSection key xs of
                  (res, xs') -> (res, s:xs')