{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE TemplateHaskell            #-}

module Avers.Types where


import           GHC.Generics

import           Control.Applicative

import           Control.Monad.Except
import           Control.Monad.State

import           Control.Concurrent.STM

import           Data.Time.Clock
import           Data.String
import           Data.ByteString.Lazy (ByteString)

import           Data.Text (Text)
import qualified Data.Text as T

import           Data.Map  (Map)

import           Data.Monoid
import           Data.Char

import           Data.Attoparsec.Text

import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import           Data.Aeson (Value(String))
import           Data.Aeson.Types (parseEither)

import           Network.URI

import qualified Database.RethinkDB       as R
import           Database.RethinkDB.TH

import           Data.Pool

import           Avers.TH
import           Avers.Index
import           Avers.Metrics.Measurements



-----------------------------------------------------------------------------
-- | Pk - Types which can be converted to a database primary key.

class Pk a where
    toPk :: a -> Text

instance Pk Text where
    toPk = id



-----------------------------------------------------------------------------
-- | Path

newtype Path = Path { unPath :: Text }
    deriving (Eq, Ord, Show, Generic)

instance IsString Path where
    fromString = Path . T.pack

instance ToJSON Path where
    toJSON = toJSON . unPath

instance FromJSON Path where
    parseJSON (String s) = return $ Path s
    parseJSON _          = fail "Path"

instance R.FromDatum Path where
    parseDatum (R.String s) = return $ Path s
    parseDatum _            = fail "Path"

instance R.ToDatum Path where
    toDatum = R.toDatum . unPath


-- | This path refers to the root of an object. It is only used in 'Set'
-- operations.
rootPath :: Path
rootPath = Path ""



-----------------------------------------------------------------------------
-- | ObjId

newtype ObjId = ObjId { unObjId :: Text }
    deriving (Eq, Ord, Show, Generic)

instance Pk ObjId where
    toPk = unObjId

instance ToJSON ObjId where
    toJSON = toJSON . unObjId

instance FromJSON ObjId where
    parseJSON x = ObjId <$> parseJSON x

instance R.FromDatum ObjId where
    parseDatum x = ObjId <$> R.parseDatum x

instance R.ToDatum ObjId where
    toDatum = R.toDatum . unObjId


-- | The root object id is used for object created internally or when there
-- is no applicable creator.
rootObjId :: ObjId
rootObjId = ObjId ""



-----------------------------------------------------------------------------
-- | RevId

newtype RevId = RevId { unRevId :: Int }
    deriving (Eq, Ord, Show, Generic)

instance Enum RevId where
    succ (RevId x) = RevId (succ x)
    pred (RevId x) = RevId (pred x)
    toEnum         = RevId
    fromEnum       = unRevId

instance Pk RevId where
    toPk = T.pack . show . unRevId

instance ToJSON RevId where
    toJSON = toJSON . unRevId

instance FromJSON RevId where
    parseJSON x = RevId <$> parseJSON x

instance R.FromDatum RevId where
    parseDatum x = RevId <$> R.parseDatum x

instance R.ToDatum RevId where
    toDatum = R.toDatum . unRevId


-- | The 'RevId' which is used for the initial snapshot.
zeroRevId :: RevId
zeroRevId = RevId 0



-----------------------------------------------------------------------------
-- | ObjectId

data ObjectId

    = BaseObjectId !ObjId
      -- ^ The base object whose snapshots contain the actual content.

    | ReleaseObjectId !ObjId !RevId
     -- ^ An object describing a particualar release of the base object.

    | AuthorizationObjectId !ObjId
      -- ^ Object which contains authorization rules.

    deriving (Eq, Ord, Show, Generic)


instance Pk ObjectId where
    toPk (BaseObjectId objId)          = toPk objId
    toPk (ReleaseObjectId objId revId) = toPk objId <> "/release/" <> toPk revId
    toPk (AuthorizationObjectId objId) = toPk objId <> "/authorization"

instance ToJSON ObjectId where
    toJSON = toJSON . toPk

instance FromJSON ObjectId where
    parseJSON (String x) = either fail return $ parseOnly objectIdParser x
    parseJSON _          = fail "ObjectId"

instance R.FromDatum ObjectId where
    parseDatum (R.String x) = either fail return $ parseOnly objectIdParser x
    parseDatum _            = fail "ObjectId"

instance R.ToDatum ObjectId where
    toDatum = R.toDatum . toPk


objectIdParser :: Parser ObjectId
objectIdParser = (releaseObjectId <|> authorizationObjectId <|> baseObjectId) <* endOfInput
  where
    objId = ObjId <$> takeWhile1 isAlphaNum
    revId = RevId <$> decimal

    baseObjectId = BaseObjectId
        <$> objId

    releaseObjectId = ReleaseObjectId
        <$> objId
        <*  string "/release/"
        <*> revId

    authorizationObjectId = AuthorizationObjectId
        <$> objId
        <*  string "/authorization"


parseObjectId :: Text -> Maybe ObjectId
parseObjectId text = case parseOnly objectIdParser text of
    Left _  -> Nothing
    Right v -> Just v


objectIdBase :: ObjectId -> ObjId
objectIdBase (BaseObjectId          objId  ) = objId
objectIdBase (ReleaseObjectId       objId _) = objId
objectIdBase (AuthorizationObjectId objId  ) = objId



-----------------------------------------------------------------------------
-- | The operations that can be applied to JSON values.
data Operation
  -- | Set is applied to 'Object's. It is used for adding, updating
  --   and deleting properties from the object.
  = Set
    { opPath  :: !Path
    , opValue :: !(Maybe Value)
    }

  -- | Splice is used to manipulate 'Array's. It can remove and insert
  --   multiple elements in a single operation.
  | Splice
    { opPath   :: !Path
    , opIndex  :: !Int
    , opRemove :: !Int
    , opInsert :: ![ Value ]
    }

    deriving (Eq, Show, Generic)

$(deriveEncoding (deriveJSONOptions "op"){
    omitNothingFields = True,
    sumEncoding       = TaggedObject "type" "content"
} ''Operation)



data PatchError
    = UnknownPatchError !Text
    deriving (Show, Generic)

type PatchM a = Either PatchError a


data Object = Object
  { objectId            :: !ObjId
  , objectType          :: !Text
  , objectCreatedAt     :: !UTCTime
  , objectCreatedBy     :: !ObjId
  , objectDeleted       :: !(Maybe Bool)
  } deriving (Show, Generic)

instance Pk Object where
    toPk = toPk . objectId

$(deriveEncoding (deriveJSONOptions "object") ''Object)



-----------------------------------------------------------------------------
-- | Patch

data Patch = Patch
  { patchObjectId       :: !ObjectId
  , patchRevisionId     :: !RevId
  , patchAuthorId       :: !ObjId
  , patchCreatedAt      :: !UTCTime
  , patchOperation      :: !Operation
  } deriving (Show, Generic)

instance Pk Patch where
    toPk Patch{..} = toPk patchObjectId <> "@" <> toPk patchRevisionId

$(deriveEncoding (deriveJSONOptions "patch") ''Patch)



-----------------------------------------------------------------------------
-- | Snapshot

data Snapshot = Snapshot
  { snapshotObjectId    :: !ObjectId
  , snapshotRevisionId  :: !RevId
  , snapshotContent     :: !Value
  } deriving (Show, Generic)

instance Pk Snapshot where
    toPk Snapshot{..} = toPk snapshotObjectId <> "@" <> toPk snapshotRevisionId

$(deriveEncoding (deriveJSONOptions "snapshot") ''Snapshot)


-- | The initial snapshot on top of which all future patches are applied.
initialSnapshot :: ObjectId -> Snapshot
initialSnapshot objId = Snapshot objId (RevId (-1)) Aeson.emptyObject



-----------------------------------------------------------------------------
-- | Release

data Release = Release

instance ToJSON Release where
    toJSON = const Aeson.emptyObject

instance FromJSON Release where
    parseJSON (Aeson.Object _) = return Release
    parseJSON _                = fail "Release"

-- $(deriveEncoding (deriveJSONOptions "release") ''Release)



-----------------------------------------------------------------------------
-- | SecretId

newtype SecretId = SecretId { unSecretId :: Text }
    deriving (Show, Generic)

instance Pk SecretId where
    toPk = unSecretId

instance ToJSON SecretId where
    toJSON = toJSON . unSecretId

instance FromJSON SecretId where
    parseJSON x = SecretId <$> parseJSON x

instance R.FromDatum SecretId where
    parseDatum x = SecretId <$> R.parseDatum x

instance R.ToDatum SecretId where
    toDatum = R.toDatum . unSecretId



-----------------------------------------------------------------------------
-- | Secret
--
-- A 'Secret' is a password (encrypted with scrypt) that is attached to
-- a 'SecretId' (for example the 'ObjId' of an account).
--
-- It is up to you to ensure that 'SecretId's are unique. If you use 'ObjId's
-- then they by definition are.

data Secret = Secret
  { secretId    :: !SecretId
  , secretValue :: !Text
  } deriving (Generic)

instance Pk Secret where
    toPk = toPk . secretId

$(deriveEncoding (deriveJSONOptions "secret") ''Secret)



-----------------------------------------------------------------------------
-- | BlobId

newtype BlobId = BlobId { unBlobId :: Text }
    deriving (Show, Generic)

instance Pk BlobId where
    toPk = unBlobId

instance ToJSON BlobId where
    toJSON = toJSON . unBlobId

instance FromJSON BlobId where
    parseJSON x = BlobId <$> parseJSON x

instance R.FromDatum BlobId where
    parseDatum x = BlobId <$> R.parseDatum x

instance R.ToDatum BlobId where
    toDatum = R.toDatum . unBlobId



-----------------------------------------------------------------------------
-- | Blob

data Blob = Blob
  { blobId          :: !BlobId
  , blobSize        :: !Int
  , blobContentType :: !Text
  } deriving (Show, Generic)

instance Pk Blob where
    toPk = toPk . blobId

$(deriveEncoding (deriveJSONOptions "blob") ''Blob)



-----------------------------------------------------------------------------
-- | SessionId

newtype SessionId = SessionId { unSessionId :: Text }
    deriving (Generic)

instance Pk SessionId where
    toPk = unSessionId

instance ToJSON SessionId where
    toJSON = toJSON . unSessionId

instance FromJSON SessionId where
    parseJSON x = SessionId <$> parseJSON x

instance R.FromDatum SessionId where
    parseDatum x = SessionId <$> R.parseDatum x

instance R.ToDatum SessionId where
    toDatum = R.toDatum . unSessionId



-----------------------------------------------------------------------------
-- | The session record that is stored in the database.
--
-- A session is a unique identifier attached to a particular object. It
-- contains the creation date and when it was last accessed. If you need to
-- store additional data for a session, we recommend to use cookies.

data Session = Session
  { sessionId             :: !SessionId
  , sessionObjId          :: !ObjId
  , sessionCreatedAt      :: !UTCTime
  , sessionLastAccessedAt :: !UTCTime
  } deriving (Generic)

instance Pk Session where
    toPk Session{..} = toPk sessionId

$(deriveEncoding (deriveJSONOptions "session") ''Session)



data AversError
    = InternalError !AversError
    | DatabaseError !Text
    | PatchError !PatchError
    | ParseError !Value !Text
    | UnknownObjectType !Text
    | ObjectNotFound !ObjId
    | DocumentNotFound !Text
    | AversError !Text
    | NotAuthorized
    deriving (Show, Generic)


internalError :: AversError -> Avers a
internalError = throwError . InternalError

internal :: Avers a -> Avers a
internal m = m `catchError` internalError

databaseError :: Text -> Avers a
databaseError = throwError . DatabaseError

patchError :: PatchError -> Avers a
patchError = throwError . PatchError

parseError :: (MonadError AversError m) => Value -> Text -> m a
parseError value text = throwError $ ParseError value text

documentNotFound :: Text -> Avers a
documentNotFound = throwError . DocumentNotFound

strErr :: String -> Avers a
strErr = throwError . AversError . T.pack


-- | An 'ObjectType' describes a particular type of object that is managed by
-- Avers.
data ObjectType a = ObjectType
  { otType   :: !Text
    -- ^ The value of the @type@ field of the 'Object'.

  , otId     :: Avers ObjId
    -- ^ Action which generates a new id. This is so that object types can
    -- have different strategies how to generate ids.

  , otViews :: [SomeView a]
  }

data SomeObjectType where
     SomeObjectType :: (R.ToDatum a, R.FromDatum a, FromJSON a, ToJSON a)
        => ObjectType a -> SomeObjectType

parseValueAs :: (FromJSON a) => ObjectType a -> Value -> Either AversError a
parseValueAs ObjectType{..} value = case parseEither parseJSON value of
    Left  e -> parseError value (T.pack $ show e)
    Right x -> return x



-- | Configuration of the 'Avers' monad.
data Config = Config

    { databaseURI :: !URI
      -- ^ 'URI' which describes the connection details to the RethinkDB
      -- database. The 'URI' *MUST* include at least the hostname ('uriRegName')
      -- and database name ('uriPath' without the leading slash). The port
      -- ('uriPort') and credentials ('uriUserInfo') *MAY* be left empty.
      -- in that case the default port will be used.

    , putBlob :: BlobId -> Text -> ByteString -> IO (Either AversError ())
      -- ^ Function which saves the given blob in the blob store. This can be
      -- the local filesystem or an external service such as Amazon S3.

    , objectTypes :: ![SomeObjectType]
      -- ^ All the object types which Avers knows about.

    , emitMeasurement :: Measurement -> Double -> IO ()
      -- ^ This is called when the internal instrumentation code creates
      -- a measurement.
    }



-- | A change in the system, for example a new object, patch, release, blob etc.
data Change
    = CPatch !Patch -- ^ A new patch was created.
    deriving (Show, Generic)

instance ToJSON Change where
    toJSON (CPatch p) = Aeson.object [ "type" Aeson..= ("patch" :: Text), "content" Aeson..= p ]



data Handle = Handle
    { hConfig :: !Config
      -- ^ A reference to the config, just in case we need it.

    , hDatabaseHandlePool :: !(Pool R.Handle)
      -- ^ A pool of handles which are used to access the database.

    , hRecentRevisionCache :: !(TVar (Map ObjectId RevId))
      -- ^ Map from 'ObjectId' to a recent 'RevId'. It may be the latest or
      -- a few revisions behind.

    , hChanges :: !(TChan Change)
      -- ^ Changes in the system (new patches, objects, releases etc), even
      -- those created through other handles, are streamed into this channel.
      -- If you want to be informed of those changes, duplicate the channel
      -- and read from the copy.
    }



newtype Avers a = Avers
    { runAvers :: StateT Handle (ExceptT AversError IO) a
    } deriving (Functor, Applicative, Monad, MonadIO, MonadError AversError, MonadState Handle)

class (Monad m) => MonadAvers m where
    liftAvers :: Avers a -> m a

instance MonadAvers Avers where
    liftAvers = id

instance MonadAvers m => MonadAvers (StateT s m) where
    liftAvers = lift . liftAvers

evalAvers :: Handle -> Avers a -> IO (Either AversError a)
evalAvers h m = runExceptT $ evalStateT (runAvers m) h



------------------------------------------------------------------------------
-- View

data View obj a = View
  { viewName :: Text
    -- ^ The table name is derived from the view name. Therefore it should
    -- be unique amongst all views.

  , viewParser :: R.Datum -> Either AversError a
    -- ^ Function which parses objects stored in this view.

  , viewObjectTransformer :: obj -> Avers (Maybe a)
    -- ^ Function which transforms an Avers Object into a type stored
    -- in the view.

  , viewIndices :: [SomeIndex]
    -- ^ Secondary indices defined on the view.
  }


data SomeView obj where
     SomeView :: (R.ToDatum a, R.FromDatum a, FromJSON obj, ToJSON a)
        => View obj a -> SomeView obj