{-# LANGUAGE ConstraintKinds  #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase       #-}
{-# LANGUAGE RankNTypes       #-}
{-# LANGUAGE TypeInType       #-}

module Language.LSP.Protocol.Message.Parsing where

import           Language.LSP.Protocol.Message.LspId
import           Language.LSP.Protocol.Message.Types
import           Language.LSP.Protocol.Message.Meta
import           Language.LSP.Protocol.Message.Method
import           Language.LSP.Protocol.Internal.Method

import           Data.Aeson
import           Data.Aeson.Types
import           Data.Function                     (on)
import           Data.GADT.Compare
import           Data.Kind
import           Data.Proxy
import           Data.Type.Equality
import           GHC.TypeLits                      (sameSymbol)

-- ---------------------------------------------------------------------
-- Working with arbitrary messages
-- ---------------------------------------------------------------------

data FromServerMessage' a where
  FromServerMess :: forall t (m :: Method ServerToClient t) a. SMethod m -> TMessage m -> FromServerMessage' a
  FromServerRsp  :: forall (m :: Method ClientToServer Request) a. a m -> TResponseMessage m -> FromServerMessage' a

type FromServerMessage = FromServerMessage' SMethod

instance Eq FromServerMessage where
  (==) = (==) `on` toJSON
instance Show FromServerMessage where
  show = show . toJSON

instance ToJSON FromServerMessage where
  toJSON (FromServerMess m p) = serverMethodJSON m (toJSON p)
  toJSON (FromServerRsp m p)  = clientResponseJSON m (toJSON p)

fromServerNot :: forall (m :: Method ServerToClient Notification).
  TMessage m ~ TNotificationMessage m => TNotificationMessage m -> FromServerMessage
fromServerNot m@TNotificationMessage{_method=meth} = FromServerMess meth m

fromServerReq :: forall (m :: Method ServerToClient Request).
  TMessage m ~ TRequestMessage m => TRequestMessage m -> FromServerMessage
fromServerReq m@TRequestMessage{_method=meth} = FromServerMess meth m

data FromClientMessage' a where
  FromClientMess :: forall t (m :: Method ClientToServer t) a . SMethod m -> TMessage m -> FromClientMessage' a
  FromClientRsp  :: forall (m :: Method ServerToClient Request) a . a m -> TResponseMessage m -> FromClientMessage' a

type FromClientMessage = FromClientMessage' SMethod

instance ToJSON FromClientMessage where
  toJSON (FromClientMess m p) = clientMethodJSON m (toJSON p)
  toJSON (FromClientRsp m p)  = serverResponseJSON m (toJSON p)

fromClientNot :: forall (m :: Method ClientToServer Notification).
  TMessage m ~ TNotificationMessage m => TNotificationMessage m -> FromClientMessage
fromClientNot m@TNotificationMessage{_method=meth} = FromClientMess meth m

fromClientReq :: forall (m :: Method ClientToServer Request).
  TMessage m ~ TRequestMessage m => TRequestMessage m -> FromClientMessage
fromClientReq m@TRequestMessage{_method=meth} = FromClientMess meth m

-- ---------------------------------------------------------------------
-- Parsing
-- ---------------------------------------------------------------------

type LookupFunc f a = forall (m :: Method f Request). LspId m -> Maybe (SMethod m, a m)

{-
Message Types we must handle are the following

Request      | jsonrpc | id | method | params?
Response     | jsonrpc | id |        |         | response? | error?
Notification | jsonrpc |    | method | params?
-}

{-# INLINE parseServerMessage #-}
parseServerMessage :: LookupFunc ClientToServer a -> Value -> Parser (FromServerMessage' a)
parseServerMessage lookupId v@(Object o) = do
  methMaybe <- o .:! "method"
  idMaybe <- o .:! "id"
  case methMaybe of
    -- Request or Notification
    Just (SomeServerMethod m) ->
      case splitServerMethod m of
        IsServerNot -> FromServerMess m <$> parseJSON v
        IsServerReq -> FromServerMess m <$> parseJSON v
        IsServerEither | SMethod_CustomMethod (p :: Proxy s') <- m -> do
          case idMaybe of
            -- Request
            Just _ ->
              let m' = (SMethod_CustomMethod p :: SMethod (Method_CustomMethod s' :: Method ServerToClient Request))
              in FromServerMess m' <$> parseJSON v
            Nothing ->
              let m' = (SMethod_CustomMethod p :: SMethod (Method_CustomMethod s' :: Method ServerToClient Notification))
              in FromServerMess m' <$> parseJSON v
    Nothing -> do
      case idMaybe of
        Just i -> do
          case lookupId i of
            Just (m,res) -> clientResponseJSON m $ FromServerRsp res <$> parseJSON v
            Nothing -> fail $ unwords ["Failed in looking up response type of", show v]
        Nothing -> fail $ unwords ["Got unexpected message without method or id"]
parseServerMessage _ v = fail $ unwords ["parseServerMessage expected object, got:",show v]

{-# INLINE parseClientMessage #-}
parseClientMessage :: LookupFunc ServerToClient a -> Value -> Parser (FromClientMessage' a)
parseClientMessage lookupId v@(Object o) = do
  methMaybe <- o .:! "method"
  idMaybe <- o .:! "id"
  case methMaybe of
    -- Request or Notification
    Just (SomeClientMethod m) ->
      case splitClientMethod m of
        IsClientNot -> FromClientMess m <$> parseJSON v
        IsClientReq -> FromClientMess m <$> parseJSON v
        IsClientEither | SMethod_CustomMethod (p :: Proxy s') <- m -> do
          case idMaybe of
            -- Request
            Just _ ->
              let m' = (SMethod_CustomMethod p :: SMethod (Method_CustomMethod s' :: Method ClientToServer Request))
              in FromClientMess m' <$> parseJSON v
            Nothing ->
              let m' = (SMethod_CustomMethod p :: SMethod (Method_CustomMethod s' :: Method ClientToServer Notification))
              in FromClientMess m' <$> parseJSON v
    Nothing -> do
      case idMaybe of
        Just i -> do
          case lookupId i of
            Just (m,res) -> serverResponseJSON m $ FromClientRsp res <$> parseJSON v
            Nothing -> fail $ unwords ["Failed in looking up response type of", show v]
        Nothing -> fail $ unwords ["Got unexpected message without method or id"]
parseClientMessage _ v = fail $ unwords ["parseClientMessage expected object, got:",show v]

-- ---------------------------------------------------------------------
-- Helper Utilities
-- ---------------------------------------------------------------------

{-# INLINE clientResponseJSON #-}
clientResponseJSON :: SClientMethod m -> (HasJSON (TResponseMessage m) => x) -> x
clientResponseJSON m x = case splitClientMethod m of
  IsClientReq    -> x
  IsClientEither -> x

{-# INLINE serverResponseJSON #-}
serverResponseJSON :: SServerMethod m -> (HasJSON (TResponseMessage m) => x) -> x
serverResponseJSON m x = case splitServerMethod m of
  IsServerReq    -> x
  IsServerEither -> x

{-# INLINE clientMethodJSON#-}
clientMethodJSON :: SClientMethod m -> (ToJSON (TClientMessage m) => x) -> x
clientMethodJSON m x =
  case splitClientMethod m of
    IsClientNot    -> x
    IsClientReq    -> x
    IsClientEither -> x

{-# INLINE serverMethodJSON #-}
serverMethodJSON :: SServerMethod m -> (ToJSON (TServerMessage m) => x) -> x
serverMethodJSON m x =
  case splitServerMethod m of
    IsServerNot    -> x
    IsServerReq    -> x
    IsServerEither -> x

type HasJSON a = (ToJSON a,FromJSON a,Eq a)

-- Reify universal properties about Client/Server Messages

type ClientNotOrReq :: forall t . Method ClientToServer t -> Type
data ClientNotOrReq m where
  IsClientNot
    :: ( HasJSON (TClientMessage m)
       , TMessage m ~ TNotificationMessage m)
    => ClientNotOrReq (m :: Method ClientToServer Notification)
  IsClientReq
    :: forall (m :: Method ClientToServer Request).
    ( HasJSON (TClientMessage m)
    , HasJSON (TResponseMessage m)
    , TMessage m ~ TRequestMessage m)
    => ClientNotOrReq m
  IsClientEither
    :: ClientNotOrReq (Method_CustomMethod s)

type ServerNotOrReq :: forall t . Method ServerToClient t -> Type
data ServerNotOrReq m where
  IsServerNot
    :: ( HasJSON (TServerMessage m)
       , TMessage m ~ TNotificationMessage m)
    => ServerNotOrReq (m :: Method ServerToClient Notification)
  IsServerReq
    :: forall (m :: Method ServerToClient Request).
    ( HasJSON (TServerMessage m)
    , HasJSON (TResponseMessage m)
    , TMessage m ~ TRequestMessage m)
    => ServerNotOrReq m
  IsServerEither
    :: ServerNotOrReq (Method_CustomMethod s)

{-# INLINE splitClientMethod #-}
splitClientMethod :: SClientMethod m -> ClientNotOrReq m
splitClientMethod = \case
  SMethod_Initialize                          -> IsClientReq
  SMethod_Initialized                         -> IsClientNot
  SMethod_Shutdown                            -> IsClientReq
  SMethod_Exit                                -> IsClientNot
  SMethod_WorkspaceDidChangeWorkspaceFolders  -> IsClientNot
  SMethod_WorkspaceDidChangeConfiguration     -> IsClientNot
  SMethod_WorkspaceDidChangeWatchedFiles      -> IsClientNot
  SMethod_WorkspaceSymbol                     -> IsClientReq
  SMethod_WorkspaceExecuteCommand             -> IsClientReq
  SMethod_WindowWorkDoneProgressCancel        -> IsClientNot
  SMethod_TextDocumentDidOpen                 -> IsClientNot
  SMethod_TextDocumentDidChange               -> IsClientNot
  SMethod_TextDocumentWillSave                -> IsClientNot
  SMethod_TextDocumentWillSaveWaitUntil       -> IsClientReq
  SMethod_TextDocumentDidSave                 -> IsClientNot
  SMethod_TextDocumentDidClose                -> IsClientNot
  SMethod_TextDocumentCompletion              -> IsClientReq
  SMethod_TextDocumentHover                   -> IsClientReq
  SMethod_TextDocumentSignatureHelp           -> IsClientReq
  SMethod_TextDocumentDeclaration             -> IsClientReq
  SMethod_TextDocumentDefinition              -> IsClientReq
  SMethod_TextDocumentTypeDefinition          -> IsClientReq
  SMethod_TextDocumentImplementation          -> IsClientReq
  SMethod_TextDocumentReferences              -> IsClientReq
  SMethod_TextDocumentDocumentHighlight       -> IsClientReq
  SMethod_TextDocumentDocumentSymbol          -> IsClientReq
  SMethod_TextDocumentCodeAction              -> IsClientReq
  SMethod_TextDocumentCodeLens                -> IsClientReq
  SMethod_TextDocumentDocumentLink            -> IsClientReq
  SMethod_TextDocumentDocumentColor           -> IsClientReq
  SMethod_TextDocumentColorPresentation       -> IsClientReq
  SMethod_TextDocumentFormatting              -> IsClientReq
  SMethod_TextDocumentRangeFormatting         -> IsClientReq
  SMethod_TextDocumentOnTypeFormatting        -> IsClientReq
  SMethod_TextDocumentRename                  -> IsClientReq
  SMethod_TextDocumentPrepareRename           -> IsClientReq
  SMethod_TextDocumentFoldingRange            -> IsClientReq
  SMethod_TextDocumentSelectionRange          -> IsClientReq
  SMethod_TextDocumentPrepareCallHierarchy    -> IsClientReq
  SMethod_TextDocumentLinkedEditingRange      -> IsClientReq
  SMethod_CallHierarchyIncomingCalls          -> IsClientReq
  SMethod_CallHierarchyOutgoingCalls          -> IsClientReq
  SMethod_TextDocumentSemanticTokensFull      -> IsClientReq
  SMethod_TextDocumentSemanticTokensFullDelta -> IsClientReq
  SMethod_TextDocumentSemanticTokensRange     -> IsClientReq
  SMethod_WorkspaceWillCreateFiles            -> IsClientReq
  SMethod_WorkspaceWillDeleteFiles            -> IsClientReq
  SMethod_WorkspaceWillRenameFiles            -> IsClientReq
  SMethod_WorkspaceDidCreateFiles             -> IsClientNot
  SMethod_WorkspaceDidDeleteFiles             -> IsClientNot
  SMethod_WorkspaceDidRenameFiles             -> IsClientNot
  SMethod_TextDocumentMoniker                 -> IsClientReq
  SMethod_TextDocumentPrepareTypeHierarchy    -> IsClientReq
  SMethod_TypeHierarchySubtypes               -> IsClientReq
  SMethod_TypeHierarchySupertypes             -> IsClientReq
  SMethod_TextDocumentInlineValue             -> IsClientReq
  SMethod_TextDocumentInlayHint               -> IsClientReq
  SMethod_TextDocumentDiagnostic              -> IsClientReq
  SMethod_WorkspaceDiagnostic                 -> IsClientReq
  SMethod_CodeLensResolve                     -> IsClientReq
  SMethod_InlayHintResolve                    -> IsClientReq
  SMethod_CodeActionResolve                   -> IsClientReq
  SMethod_DocumentLinkResolve                 -> IsClientReq
  SMethod_CompletionItemResolve               -> IsClientReq
  SMethod_WorkspaceSymbolResolve              -> IsClientReq
  SMethod_NotebookDocumentDidChange           -> IsClientNot
  SMethod_NotebookDocumentDidClose            -> IsClientNot
  SMethod_NotebookDocumentDidOpen             -> IsClientNot
  SMethod_NotebookDocumentDidSave             -> IsClientNot
  SMethod_SetTrace                            -> IsClientNot
  SMethod_Progress                            -> IsClientNot
  SMethod_CancelRequest                       -> IsClientNot
  (SMethod_CustomMethod _)                    -> IsClientEither

{-# INLINE splitServerMethod #-}
splitServerMethod :: SServerMethod m -> ServerNotOrReq m
splitServerMethod = \case
  SMethod_WindowShowMessage              -> IsServerNot
  SMethod_WindowShowMessageRequest       -> IsServerReq
  SMethod_WindowShowDocument             -> IsServerReq
  SMethod_WindowLogMessage               -> IsServerNot
  SMethod_WindowWorkDoneProgressCreate   -> IsServerReq
  SMethod_Progress                       -> IsServerNot
  SMethod_TelemetryEvent                 -> IsServerNot
  SMethod_ClientRegisterCapability       -> IsServerReq
  SMethod_ClientUnregisterCapability     -> IsServerReq
  SMethod_WorkspaceWorkspaceFolders      -> IsServerReq
  SMethod_WorkspaceConfiguration         -> IsServerReq
  SMethod_WorkspaceApplyEdit             -> IsServerReq
  SMethod_TextDocumentPublishDiagnostics -> IsServerNot
  SMethod_LogTrace                       -> IsServerNot
  SMethod_CancelRequest                  -> IsServerNot
  SMethod_WorkspaceCodeLensRefresh       -> IsServerReq
  SMethod_WorkspaceSemanticTokensRefresh -> IsServerReq
  SMethod_WorkspaceInlineValueRefresh         -> IsServerReq
  SMethod_WorkspaceInlayHintRefresh           -> IsServerReq
  SMethod_WorkspaceDiagnosticRefresh          -> IsServerReq
  (SMethod_CustomMethod _)               -> IsServerEither

-- | Given a witness that two custom methods are of the same type, produce a witness that the methods are the same
data CustomEq m1 m2 where
  CustomEq
    :: (m1 ~ (Method_CustomMethod s :: Method f t1), m2 ~ (Method_CustomMethod s :: Method f t2))
    => { runCustomEq :: (t1 ~ t2 => m1 :~~: m2) }
    -> CustomEq m1 m2

runEq :: (t1 ~ t2)
      => (SMethod m1 -> SMethod m2 -> Maybe (Either (CustomEq m1 m2) (m1 :~~: m2)))
      -> SMethod (m1 :: Method f t1)
      -> SMethod (m2 :: Method f t2)
      -> Maybe (m1 :~~: m2)
runEq f m1 m2 = do
  res <- f m1 m2
  pure $ case res of
    Right eq -> eq
    Left ceq -> runCustomEq ceq

-- | Heterogeneous equality on singleton server methods
mEqServer :: SServerMethod m1 -> SServerMethod m2 -> Maybe (Either (CustomEq m1 m2) (m1 :~~: m2))
mEqServer m1 m2 = go (splitServerMethod m1) (splitServerMethod m2)
  where
    go IsServerNot IsServerNot = do
      Refl <- geq m1 m2
      pure $ Right HRefl
    go IsServerReq IsServerReq = do
      Refl <- geq m1 m2
      pure $ Right HRefl
    go IsServerEither IsServerEither
      | SMethod_CustomMethod p1 <- m1
      , SMethod_CustomMethod p2 <- m2
      = case sameSymbol p1 p2 of
          Just Refl -> Just $ Left $ CustomEq HRefl
          _         -> Nothing
    go _ _ = Nothing

-- | Heterogeneous equality on singleton client methods
mEqClient :: SClientMethod m1 -> SClientMethod m2 -> Maybe (Either (CustomEq m1 m2) (m1 :~~: m2))
mEqClient m1 m2 = go (splitClientMethod m1) (splitClientMethod m2)
  where
    go IsClientNot IsClientNot = do
      Refl <- geq m1 m2
      pure $ Right HRefl
    go IsClientReq IsClientReq = do
      Refl <- geq m1 m2
      pure $ Right HRefl
    go IsClientEither IsClientEither
      | SMethod_CustomMethod p1 <- m1
      , SMethod_CustomMethod p2 <- m2
      = case sameSymbol p1 p2 of
          Just Refl -> Just $ Left $ CustomEq HRefl
          _         -> Nothing
    go _ _ = Nothing
