module Data.JsonPointer where

import           Control.Applicative
import           Control.Monad
import           Data.Aeson
import qualified Data.HashMap.Strict as H
import           Data.Text           (Text)
import qualified Data.Text           as T
import qualified Data.Vector         as V
import           Text.Read           (readMaybe)

newtype JsonPointer = JsonPointer { _unJsonPointer :: [Text] } deriving (Eq, Show)

data PointerErr
  -- | The Text to build a JSON Pointer must either be empty
  -- or start with a '/'.
  = InvalidFirstChar
  | UnescapedTilde
  | ObjectLookupFailed
  | ArrayIndexInvalid
  | ArrayElemNotFound
  | UnindexableValue
  deriving (Eq, Show)

-- | The Text to build a JSON Pointer must either be empty or start
-- with a '/'. If you're turning a URI Fragment into a JSON Pointer
-- you must drop the initial '#'.
jsonPointer :: Text -> Either PointerErr JsonPointer
jsonPointer t =
  JsonPointer <$> (unescape =<< process (T.splitOn "/" t))
  where
    process ::[Text] -> Either PointerErr [Text]
    process []     = Right []
    process (x:xs) = do
      unless (T.null x) $ Left InvalidFirstChar
      Right xs

    unescape :: [Text] -> Either PointerErr [Text]
    unescape xs = do
      void $ mapM checkValid xs
      Right $ T.replace "~0" "~" . T.replace "~1" "/" <$> xs

    checkValid :: Text -> Either PointerErr ()
    checkValid x = do
      let afterTildes = drop 1 $ T.splitOn "~" x
      if all (\y -> T.isPrefixOf "0" y || T.isPrefixOf "1" y) afterTildes
        then Right ()
        else Left UnescapedTilde

resolvePointer :: Value -> JsonPointer -> Either PointerErr Value
resolvePointer v p =
  case _unJsonPointer p of
    [] -> Right v
    _  -> resolveRefTok v p >>= uncurry resolvePointer

-- | For internal use and specialized applications that don't want to
-- resolve the entire pointer at once.
resolveRefTok :: Value -> JsonPointer -> Either PointerErr (Value, JsonPointer)
resolveRefTok v p = do
  case _unJsonPointer p of
    []       -> Right (v, p)
    (tok:ps) ->
      case v of
        Object h ->
          case H.lookup tok h of
            Nothing -> Left ObjectLookupFailed
            Just vv -> Right (vv, JsonPointer ps)
        Array vs -> do
          case readMaybe (T.unpack tok) of
            Nothing -> Left ArrayIndexInvalid
            Just n  -> do
              unless (n >= 0)          $ Left ArrayIndexInvalid
              unless (n < V.length vs) $ Left ArrayElemNotFound
              Right (vs V.! n, JsonPointer ps)
        vv -> do
          unless (null ps) $ Left UnindexableValue
          Right (vv, JsonPointer [])