{-# LANGUAGE
    GeneralizedNewtypeDeriving
  , ScopedTypeVariables
  #-}
module Data.JSON.Schema.Validate
  ( isValid
  , validate
  , ValidationError (..)
  , ErrorType (..)
  ) where

import Control.Applicative
import Control.Monad.RWS.Strict
import Data.Aeson (Value)
import Data.HashMap.Strict (HashMap)
import Data.Scientific
import Data.Text (Text)
import Data.Vector (Vector)
import qualified Data.Aeson          as A
import qualified Data.HashMap.Strict as H
import qualified Data.Text           as T
import qualified Data.Vector         as V

import Data.JSON.Schema (Schema)
import qualified Data.JSON.Schema as S

-- | Validates a value against a schema returning errors.
validate :: Schema -> Value -> Vector ValidationError
validate s v = (\(_,_,errs) -> errs) $ runRWS (unM $ validate' s v) V.empty ()

-- | Predicate version of 'validate'.
isValid :: Schema -> Value -> Bool
isValid s v = V.null $ validate s v

data ValidationError = ValidationError
  { path      :: Vector Text -- ^ The Path to the property where the error occured, empty if the error is on the top level.
  , errorType :: ErrorType
  } deriving (Eq, Show)

data ErrorType
  = Mismatch             Schema Value                            -- ^ General type error.
  | BoundError           S.Bound Scientific                      -- ^ Number out of bounds.
  | LengthBoundError     S.LengthBound Int                       -- ^ String or Array out of bounds.
  | TupleLength          Int Int                                 -- ^ Expected and actual tuple length.
  | MissingRequiredField Text                                    -- ^ A required field is missing.
  | ChoiceError          (Vector (Vector ValidationError)) Value -- ^ All choices failed, contains the error of each branch.
  | NonUniqueArray       (HashMap Value Int)                    -- ^ The elements in the array that are duplicated with the number of occurences (at least 2).
  deriving (Eq, Show)

newtype M a = M { unM :: RWS (Vector Text) (Vector ValidationError) () a }
  deriving
    ( Functor
    , Applicative
    , Monad
    , MonadWriter (Vector ValidationError)
    , MonadReader (Vector Text)
    )

ok :: M ()
ok = return ()

err :: ErrorType -> M ()
err e = do
  pth <- ask
  tell . V.singleton . ValidationError pth $ e

cond :: ErrorType -> Bool -> M ()
cond e p = if p then ok else err e

nestPath :: Text -> M a -> M a
nestPath p = local (`V.snoc` p)

validate' :: Schema -> Value -> M ()
validate' sch val = case (sch, val) of
  ( S.Any       , _          ) -> ok
  ( S.Boolean   , A.Bool{}   ) -> ok
  ( S.Constant x, _          ) -> cond (Mismatch sch val) (x == val)
  ( S.Number   b, A.Number n ) ->
    do inLower b n
       inUpper b n
  ( S.Tuple   xs, A.Array vs ) ->
    do let vlen = V.length vs
       let xlen = length xs
       cond (TupleLength xlen vlen) (xlen == vlen)
       sequence_ $ zipWith3
         (\i s -> nestPath (T.pack (show i)) . validate' s)
         [(0::Int)..] xs (V.toList vs)
  ( S.Map      x, A.Object h ) ->
    do let kvs = H.toList h
       mapM_ (\(k,v) -> nestPath k $ validate' x v) kvs
  ( S.Object  fs, A.Object h ) -> mapM_ (`validateField` h) fs
  ( S.Choice   s, _          ) ->
    do let errs = map (`validate` val) s
       if any V.null errs
         then ok
         else err $ ChoiceError (V.fromList errs) val
  ( S.Value    b, A.String w ) ->
    do inLowerLength b (T.length w)
       inUpperLength b (T.length w)
  ( S.Array b u s, A.Array vs) ->
    do inLowerLength b (V.length vs)
       inUpperLength b (V.length vs)
       if u then unique vs else ok
       zipWithM_
         (\i -> nestPath (T.pack (show i)) . validate' s)
         [(0::Int)..] (V.toList vs)
  ( S.Boolean {}, _          ) -> err $ Mismatch sch val
  ( S.Number  {}, _          ) -> err $ Mismatch sch val
  ( S.Tuple   {}, _          ) -> err $ Mismatch sch val
  ( S.Object  {}, _          ) -> err $ Mismatch sch val
  ( S.Map     {}, _          ) -> err $ Mismatch sch val
  ( S.Value   {}, _          ) -> err $ Mismatch sch val
  ( S.Array   {}, _          ) -> err $ Mismatch sch val

validateField :: S.Field -> A.Object -> M ()
validateField f o = maybe req (nestPath (S.key f) . validate' (S.content f)) $ H.lookup (S.key f) o
  where
    req | not (S.required f) = ok
        | otherwise          = err $ MissingRequiredField (S.key f)

unique :: Vector Value -> M ()
unique vs = do
  let dups = H.filter (>= 2) . V.foldl' (\h v -> H.insertWith (+) v 1 h) H.empty $ vs
  unless (H.null dups) $
    err (NonUniqueArray dups)

inLower :: S.Bound -> Scientific -> M ()
inLower b v =
  if maybe True ((<= v) . fromIntegral) . S.lower $ b
    then ok
    else err (BoundError b v)

inUpper :: S.Bound -> Scientific -> M ()
inUpper b v =
  if maybe True ((>= v) . fromIntegral) . S.upper $ b
    then ok
    else err (BoundError b v)

inLowerLength :: S.LengthBound -> Int -> M ()
inLowerLength b v =
  if maybe True (<= v) . S.lowerLength $ b
    then ok
    else err (LengthBoundError b v)

inUpperLength :: S.LengthBound -> Int -> M ()
inUpperLength b v =
  if maybe True (>= v) . S.upperLength $ b
    then ok
    else err (LengthBoundError b v)