/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: David Thrane Christiansen
-/
prelude
import Lean.Elab.Do

/-!
This module contains helpers used for formatting hovers according to docstring conventions. Links to
the reference manual are updated in the module Lean.DocString.Links, because that functionality is
not specific to hovers.
-/

set_option linter.missingDocs true

namespace Lean.Server.FileWorker.Hover

/--
Adds a comment marker `-- ` to a line in an output code block, respecting indentation, if the line
contains any non-space characters. Lines containing only spaces are returned unmodified.

The comment marker is always added at the indicated indentation. If the content of the line is at
least as indented, then its relative indentation is preserved. Otherwise, it's placed just after the
line comment marker.
-/
private def addCommentAt (indent : Nat) (line : String) : String := Id.run do
  let s := "".pushn ' ' indent ++ "-- "
  let mut iter := line.iter
  for _i in [0:indent] do
    if h : iter.hasNext then
      if iter.curr' h == ' ' then
        iter := iter.next' h
      else
        -- Non-whitespace found *before* the indentation level. This output should be indented
        -- as if it were exactly at the indentation level.
        break
    else
      -- The line was entirely ' ', and was shorter than the indentation level. No `--` added.
      return line
  let remaining := iter.remainingToString
  if remaining.all (· == ' ') then
    return line
  else
    return s ++ remaining

/--
Splits a string into lines, preserving newline characters.
-/
private def lines (s : String) : Array String := Id.run do
  let mut result := #[]
  let mut lineStart := s.iter
  let mut iter := lineStart
  while h : iter.hasNext do
    let c := iter.curr' h
    iter := iter.next' h
    if c == '\n' then
      result := result.push <| lineStart.extract iter
      lineStart := iter

  if iter != lineStart then
    result := result.push <| lineStart.extract iter
  return result

private inductive RWState where
  /-- Not in a code block -/
  | normal
  /-- In a non-`output` code block opened with `ticks` backtick characters -/
  | nonOutput (ticks : Nat)
  /-- In an `output` code block indented `indent` columns opened with `ticks` backtick characters -/
  | output (indent : Nat) (ticks : Nat)

/--
Rewrites examples in docstring Markdown for output as hover Markdown.

In particular, code blocks with the `output` class have line comment markers inserted. Editors do
not typically distinguish between code block classes, so some other visual indication is needed to
separate them. This function is not based on a compliant Markdown parser and may give unpredictable
results when used with invalid Markdown.
-/
def rewriteExamples (docstring : String) : String := Id.run do
  let lines := lines docstring
  let mut result : String := ""
  -- The current state, which tracks the context of the line being processed
  let mut inOutput : RWState := .normal
  for l in lines do
    let indent := l.takeWhile (· == ' ') |>.length
    let mut l' := l.trimLeft
    -- Is this a code block fence?
    if l'.startsWith "```" then
      let count := l'.takeWhile (· == '`') |>.length
      l' := l'.dropWhile (· == '`')
      l' := l'.dropWhile (· == ' ')
      match inOutput with
      | .normal =>
        if l'.startsWith "output" then
          inOutput := .output indent count
        else
          inOutput := .nonOutput count
        result := result ++ l
      | .output i c =>
        if c == count then
          inOutput := .normal
          result := result ++ l
        else
          result := result ++ addCommentAt i l
      | .nonOutput c =>
        if c == count then
          inOutput := .normal
        result := result ++ l
    else -- Current line is not a fence ("```")
      match inOutput with
      | .output i _c =>
        -- append whitespace and comment marker
        result := result ++ addCommentAt i l
      | _ => -- Not in an output code block
        result := result ++ l
  return result
