{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}
module Data.Aeson.Patch (
  Patch(..),
  Operation(..),
  
  modifyPointer,
  modifyPointers,
  
  isAdd,
  isRem,
  isRep,
  isMov,
  isCpy,
  isTst,
) where
import           Control.Applicative
import           Control.Monad
import           Data.Aeson
import           Data.Aeson.Types
import qualified Data.ByteString.Lazy.Char8 as BS
import           Data.Monoid
import           Data.Semigroup             (Semigroup)
import           Data.Vector                (Vector)
import qualified Data.Vector                as V
import           GHC.Generics               (Generic)
import Data.Aeson.Pointer
newtype Patch = Patch
    { patchOperations :: [Operation] }
  deriving (Eq, Show, Semigroup, Monoid, Generic)
instance ToJSON Patch where
    toJSON (Patch ops) = toJSON ops
instance FromJSON Patch where
    parseJSON = modifyFailure ("Could not parse patch: " <> ) . parsePatch
      where
        parsePatch (Array v) = Patch <$> mapM parseJSON (V.toList v)
        parsePatch v = typeMismatch "Array" v
modifyPointers :: (Pointer -> Pointer) -> Patch -> Patch
modifyPointers f (Patch ops) = Patch (map (modifyPointer f) ops)
data Operation
    = Add { changePointer :: Pointer, changeValue :: Value }
    
    | Cpy { changePointer :: Pointer, fromPointer :: Pointer }
    
    | Mov { changePointer :: Pointer, fromPointer :: Pointer }
    
    | Rem { changePointer :: Pointer }
    
    | Rep { changePointer :: Pointer, changeValue :: Value }
    
    | Tst { changePointer :: Pointer, changeValue :: Value }
    
  deriving (Eq, Show, Generic)
instance ToJSON Operation where
    toJSON (Add p v) = object
        [ ("op", "add")
        , "path"  .= p
        , "value" .= v
        ]
    toJSON (Cpy p f) = object
        [ ("op", "copy")
        , "path" .= p
        , "from" .= f
        ]
    toJSON (Mov p f) = object
        [ ("op", "move")
        , "path" .= p
        , "from" .= f
        ]
    toJSON (Rem p) = object
        [ ("op", "remove")
        , "path" .= p
        ]
    toJSON (Rep p v) = object
        [ ("op", "replace")
        , "path"  .= p
        , "value" .= v
        ]
    toJSON (Tst p v) = object
        [ ("op", "test")
        , "path" .= p
        , "value" .= v
        ]
instance FromJSON Operation where
    parseJSON = parse
      where
        parse o@(Object v)
            =   (op v "add"     *> (Add <$> v .: "path" <*> v .: "value"))
            <|> (op v "copy"    *> (Cpy <$> v .: "path" <*> v .: "from"))
            <|> (op v "move"    *> (Mov <$> v .: "path" <*> v .: "from"))
            <|> (op v "remove"  *> (Rem <$> v .: "path"))
            <|> (op v "replace" *> (Rep <$> v .: "path" <*> v .: "value"))
            <|> (op v "test"    *> (Tst <$> v .: "path" <*> v .: "value"))
            <|> fail ("Expected a JSON patch operation, encountered: " <> BS.unpack (encode o))
        parse v = typeMismatch "Operation" v
        op v n = fixed v "op" (String n)
        fixed o n val = do
            v' <- o .: n
            if v' == val
              then return v'
              else mzero
        fixed' o n val = (o .: n) >>= \v -> guard (v == n)
modifyPointer :: (Pointer -> Pointer) -> Operation -> Operation
modifyPointer f op =
  case op of
    Add{..} -> op{ changePointer = f changePointer }
    Cpy{..} -> op{ changePointer = f changePointer, fromPointer = f fromPointer }
    Mov{..} -> op{ changePointer = f changePointer, fromPointer = f fromPointer }
    Rem{..} -> op{ changePointer = f changePointer }
    Rep{..} -> op{ changePointer = f changePointer }
    Tst{..} -> op{ changePointer = f changePointer }
isAdd :: Operation -> Bool
isAdd Add{} = True
isAdd _ = False
isCpy :: Operation -> Bool
isCpy Cpy{} = True
isCpy _ = False
isMov :: Operation -> Bool
isMov Mov{} = True
isMov _ = False
isRem :: Operation -> Bool
isRem Rem{} = True
isRem _ = False
isRep :: Operation -> Bool
isRep Rep{} = True
isRep _ = False
isTst :: Operation -> Bool
isTst Tst{} = True
isTst _ = False