{-|
Module: BattlePlace.WebApi.Types
Description: Web API types.
License: MIT
-}

{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, LambdaCase, ViewPatterns #-}

module BattlePlace.WebApi.Types
	( AccountId(..)
	, ProjectId(..)
	, Auth(..)
	, AuthType(..)
	, authTypeOf
	, Client(..)
	, ClientType(..)
	, clientTypeOf
	, ClientToken(..)
	, ProjectServerId(..)
	, ProjectServerToken(..)
	, MatchTeamSize
	, MatchTag(..)
	, MatchPlayerInfo(..)
	, MatchToken(..)
	, SessionToken(..)
	, SessionId(..)
	, ServerSessionToken(..)
	, Identified(..)
	, Base64ByteString(..)
	, Base64Word64(..)
	, StrWord64(..)
	) where

import Control.Monad
import qualified Data.Aeson as J
import qualified Data.Aeson.Types as J
import qualified Data.ByteArray as BA
import qualified Data.ByteArray.Encoding as BA
import qualified Data.ByteString as B
import Data.Either
import Data.Hashable
import qualified Data.Serialize as S
import Data.String
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Word
import Foreign.Storable
import GHC.Generics(Generic)
import Servant.API

import BattlePlace.Util

-- | Account id.
-- At the moment it's just itch user id, but that may change.
newtype AccountId = AccountId Base64Word64 deriving (J.FromJSON, J.ToJSON, FromHttpApiData)

-- | Project id.
newtype ProjectId = ProjectId Base64Word64 deriving (Eq, Hashable, J.FromJSON, J.ToJSON, FromHttpApiData)

data Auth
	= Auth_itchJwtToken
		{ auth_itchJwtToken :: !T.Text
		}
	| Auth_itchApiKey
		{ auth_itchApiKey :: !T.Text
		}
	| Auth_steamEncryptedTicket
		{ auth_steamEncryptedTicket :: !T.Text
		}
	| Auth_testKey
		{ auth_testKey :: !T.Text
		, auth_testId :: !StrWord64
		}
	deriving Generic
instance J.FromJSON Auth where
	parseJSON = J.genericParseJSON jsonOptions
		{ J.sumEncoding = J.UntaggedValue
		}
instance J.ToJSON Auth where
	toJSON = J.genericToJSON jsonOptions
		{ J.sumEncoding = J.UntaggedValue
		}
	toEncoding = J.genericToEncoding jsonOptions
		{ J.sumEncoding = J.UntaggedValue
		}

-- | Auth type (for logging).
data AuthType
	= AuthType_itchJwtToken
	| AuthType_itchApiKey
	| AuthType_steamEncryptedTicket
	| AuthType_testKey
	deriving (Eq, Generic)
instance Hashable AuthType
instance J.FromJSON AuthType where
	parseJSON = J.genericParseJSON jsonOptions
instance J.ToJSON AuthType where
	toJSON = J.genericToJSON jsonOptions
	toEncoding = J.genericToEncoding jsonOptions

authTypeOf :: Auth -> AuthType
authTypeOf = \case
	Auth_itchJwtToken {} -> AuthType_itchJwtToken
	Auth_itchApiKey {} -> AuthType_itchApiKey
	Auth_steamEncryptedTicket {} -> AuthType_steamEncryptedTicket
	Auth_testKey {} -> AuthType_testKey

data Client
	= Client_itch
		{ client_itchUserId :: {-# UNPACK #-} !StrWord64
		}
	| Client_steam
		{ client_steamId :: {-# UNPACK #-} !StrWord64
		}
	| Client_test
		{ client_testId :: {-# UNPACK #-} !StrWord64
		}
	deriving (Eq, Generic)
instance Hashable Client
instance J.FromJSON Client where
	parseJSON = J.genericParseJSON $ jsonOptionsWithTag "type"
instance J.ToJSON Client where
	toJSON = J.genericToJSON $ jsonOptionsWithTag "type"
	toEncoding = J.genericToEncoding $ jsonOptionsWithTag "type"

-- | Type of the client. Must correspont to JSON field "type" of 'Client'.
data ClientType
	= ClientType_itch
	| ClientType_steam
	| ClientType_test
	deriving (Eq, Generic)
instance Hashable ClientType
instance J.FromJSON ClientType where
	parseJSON = J.genericParseJSON jsonOptions
instance J.ToJSON ClientType where
	toJSON = J.genericToJSON jsonOptions
	toEncoding = J.genericToEncoding jsonOptions

clientTypeOf :: Client -> ClientType
clientTypeOf = \case
	Client_itch {} -> ClientType_itch
	Client_steam {} -> ClientType_steam
	Client_test {} -> ClientType_test

data ClientToken = ClientToken
	{ clientToken_projectId :: {-# UNPACK #-} !ProjectId
	, clientToken_client :: !Client
	} deriving Generic
instance J.FromJSON ClientToken where
	parseJSON = J.genericParseJSON jsonOptions
instance J.ToJSON ClientToken where
	toJSON = J.genericToJSON jsonOptions
	toEncoding = J.genericToEncoding jsonOptions

-- | Project's server id.
newtype ProjectServerId = ProjectServerId Base64Word64 deriving (Eq, Hashable, J.FromJSON, J.FromJSONKey, J.ToJSON, J.ToJSONKey)

-- | Project's secret server token.
newtype ProjectServerToken = ProjectServerToken T.Text deriving (Eq, Hashable, J.FromJSON, J.ToJSON)

-- | Size of a team in match request.
type MatchTeamSize = Int

-- | Match tag in match request.
newtype MatchTag = MatchTag T.Text deriving (Eq, Hashable, Monoid, J.FromJSON, J.ToJSON)

-- | Opaque player info.
newtype MatchPlayerInfo = MatchPlayerInfo T.Text deriving (Monoid, J.FromJSON, J.ToJSON)

-- | Match token.
data MatchToken = MatchToken
	{ matchToken_projectId :: {-# UNPACK #-} !ProjectId
	, matchToken_client :: !Client
	} deriving Generic
instance J.FromJSON MatchToken where
	parseJSON = J.genericParseJSON jsonOptions
instance J.ToJSON MatchToken where
	toJSON = J.genericToJSON jsonOptions
	toEncoding = J.genericToEncoding jsonOptions

-- | Session token.
data SessionToken = SessionToken
	{ sessionToken_sessionId :: !SessionId
	, sessionToken_teamIndex :: {-# UNPACK #-} !Int
	, sessionToken_mateIndex :: {-# UNPACK #-} !Int
	, sessionToken_client :: !Client
	} deriving Generic
instance J.FromJSON SessionToken where
	parseJSON = J.genericParseJSON jsonOptions
instance J.ToJSON SessionToken where
	toJSON = J.genericToJSON jsonOptions
	toEncoding = J.genericToEncoding jsonOptions

-- | Session id.
newtype SessionId = SessionId Base64ByteString deriving (Eq, Hashable, J.FromJSON, J.ToJSON)

-- | Server session token.
newtype ServerSessionToken = ServerSessionToken
	{ serverSessionToken_sessionId :: SessionId
	} deriving Generic
instance J.FromJSON ServerSessionToken where
	parseJSON = J.genericParseJSON jsonOptions
instance J.ToJSON ServerSessionToken where
	toJSON = J.genericToJSON jsonOptions
	toEncoding = J.genericToEncoding jsonOptions

-- | Generic data type for id + object.
data Identified i a = Identified
	{ identified_id :: !i
	, identified_info :: !a
	} deriving Generic
instance (J.FromJSON i, J.FromJSON a) => J.FromJSON (Identified i a) where
	parseJSON = J.genericParseJSON jsonOptions
instance (J.ToJSON i, J.ToJSON a) => J.ToJSON (Identified i a) where
	toJSON = J.genericToJSON jsonOptions
	toEncoding = J.genericToEncoding jsonOptions

-- | Implementation for parseJson using base64-serialized string.
base64ParseJsonStr :: S.Serialize a => T.Text -> J.Parser a
base64ParseJsonStr = \case
	(BA.convertFromBase BA.Base64URLUnpadded . T.encodeUtf8 -> Right (S.decode -> Right object)) -> return object
	_ -> fail "wrong base64 object"

-- | Implementation for toJson using base64-serialized string.
base64ToJsonStr :: S.Serialize a => a -> T.Text
base64ToJsonStr = T.decodeUtf8 . BA.convertToBase BA.Base64URLUnpadded . S.encode

-- | Implementation for parseUrlPiece using base64-serialized string.
base64ParseUrlPiece :: S.Serialize a => T.Text -> Either T.Text a
base64ParseUrlPiece = either (Left . T.pack) (either (Left . T.pack) Right . S.decode) . BA.convertFromBase BA.Base64URLUnpadded . T.encodeUtf8

-- | ByteString which serializes to JSON as base64 string.
newtype Base64ByteString = Base64ByteString B.ByteString deriving (Eq, Ord, Monoid, Hashable, BA.ByteArray, BA.ByteArrayAccess)
instance J.FromJSON Base64ByteString where
	parseJSON = either fail return . BA.convertFromBase BA.Base64URLUnpadded . T.encodeUtf8 <=< J.parseJSON
instance J.ToJSON Base64ByteString where
	toJSON = J.toJSON . T.decodeUtf8 . BA.convertToBase BA.Base64URLUnpadded

-- | Word64 which serializes to JSON as base64 string.
-- Useful because 64-bit integer is not representable in javascript.
newtype Base64Word64 = Base64Word64 Word64 deriving (Eq, Ord, Num, Storable, Hashable, S.Serialize, Read, Show)
instance J.FromJSON Base64Word64 where
	parseJSON = base64ParseJsonStr <=< J.parseJSON
instance J.FromJSONKey Base64Word64 where
	fromJSONKey = J.FromJSONKeyTextParser base64ParseJsonStr
instance J.ToJSON Base64Word64 where
	toJSON = J.toJSON . base64ToJsonStr
	toEncoding = J.toEncoding . base64ToJsonStr
instance J.ToJSONKey Base64Word64 where
	toJSONKey = J.toJSONKeyText base64ToJsonStr
instance FromHttpApiData Base64Word64 where
	parseUrlPiece = base64ParseUrlPiece
instance IsString Base64Word64 where
	fromString = fromRight (error "wrong base64 word") . J.parseEither J.parseJSON . J.String . T.pack

-- | Word64 which serializes to JSON as decimal string.
-- Useful because 64-bit integer is not representable in javascript.
newtype StrWord64 = StrWord64 Word64 deriving (Eq, Ord, Num, Storable, Hashable, S.Serialize, Read, Show)
instance J.FromJSON StrWord64 where
	parseJSON = f . reads <=< J.parseJSON where
		f = \case
			[(n, "")] -> return $ StrWord64 n
			_ -> fail "wrong number"
instance J.ToJSON StrWord64 where
	toJSON (StrWord64 n) = J.toJSON $ show n
instance FromHttpApiData StrWord64 where
	parseUrlPiece = f . reads <=< parseUrlPiece where
		f = \case
			[(n, "")] -> return $ StrWord64 n
			_ -> fail "wrong number"