require "../completion_provider"
require "../../index/index"

module Lattice
  module Completion
    module Providers
      # ReferenceProvider resolves completions from the workspace symbol index.
      # It covers local variables visible before the cursor, instance variables,
      # and workspace-wide class/module/method names via `WorkspaceIndex`.
      class ReferenceProvider < CompletionProvider
        def initialize(@index : Lattice::Index::WorkspaceIndex)
        end

        def completions(
          uri : String,
          source : String,
          position : ::LSP::Position,
        ) : Array(::LSP::CompletionItem)
          items = [] of ::LSP::CompletionItem
          prefix = word_before(source, position)

          # Local variables and parameters visible before the cursor.
          local_vars(source, position).each do |var|
            next unless var.starts_with?(prefix)
            items << variable_item(var)
          end

          # Instance variables visible in the current file.
          instance_vars(source).each do |ivar, type|
            next unless ivar.starts_with?(prefix)
            items << field_item(ivar, type)
          end

          # Workspace-wide symbols: classes, modules, methods.
          @index.search(prefix).each do |entry|
            item = case entry.kind
                   when "class", "struct"  then class_item(entry.name, entry.type_name)
                   when "module"           then class_item(entry.name)
                   when "method"           then method_item(entry.name, entry.signature)
                   when "constant"         then variable_item(entry.name, entry.type_name)
                   when "instance_variable", "property", "getter", "setter"
                     field_item(entry.name, entry.type_name)
                   else
                     i = ::LSP::CompletionItem.new(entry.name)
                     i.kind = ::LSP::CompletionItemKind::Text
                     i
                   end
            if doc = entry.documentation
              item.doc = doc
            end
            items << item
          end

          items
        end

        # Scan the source above *position* for local variable assignments and
        # method parameter declarations.
        private def local_vars(source : String, position : ::LSP::Position) : Array(String)
          vars = [] of String
          lines = source.split('\n')
          limit = position.line.clamp(0, lines.size - 1)

          lines[0..limit].each do |line|
            # Simple assignment: foo = …
            if m = line.match(/^\s*(\w+)\s*=(?!=)/)
              vars << m[1] unless m[1] == "_"
            end
            # Typed assignment: foo : Type = …
            if m = line.match(/^\s*(\w+)\s*:\s*\w+\s*=/)
              vars << m[1]
            end
            # Block parameter: |x, y|
            line.scan(/\|\s*([\w,\s]+)\s*\|/) do |bm|
              bm[1].split(',').each { |p| vars << p.strip unless p.strip.empty? }
            end
            # Method parameter: def foo(x : T, y : U)
            if m = line.match(/def\s+\w+[\w?!]*\s*\(([^)]*)\)/)
              m[1].split(',').each do |param|
                pname = param.strip.split(/\s*:\s*/).first.lstrip('@')
                vars << pname unless pname.empty?
              end
            end
          end

          vars.uniq
        end

        # Scan the source for @ivar : Type declarations.
        private def instance_vars(source : String) : Hash(String, String)
          result = {} of String => String
          source.each_line do |line|
            if m = line.match(/^\s*@(\w+)\s*:\s*([\w:?!]+)/)
              result["@#{m[1]}"] = m[2]
            elsif m = line.match(/^\s*(?:property|getter|setter)\s+(\w+)\s*:\s*([\w:?!]+)/)
              result["@#{m[1]}"] = m[2]
            end
          end
          result
        end
      end
    end
  end
end
