require "../protocol"

module Lattice
  # Emits code lens items for a Crystal source file.
  #
  # Two categories of lens are produced:
  #
  # - **Run spec** — one lens per `it "..."` / `describe "..."` / `context "..."`
  #   block in spec files (files whose URI contains `/spec/` or ends with
  #   `_spec.cr`).
  # - **Run file** — a single lens on the first line of any spec file, offering
  #   a one-click way to run the whole file via `crystal spec <file>`.
  #
  # The lenses use the `lattice.runSpec` command which the companion Zed
  # extension (or VS Code client) must register.  If the client does not
  # support the command the lenses are visible but inert.
  class CodeLensProvider
    SPEC_PATTERN = /(?:\/spec\/|_spec\.cr$)/

    # Return code lenses for *uri* / *source*.
    def lenses(uri : String, source : String) : Array(::LSP::CodeLens)
      result = [] of ::LSP::CodeLens
      is_spec = uri.matches?(SPEC_PATTERN)

      lines = source.split('\n')

      if is_spec
        # "Run file" lens on line 0.
        result << file_lens(uri, lines)
      end

      lines.each_with_index do |line, idx|
        stripped = line.strip

        # Spec block openers.
        if is_spec && (m = stripped.match(/^(?:it|describe|context)\s+["'](.+?)["']/))
          result << spec_lens(uri, idx, m[1])
          next
        end

        # `def main` or top-level `def run` lens (non-spec files).
        unless is_spec
          if stripped.starts_with?("def main") || stripped.starts_with?("def run(")
            result << run_lens(uri, idx)
          end
        end
      end

      result
    end

    # ---------------------------------------------------------------------------
    # Lens constructors
    # ---------------------------------------------------------------------------

    private def file_lens(uri : String, lines : Array(String)) : ::LSP::CodeLens
      # Place the lens on the first non-blank, non-comment line.
      target_line = 0
      lines.each_with_index do |l, i|
        unless l.strip.empty? || l.strip.starts_with?('#')
          target_line = i
          break
        end
      end

      range = single_line_range(target_line)
      command = ::LSP::Command.new(
        title: "▶ Run spec file",
        command: "lattice.runSpec",
        arguments: [JSON::Any.new(uri), JSON::Any.new(nil)],
      )
      ::LSP::CodeLens.new(range: range, command: command)
    end

    private def spec_lens(uri : String, line : Int32, description : String) : ::LSP::CodeLens
      range = single_line_range(line)
      command = ::LSP::Command.new(
        title: "▶ Run: #{description}",
        command: "lattice.runSpec",
        arguments: [JSON::Any.new(uri), JSON::Any.new(description)],
      )
      ::LSP::CodeLens.new(range: range, command: command)
    end

    private def run_lens(uri : String, line : Int32) : ::LSP::CodeLens
      range = single_line_range(line)
      command = ::LSP::Command.new(
        title: "▶ Run",
        command: "lattice.run",
        arguments: [JSON::Any.new(uri)],
      )
      ::LSP::CodeLens.new(range: range, command: command)
    end

    private def single_line_range(line : Int32) : ::LSP::Range
      pos = ::LSP::Position.new(line, 0)
      ::LSP::Range.new(pos, pos)
    end
  end
end

module LSP
  # ---------------------------------------------------------------------------
  # Protocol types for textDocument/codeLens
  # ---------------------------------------------------------------------------

  struct Command
    include JSON::Serializable

    property title : String
    property command : String
    property? arguments : Array(JSON::Any)?

    def initialize(
      @title : String,
      @command : String,
      @arguments : Array(JSON::Any)? = nil,
    )
    end
  end

  struct CodeLens
    include JSON::Serializable

    property range : Range
    property? command : Command?
    property? data : JSON::Any?

    def initialize(@range : Range, @command : Command? = nil, @data : JSON::Any? = nil)
    end
  end

  struct CodeLensParams
    include JSON::Serializable

    @[JSON::Field(key: "textDocument")]
    property text_document : TextDocumentIdentifier
  end

  struct CodeLensOptions
    include JSON::Serializable

    @[JSON::Field(key: "resolveProvider")]
    property? resolve_provider : Bool?

    def initialize(@resolve_provider : Bool? = nil)
    end
  end
end
