// ƿit Language Server Protocol (LSP) main loop.
// Communicates via JSON-RPC over stdin/stdout.

var fd = use('fd')
var json_mod = use('json')
var protocol = use('protocol')
var analysis_make = use('analysis')
var completions = use('completions')
var hover_mod = use('hover')
var symbols = use('symbols')

// Get tokenize_mod and parse_mod from the environment.
// These are the same functions the compiler uses internally.
var tokenize_mod = use('tokenize')
var parse_mod = use('parse')
var index_mod = use('index')
var explain_mod = use('explain')

// Create analysis module bound to tokenize/parse/index
var analysis = analysis_make(tokenize_mod, parse_mod, index_mod)

// Document store: URI -> {text, version, ast, tokens, errors}
var docs = {}

// Log to stderr for debugging (does not interfere with protocol).
var log = function(msg) {
  fd.write(2, `[pit-lsp] ${msg}\n`)
}

// Publish diagnostics for a document.
var publish_diagnostics = function(uri, doc) {
  var diags = analysis.diagnostics(doc)
  protocol.notify("textDocument/publishDiagnostics", {
    uri: uri,
    diagnostics: diags
  })
}

// Parse a document and publish diagnostics.
var parse_and_notify = function(uri, src, version) {
  var doc = analysis.update(docs, uri, {src: src, version: version})
  publish_diagnostics(uri, doc)
}

// Handle initialize request.
var handle_initialize = function(id, params) {
  protocol.respond(id, {
    capabilities: {
      textDocumentSync: {
        openClose: true,
        change: 1,
        save: {includeText: true}
      },
      completionProvider: {
        triggerCharacters: [".", "$"]
      },
      hoverProvider: true,
      definitionProvider: true,
      documentSymbolProvider: true,
      referencesProvider: true,
      renameProvider: {prepareProvider: true}
    },
    serverInfo: {
      name: "pit-lsp",
      version: "0.1.0"
    }
  })
}

// Handle textDocument/didOpen notification.
var handle_did_open = function(params) {
  var td = params.textDocument
  parse_and_notify(td.uri, td.text, td.version)
}

// Handle textDocument/didChange notification (full text sync).
var handle_did_change = function(params) {
  var td = params.textDocument
  var changes = params.contentChanges
  if (length(changes) > 0) {
    parse_and_notify(td.uri, changes[0].text, td.version)
  }
}

// Handle textDocument/didClose notification.
var handle_did_close = function(params) {
  var uri = params.textDocument.uri
  analysis.remove(docs, uri)
  // Clear diagnostics
  protocol.notify("textDocument/publishDiagnostics", {
    uri: uri,
    diagnostics: []
  })
}

// Handle textDocument/didSave notification.
var handle_did_save = function(params) {
  var td = params.textDocument
  if (params.text != null) {
    parse_and_notify(td.uri, params.text, td.version)
  }
}

// Handle textDocument/completion request.
var handle_completion = function(id, params) {
  var uri = params.textDocument.uri
  var pos = params.position
  var doc = docs[uri]
  var items = []
  if (doc != null) {
    items = completions.complete(doc, pos.line, pos.character)
  }
  protocol.respond(id, items)
}

// Handle textDocument/hover request.
var handle_hover = function(id, params) {
  var uri = params.textDocument.uri
  var pos = params.position
  var doc = docs[uri]
  var result = null
  if (doc != null) {
    result = hover_mod.hover(doc, pos.line, pos.character, analysis.token_at)
  }
  protocol.respond(id, result)
}

// Handle textDocument/definition request.
var handle_definition = function(id, params) {
  var uri = params.textDocument.uri
  var pos = params.position
  var doc = docs[uri]
  var result = null
  if (doc != null) {
    result = symbols.definition(doc, pos.line, pos.character, analysis.token_at)
  }
  protocol.respond(id, result)
}

// Handle textDocument/documentSymbol request.
var handle_document_symbol = function(id, params) {
  var uri = params.textDocument.uri
  var doc = docs[uri]
  var result = []
  if (doc != null) {
    result = symbols.document_symbols(doc)
  }
  protocol.respond(id, result)
}

// Handle textDocument/references request.
var handle_references = function(id, params) {
  var uri = params.textDocument.uri
  var pos = params.position
  var doc = docs[uri]
  var result = []
  var tok = null
  var name = null
  var refs = null
  var _i = 0
  var ref = null
  var expl = null
  var sym_result = null
  if (doc != null && doc.index != null) {
    tok = analysis.token_at(doc, pos.line, pos.character)
    if (tok != null && tok.kind == "name" && tok.value != null) {
      name = tok.value
      refs = doc.index.reverse_refs[name]
      if (refs != null) {
        _i = 0
        while (_i < length(refs)) {
          ref = refs[_i]
          if (ref.span != null) {
            result[] = {
              uri: uri,
              range: {
                start: {line: ref.span.from_row, character: ref.span.from_col},
                end: {line: ref.span.to_row, character: ref.span.to_col}
              }
            }
          }
          _i = _i + 1
        }
      }
      // Also include the declaration itself if found
      expl = explain_mod.make(doc.index)
      sym_result = expl.by_symbol(name)
      if (sym_result != null && length(sym_result.symbols) > 0) {
        _i = 0
        while (_i < length(sym_result.symbols)) {
          if (sym_result.symbols[_i].decl_span != null) {
            result[] = {
              uri: uri,
              range: {
                start: {line: sym_result.symbols[_i].decl_span.from_row, character: sym_result.symbols[_i].decl_span.from_col},
                end: {line: sym_result.symbols[_i].decl_span.to_row, character: sym_result.symbols[_i].decl_span.to_col}
              }
            }
          }
          _i = _i + 1
        }
      }
    }
  }
  protocol.respond(id, result)
}

// Handle textDocument/prepareRename request.
var handle_prepare_rename = function(id, params) {
  var uri = params.textDocument.uri
  var pos = params.position
  var doc = docs[uri]
  var tok = null
  var name = null
  var result = null
  var expl = null
  var sym_result = null
  if (doc != null) {
    tok = analysis.token_at(doc, pos.line, pos.character)
    if (tok != null && tok.kind == "name" && tok.value != null) {
      name = tok.value
      // Don't allow renaming intrinsics
      if (doc.index != null) {
        expl = explain_mod.make(doc.index)
        sym_result = expl.by_symbol(name)
        if (sym_result != null && length(sym_result.symbols) > 0) {
          result = {
            range: {
              start: {line: tok.from_row, character: tok.from_column},
              end: {line: tok.to_row, character: tok.to_column}
            },
            placeholder: name
          }
        }
      }
    }
  }
  protocol.respond(id, result)
}

// Handle textDocument/rename request.
var handle_rename = function(id, params) {
  var uri = params.textDocument.uri
  var pos = params.position
  var new_name = params.newName
  var doc = docs[uri]
  var tok = null
  var name = null
  var edits = []
  var refs = null
  var _i = 0
  var ref = null
  var expl = null
  var sym_result = null
  if (doc != null && doc.index != null) {
    tok = analysis.token_at(doc, pos.line, pos.character)
    if (tok != null && tok.kind == "name" && tok.value != null) {
      name = tok.value
      expl = explain_mod.make(doc.index)
      sym_result = expl.by_symbol(name)
      // Add edit for declaration
      if (sym_result != null && length(sym_result.symbols) > 0) {
        _i = 0
        while (_i < length(sym_result.symbols)) {
          if (sym_result.symbols[_i].decl_span != null) {
            edits[] = {
              range: {
                start: {line: sym_result.symbols[_i].decl_span.from_row, character: sym_result.symbols[_i].decl_span.from_col},
                end: {line: sym_result.symbols[_i].decl_span.to_row, character: sym_result.symbols[_i].decl_span.to_col}
              },
              newText: new_name
            }
          }
          _i = _i + 1
        }
      }
      // Add edits for all references
      refs = doc.index.reverse_refs[name]
      if (refs != null) {
        _i = 0
        while (_i < length(refs)) {
          ref = refs[_i]
          if (ref.span != null) {
            edits[] = {
              range: {
                start: {line: ref.span.from_row, character: ref.span.from_col},
                end: {line: ref.span.to_row, character: ref.span.to_col}
              },
              newText: new_name
            }
          }
          _i = _i + 1
        }
      }
    }
  }
  var changes = {}
  if (length(edits) > 0) {
    changes[uri] = edits
  }
  protocol.respond(id, {changes: changes})
}

// Dispatch a single message. Wrapped in a function for disruption handling.
var dispatch_message = function(msg) {
  var method = msg.method
  if (method == "initialize") {
    handle_initialize(msg.id, msg.params)
  } else if (method == "initialized") {
    // no-op
  } else if (method == "textDocument/didOpen") {
    handle_did_open(msg.params)
  } else if (method == "textDocument/didChange") {
    handle_did_change(msg.params)
  } else if (method == "textDocument/didClose") {
    handle_did_close(msg.params)
  } else if (method == "textDocument/didSave") {
    handle_did_save(msg.params)
  } else if (method == "textDocument/completion") {
    handle_completion(msg.id, msg.params)
  } else if (method == "textDocument/hover") {
    handle_hover(msg.id, msg.params)
  } else if (method == "textDocument/definition") {
    handle_definition(msg.id, msg.params)
  } else if (method == "textDocument/documentSymbol") {
    handle_document_symbol(msg.id, msg.params)
  } else if (method == "textDocument/references") {
    handle_references(msg.id, msg.params)
  } else if (method == "textDocument/prepareRename") {
    handle_prepare_rename(msg.id, msg.params)
  } else if (method == "textDocument/rename") {
    handle_rename(msg.id, msg.params)
  } else if (method == "shutdown") {
    protocol.respond(msg.id, null)
    return "shutdown"
  } else if (method == "exit") {
    return "exit"
  } else {
    if (msg.id != null) {
      protocol.respond_error(msg.id, -32601, `Method not found: ${method}`)
    }
  }
  return null
} disruption {
  log(`error handling ${msg.method}`)
  if (msg.id != null) {
    protocol.respond_error(msg.id, -32603, `Internal error handling ${msg.method}`)
  }
  return null
}

// Main loop.
log("starting")

var running = true
var msg = null
var result = null

while (running) {
  msg = protocol.read_message()
  if (msg == null) {
    running = false
    break
  }

  result = dispatch_message(msg)
  if (result == "exit") {
    running = false
  }
}

log("stopped")
