{-# LANGUAGE ScopedTypeVariables #-}

module Data.JsonSchema
  ( module Data.JsonSchema.Core
    -- * Schema feching tools
  , module Data.JsonSchema
    -- * Draft 4 specific
  , module Data.JsonSchema.Draft4
  ) where

import           Control.Monad.Except
import qualified Data.ByteString.Lazy as LBS
import           Data.Foldable
import qualified Data.HashMap.Strict as H
import           Data.String
import qualified Data.Text as T
import qualified Data.Vector as V

import           Data.JsonSchema.Core
import           Data.JsonSchema.Draft4
import qualified Data.JsonSchema.Helpers as HE
import           Data.JsonSchema.Reference
import           Import

-- | Take a schema. Retrieve every document either it or its
-- subschemas include via the "$ref" keyword. Load a 'Graph' out
-- with them.
--
-- TODO: This function's URL processing is hacky and needs improvement.
fetchReferencedSchemas :: Spec err -> RawSchema -> Graph -> IO (Either Text Graph)
fetchReferencedSchemas = fetchReferencedSchemas' HE.defaultFetch

-- | A version of fetchReferencedSchemas where the function to make requests
-- is provided by the user. This allows restrictions to be added, e.g. rejecting
-- non-local URLs.
fetchReferencedSchemas'
  :: forall t e m err. (IsString t, MonadError t e, Traversable e, MonadIO m)
  => (Text -> m (e LBS.ByteString))
  -> Spec err
  -> RawSchema
  -> Graph
  -> m (e Graph)
fetchReferencedSchemas' fetchRef spec rawSchema graph =
  let startingGraph = H.insert (_rsURI rawSchema) (_rsObject rawSchema) graph
  in foldlM (modFetch fetch) (return startingGraph) (includeSubschemas rawSchema)

  where
    includeSubschemas :: RawSchema -> Vector RawSchema
    includeSubschemas r =
      let newId = newResolutionScope (_rsURI r) (_rsObject r)
          xs = H.intersectionWith (\(ValSpec f _) x -> f newId x) (_unSpec spec) (_rsObject r)
          ys = V.concat . H.elems $ xs -- TODO: optimize
      in V.cons r . V.concat . V.toList $ includeSubschemas <$> ys

    modFetch :: (Graph -> RawSchema -> m (e Graph)) -> e Graph -> RawSchema -> m (e Graph)
    modFetch f eg rs = join <$> sequence (flip f rs <$> eg)

    fetch :: Graph -> RawSchema -> m (e Graph)
    fetch g r =
      case H.lookup "$ref" (_rsObject r) >>= HE.toTxt >>= refAndPointer of
        Nothing     -> return (return g)
        Just (s, _) ->
          let url = (_rsURI r `combineIdAndRef` s)
          in if T.length url <= 0 || H.member url g || not ("://" `T.isInfixOf` url)
            then return (return g)
            else modDec (decodeResponse url) =<< fetchRef url
      where
        decodeResponse :: Text -> LBS.ByteString -> m (e Graph)
        decodeResponse url bts =
          case eitherDecode bts of
            Left e    -> return $ throwError (fromString e)
            Right obj -> fetchReferencedSchemas' fetchRef spec (RawSchema url obj) g

        modDec :: (LBS.ByteString -> m (e Graph)) -> e LBS.ByteString -> m (e Graph)
        modDec f eBts = join <$> sequence (f <$> eBts)