{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DerivingStrategies             #-}
{-# LANGUAGE DeriveLift             #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeApplications      #-}
{-|
This defines the types of the LSP "metamodel", which is a machine-readable format specifying the
types used in the LSP protocol.

The type system is quite typescript-y, which isn't surprising given that the whole protocol is
very typescript-y.

A typescript version of the metamodel types can be found here, which is useful for constructing
this Haskell model of them:
https://github.com/microsoft/vscode-languageserver-node/blob/main/tools/src/metaModel.ts
-}
module Language.LSP.MetaModel.Types where

import           Data.Aeson         hiding (Null, String)
import qualified Data.Aeson         as JSON
import qualified Data.Aeson.TH      as JSON
import qualified Data.Char          as Char
import           Data.Text          (Text)

import           Control.Lens
import           Control.Monad.IO.Class
import qualified Data.List.NonEmpty as NE

import Language.Haskell.TH.Syntax (Lift(..), Q, Exp, addDependentFile)

-- | What direction is this message sent in: server to client, client to server, or both?
data MessageDirection = ServerToClient | ClientToServer | Both
  deriving stock (Show, Eq, Ord, Lift)

instance ToJSON MessageDirection where
  toJSON ServerToClient = toJSON @String "serverToClient"
  toJSON ClientToServer = toJSON @String "clientToServer"
  toJSON Both           = toJSON @String "both"

instance FromJSON MessageDirection where
  parseJSON = withText "MessageDirection" $ \case
    "serverToClient" -> pure ServerToClient
    "clientToServer" -> pure ClientToServer
    "both"           -> pure Both
    t                -> fail $ "unknown message direction " ++ show t

-- | The "base types" in the metamodel.
data BaseTypeName = URI | DocumentUri | Integer | UInteger | Decimal | RegExp | String | Boolean | Null
  deriving stock (Show, Eq, Ord, Lift)

-- | A property of a structure.
data Property = Property
  { name          :: Text
  , type_         :: Type
  , optional      :: Maybe Bool
  , documentation :: Maybe Text
  , since         :: Maybe Text
  , proposed      :: Maybe Bool
  , deprecated    :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | An anonymous structure type.
data StructureLiteral = StructureLiteral
  { properties    :: [Property]
  , documentation :: Maybe Text
  , since         :: Maybe Text
  , proposed      :: Maybe Bool
  , deprecated    :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | The various kinds of type in the metamodel.
data Type =
  BaseType { btName :: BaseTypeName }
  | ReferenceType { rtName :: Text }
  | ArrayType { atElement :: Type }
  | MapType { mKey :: Type, mValue :: Type }
  | AndType { aItems :: NE.NonEmpty Type }
  | OrType { oItems :: NE.NonEmpty Type }
  | TupleType { tItems :: [Type] }
  | StructureLiteralType { stlValue :: StructureLiteral }
  | StringLiteralType { slValue :: Text }
  | IntegerLiteralType { ilValue :: Integer }
  | BooleanLiteralType { blValue :: Bool }
  deriving stock (Show, Eq, Ord, Lift)

-- | A request message.
data Request = Request
  { method              :: Text
  , params              :: Maybe Type -- typescript says it can be [Type], but it never is so whatever
  , result              :: Type
  , partialResult       :: Maybe Type
  , errorData           :: Maybe Type
  , registrationOptions :: Maybe Type
  , messageDirection    :: MessageDirection
  , documentation       :: Maybe Text
  , since               :: Maybe Text
  , proposed            :: Maybe Bool
  , deprecated          :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | A notification message.
data Notification = Notification
  { method              :: Text
  , params              :: Maybe Type
  , registrationOptions :: Maybe Type
  , messageDirection    :: MessageDirection
  , documentation       :: Maybe Text
  , since               :: Maybe Text
  , proposed            :: Maybe Bool
  , deprecated          :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | A structure type.
data Structure = Structure
  { name          :: Text
  , extends       :: Maybe [Type]
  , mixins        :: Maybe [Type]
  , properties    :: [Property]
  , documentation :: Maybe Text
  , since         :: Maybe Text
  , proposed      :: Maybe Bool
  , deprecated    :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | A type alias.
data TypeAlias = TypeAlias
  { name          :: Text
  , type_         :: Type
  , documentation :: Maybe Text
  , since         :: Maybe Text
  , proposed      :: Maybe Bool
  , deprecated    :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | This is just 'string | int' on the typescript side, but
-- it's convenient to have a proper type here.
data TextOrInteger = T Text | I Integer
  deriving stock (Show, Eq, Ord, Lift)

-- | An entry in an enumeration.
data EnumerationEntry = EnumerationEntry
  { name          :: Text
  , value         :: TextOrInteger
  , documentation :: Maybe Text
  , since         :: Maybe Text
  , proposed      :: Maybe Bool
  , deprecated    :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | An enumeration type.
data Enumeration = Enumeration
  { name                 :: Text
  , type_                :: Type
  , values               :: [EnumerationEntry]
  , supportsCustomValues :: Maybe Bool
  , documentation        :: Maybe Text
  , since                :: Maybe Text
  , proposed             :: Maybe Bool
  , deprecated           :: Maybe Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | Metadata about the metamodel iteslf.
data MetaData = MetaData
  { version :: Text
  }
  deriving stock (Show, Eq, Ord, Lift)

-- | The entire metamodel.
data MetaModel = MetaModel
  { metaData      :: MetaData
  , requests      :: [Request]
  , notifications :: [Notification]
  , structures    :: [Structure]
  , enumerations  :: [Enumeration]
  , typeAliases   :: [TypeAlias]
  }
  deriving stock (Show, Eq, Ord, Lift)

-- We need to do some massaging to make sure that we get the right aeson instances for
-- these types and can actually parse the incoming data!
$(
  let
      -- "type" is a very common field name, we use "type_" on the Haskell side
      defOpts = defaultOptions{fieldLabelModifier = \case { "type_" -> "type"; x -> x; }}

      propertyInst = JSON.deriveJSON defOpts ''Property
      slInst = JSON.deriveJSON defOpts ''StructureLiteral

      -- 'BaseType' is a union of strings, so we encode it as an untagged sum with some
      -- mangling of the constructor names
      baseTyNameToTag :: String -> String
      baseTyNameToTag = \case
        "Integer"  -> "integer"
        "UInteger" -> "uinteger"
        "Decimal"  -> "decimal"
        "String"   -> "string"
        "Boolean"  -> "boolean"
        "Null"     -> "null"
        x          -> x
      baseTyNameInst = JSON.deriveJSON (defOpts{sumEncoding=JSON.UntaggedValue, constructorTagModifier=baseTyNameToTag}) ''BaseTypeName

      -- 'Type' is a *tagged* union, but the tag is a string field (sigh), fortunately
      -- aeson can deal with this. Also needs some constructor mangling.
      typeToTag :: String -> String
      typeToTag = \case
        "BaseType"             -> "base"
        "ReferenceType"        -> "reference"
        "ArrayType"            -> "array"
        "MapType"              -> "map"
        "AndType"              -> "and"
        "OrType"               -> "or"
        "TupleType"            -> "tuple"
        "StructureLiteralType" -> "literal"
        "StringLiteralType"    -> "stringLiteral"
        "IntegerLiteralType"   -> "integerLiteral"
        "BooleanLiteralType"   -> "booleanLiteral"
        x                      -> x
      typeOpts = defOpts
        { sumEncoding=JSON.defaultTaggedObject{tagFieldName="kind"}
        , constructorTagModifier=typeToTag
        , fieldLabelModifier= \s -> over _head Char.toLower $ Prelude.dropWhile Char.isLower s
        }
      typeInst = JSON.deriveJSON typeOpts ''Type

      -- The rest are mostly normal
      reqInst = JSON.deriveJSON defOpts ''Request
      notInst = JSON.deriveJSON defOpts ''Notification
      sInst = JSON.deriveJSON defOpts ''Structure
      taInst = JSON.deriveJSON defOpts ''TypeAlias
      -- TextOrInteger is also an untagged sum
      tiInst = JSON.deriveJSON (defOpts{sumEncoding=UntaggedValue}) ''TextOrInteger
      eeInst = JSON.deriveJSON defOpts ''EnumerationEntry
      eInst = JSON.deriveJSON defOpts ''Enumeration
      mdInst = JSON.deriveJSON defOpts ''MetaData
      mmInst = JSON.deriveJSON defOpts ''MetaModel
  in mconcat <$> sequence [ propertyInst, slInst, baseTyNameInst, typeInst, reqInst, notInst, sInst, taInst, tiInst, eeInst, eInst, mdInst, mmInst ]
 )

loadMetaModelFromFile :: FilePath -> Q Exp
loadMetaModelFromFile fp = do
  addDependentFile fp
  res <- liftIO $ JSON.eitherDecodeFileStrict' fp
  case res of
    Left e   -> fail e
    Right (mm :: MetaModel) -> lift mm
