/-
Copyright (c) 2022 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

Authors: E.W.Ayers
-/
import Lean.Widget.Basic
import Lean.Data.Json
import Lean.Environment
import Lean.Server

open Lean

namespace Lean.Widget

/-- A custom piece of code that is run on the editor client.

The editor can use the `Lean.Widget.getWidgetSource` RPC method to
get this object.

See the [tutorial](doc/widgets.md) above this declaration for more information on
how to use the widgets system.

-/
structure WidgetSource where
  /-- Unique identifier for the widget. -/
  widgetSourceId : Name
  /-- Sourcetext of the code to run.-/
  sourcetext : String
  hash : String := toString <| hash sourcetext
  deriving Inhabited, ToJson, FromJson

namespace WidgetSource

builtin_initialize widgetSourceRegistry : MapDeclarationExtension WidgetSource ← mkMapDeclarationExtension `widgetSourceRegistry

private unsafe def attributeImplUnsafe : AttributeImpl where
  name := `widgetSource
  descr := "Mark a string as static code that can be loaded by a widget handler."
  applicationTime := AttributeApplicationTime.afterCompilation
  add decl _stx _kind := do
    let env ← getEnv
    let value ← evalConstCheck String ``String decl
    setEnv <| widgetSourceRegistry.insert env decl {widgetSourceId := decl, sourcetext := value}

@[implementedBy attributeImplUnsafe]
opaque attributeImpl : AttributeImpl

/-- Find the WidgetSource for given widget id. -/
protected def find? (env : Environment) (id : Name) : Option WidgetSource :=
  widgetSourceRegistry.find? env id

/-- Returns true if the environment contains the given widget id. -/
protected def contains (env : Environment) (id : Name) : Bool :=
  widgetSourceRegistry.contains env id

open Lean.Server in
/-- Gets the hash of the static javascript string for the given widget id, or throws if
there is no static javascript registered. -/
def getHash [Monad m] [MonadExcept RequestError m] (env : Environment) (id : Name) : m String := do
  let some j := WidgetSource.find? env id | throw $ RequestError.internalError s!"getHash: No code found for {id}."
  return j.hash

builtin_initialize registerBuiltinAttribute attributeImpl

end WidgetSource

structure GetWidgetSourceParams where
  widgetSourceId : Name
  pos : Lean.Lsp.TextDocumentPositionParams
  deriving ToJson, FromJson

open Lean.Server Lean

open RequestM in
@[serverRpcMethod]
def getWidgetSource (args : GetWidgetSourceParams) : RequestM (RequestTask WidgetSource) :=
  RequestM.withWaitFindSnapAtPos args.pos fun snap => do
    let env := snap.cmdState.env
    if let some w := WidgetSource.find? env args.widgetSourceId then
      return w
    else
      throw  (RequestError.mk JsonRpc.ErrorCode.invalidParams s!"No registered user-widget with name {args.widgetSourceId}")

open Lean Elab

/--
  Try to retrieve the `UserWidgetInfo` at a particular position.
-/
def widgetInfoAt? (text : FileMap) (t : InfoTree) (hoverPos : String.Pos) : List UserWidgetInfo :=
  t.deepestNodes fun
    | _ctx, i@(Info.ofUserWidgetInfo wi), _cs => do
      if let (some pos, some tailPos) := (i.pos?, i.tailPos?) then
        let trailSize := i.stx.getTrailingSize
        -- show info at EOF even if strictly outside token + trail
        let atEOF := tailPos.byteIdx + trailSize == text.source.endPos.byteIdx
        guard <| pos ≤ hoverPos ∧ (hoverPos.byteIdx < tailPos.byteIdx + trailSize || atEOF)
        return wi
      else
        failure
    | _, _, _ => none

structure UserWidget where
  widgetSourceId : Name
  hash : String
  props : Json
  range? : Option Lsp.Range
  deriving ToJson, FromJson

structure GetWidgetsResponse where
  widgets : Array UserWidget
  deriving ToJson, FromJson

open RequestM in
/-- Get the `UserWidget`s present at a particular position. -/
@[serverRpcMethod]
def getWidgets (args : Lean.Lsp.TextDocumentPositionParams) : RequestM (RequestTask (GetWidgetsResponse)) := do
  let doc ← readDoc
  let filemap := doc.meta.text
  let pos := filemap.lspPosToUtf8Pos args.position
  withWaitFindSnapAtPos args fun snap => do
      let env := snap.env
      let ws := widgetInfoAt? filemap snap.infoTree pos
      let ws ← ws.toArray.mapM (fun w => do
        let hash ← WidgetSource.getHash env w.widgetSourceId
        return {
          widgetSourceId := w.widgetSourceId,
          hash := hash,
          props := w.props,
          range? := String.Range.toLspRange filemap <$> Syntax.getRange? w.stx,
        })
      return {widgets := ws}

/-- Save a user-widget instance to the infotree. -/
def saveWidgetInfo [Monad m] [MonadEnv m] [MonadError m] [MonadInfoTree m] (widgetSourceId : Name) (props : Json) (stx : Syntax):  m Unit := do
  let info := Info.ofUserWidgetInfo {
    widgetSourceId := widgetSourceId,
    props := props,
    stx := stx,
  }
  pushInfoLeaf info

/-!
# Widget command

Use this to place a widget. Useful for debugging widgets.
-/

syntax (name := widgetCmd) "#widget " ident term : command

private unsafe def  evalJsonUnsafe (stx : Syntax) : TermElabM Json := do
  let e ← Term.elabTerm stx (mkConst ``Json)
  let e ← Meta.instantiateMVars e
  Term.evalExpr Json ``Json e

@[implementedBy evalJsonUnsafe]
private opaque evalJson (stx : Syntax) : TermElabM Json

open Elab Command in
@[commandElab widgetCmd] def elabWidgetCmd : CommandElab := fun
  | stx@`(#widget $id:ident $props) => do
    let props : Json ← runTermElabM none (fun _ => evalJson props)
    saveWidgetInfo id.getId props stx
    return ()
  | _ => throwUnsupportedSyntax


end Lean.Widget
