require "../completion_provider"

module Lattice
  module Completion
    module Providers
      # SnippetCompletionProvider emits LSP snippet items (insertTextFormat: 2)
      # for common Crystal structural patterns.  Each item uses `$1`, `$2`, …
      # tab stops so editors that support LSP snippets (Zed, Neovim, Helix) will
      # place the cursor at the first tab stop and let the user tab through the
      # remaining placeholders.
      #
      # Items are suppressed when the cursor is inside a string literal,
      # a comment, or a dot-completion / namespace-qualified context —
      # situations where structural snippets are never useful.
      class SnippetCompletionProvider < CompletionProvider
        # Snippet definition: {trigger_prefix, label, insert_text, detail, doc}
        private record SnippetDef,
          label : String,
          insert_text : String,
          detail : String,
          documentation : String

        SNIPPETS = [
          # --- Method definitions ---
          SnippetDef.new(
            label: "def",
            insert_text: "def ${1:method_name}(${2:params})\n  ${3:# body}\nend",
            detail: "Method definition",
            documentation: "Define a new method.",
          ),
          SnippetDef.new(
            label: "defr",
            insert_text: "def ${1:method_name}(${2:params}) : ${3:ReturnType}\n  ${4:# body}\nend",
            detail: "Method with return type",
            documentation: "Define a method with an explicit return type annotation.",
          ),
          SnippetDef.new(
            label: "defa",
            insert_text: "abstract def ${1:method_name}(${2:params}) : ${3:ReturnType}",
            detail: "Abstract method",
            documentation: "Declare an abstract method that subclasses must implement.",
          ),
          SnippetDef.new(
            label: "defc",
            insert_text: "def self.${1:method_name}(${2:params})\n  ${3:# body}\nend",
            detail: "Class method (def self.)",
            documentation: "Define a class-level method.",
          ),
          SnippetDef.new(
            label: "defg",
            insert_text: "def ${1:method_name}(${2:arg} : ${3:T}) : ${3:T} forall ${3:T}\n  ${4:# body}\nend",
            detail: "Generic method",
            documentation: "Define a generic method with a type parameter.",
          ),
          SnippetDef.new(
            label: "init",
            insert_text: "def initialize(@${1:name} : ${2:Type})\nend",
            detail: "initialize constructor",
            documentation: "Define an initialize constructor using instance variable shorthand.",
          ),

          # --- Type definitions ---
          SnippetDef.new(
            label: "class",
            insert_text: "class ${1:ClassName}\n  ${2:# body}\nend",
            detail: "Class definition",
            documentation: "Define a new class.",
          ),
          SnippetDef.new(
            label: "classs",
            insert_text: "class ${1:ClassName} < ${2:Superclass}\n  ${2:# body}\nend",
            detail: "Class with superclass",
            documentation: "Define a class that inherits from a superclass.",
          ),
          SnippetDef.new(
            label: "classa",
            insert_text: "abstract class ${1:ClassName}\n  ${2:# body}\nend",
            detail: "Abstract class",
            documentation: "Define an abstract class.",
          ),
          SnippetDef.new(
            label: "classg",
            insert_text: "class ${1:ClassName}(${2:T})\n  ${3:# body}\nend",
            detail: "Generic class",
            documentation: "Define a generic class with a type parameter.",
          ),
          SnippetDef.new(
            label: "struct",
            insert_text: "struct ${1:StructName}\n  ${2:# body}\nend",
            detail: "Struct definition",
            documentation: "Define a new struct.",
          ),
          SnippetDef.new(
            label: "record",
            insert_text: "record ${1:RecordName}, ${2:field} : ${3:Type}",
            detail: "Record macro",
            documentation: "Define a compact immutable struct via the record macro.",
          ),
          SnippetDef.new(
            label: "recordm",
            insert_text: "record ${1:RecordName},\n  ${2:field1} : ${3:Type1},\n  ${4:field2} : ${5:Type2}",
            detail: "Record with multiple fields",
            documentation: "Define a record with several named fields.",
          ),
          SnippetDef.new(
            label: "module",
            insert_text: "module ${1:ModuleName}\n  ${2:# body}\nend",
            detail: "Module definition",
            documentation: "Define a new module.",
          ),
          SnippetDef.new(
            label: "enum",
            insert_text: "enum ${1:EnumName}\n  ${2:Member}\nend",
            detail: "Enum definition",
            documentation: "Define a new enum.",
          ),
          SnippetDef.new(
            label: "enumt",
            insert_text: "enum ${1:EnumName} : ${2:UInt8}\n  ${3:Member}\nend",
            detail: "Enum with backing type",
            documentation: "Define an enum with an explicit backing integer type.",
          ),
          SnippetDef.new(
            label: "annotation",
            insert_text: "annotation ${1:AnnotationName}\nend",
            detail: "Annotation definition",
            documentation: "Define a custom annotation.",
          ),
          SnippetDef.new(
            label: "ann",
            insert_text: "@[${1:AnnotationName}(${2:key}: ${3:value})]",
            detail: "Annotation usage",
            documentation: "Apply an annotation to a declaration.",
          ),
          SnippetDef.new(
            label: "macro",
            insert_text: "macro ${1:macro_name}(${2:args})\n  ${3:# body}\nend",
            detail: "Macro definition",
            documentation: "Define a Crystal macro.",
          ),
          SnippetDef.new(
            label: "alias",
            insert_text: "alias ${1:NewName} = ${2:ExistingType}",
            detail: "Type alias",
            documentation: "Create a type alias.",
          ),

          # --- Properties and accessors ---
          SnippetDef.new(
            label: "getter",
            insert_text: "getter ${1:name} : ${2:Type}",
            detail: "getter macro",
            documentation: "Declare a read-only property.",
          ),
          SnippetDef.new(
            label: "setter",
            insert_text: "setter ${1:name} : ${2:Type}",
            detail: "setter macro",
            documentation: "Declare a write-only property.",
          ),
          SnippetDef.new(
            label: "property",
            insert_text: "property ${1:name} : ${2:Type}",
            detail: "property macro",
            documentation: "Declare a read-write property.",
          ),

          # --- Control flow ---
          SnippetDef.new(
            label: "if",
            insert_text: "if ${1:condition}\n  ${2:# body}\nend",
            detail: "If statement",
            documentation: "Conditional if statement.",
          ),
          SnippetDef.new(
            label: "ifelse",
            insert_text: "if ${1:condition}\n  ${2:# then branch}\nelse\n  ${3:# else branch}\nend",
            detail: "If-else statement",
            documentation: "Conditional if-else statement.",
          ),
          SnippetDef.new(
            label: "unless",
            insert_text: "unless ${1:condition}\n  ${2:# body}\nend",
            detail: "Unless statement",
            documentation: "Conditional unless (negated if) statement.",
          ),
          SnippetDef.new(
            label: "case",
            insert_text: "case ${1:expr}\nwhen ${2:value}\n  ${3:# body}\nend",
            detail: "Case/when",
            documentation: "Pattern-matching case statement.",
          ),
          SnippetDef.new(
            label: "casein",
            insert_text: "case ${1:expr}\nin ${2:ClassName}\n  ${3:# body}\nend",
            detail: "Exhaustive case/in",
            documentation: "Exhaustive case/in statement for union types.",
          ),
          SnippetDef.new(
            label: "while",
            insert_text: "while ${1:condition}\n  ${2:# body}\nend",
            detail: "While loop",
            documentation: "Repeat a block while the condition is true.",
          ),
          SnippetDef.new(
            label: "until",
            insert_text: "until ${1:condition}\n  ${2:# body}\nend",
            detail: "Until loop",
            documentation: "Repeat a block until the condition is true.",
          ),
          SnippetDef.new(
            label: "loop",
            insert_text: "loop do\n  ${1:# body}\nend",
            detail: "Infinite loop",
            documentation: "An infinite loop construct.",
          ),

          # --- Collection iteration ---
          SnippetDef.new(
            label: "each",
            insert_text: "${1:collection}.each do |${2:item}|\n  ${3:# body}\nend",
            detail: "each iterator",
            documentation: "Iterate over each element of a collection.",
          ),
          SnippetDef.new(
            label: "eachi",
            insert_text: "${1:collection}.each_with_index do |${2:item}, ${3:index}|\n  ${4:# body}\nend",
            detail: "each_with_index iterator",
            documentation: "Iterate with element and index.",
          ),
          SnippetDef.new(
            label: "times",
            insert_text: "${1:n}.times do |${2:i}|\n  ${3:# body}\nend",
            detail: "N.times loop",
            documentation: "Repeat a block N times.",
          ),
          SnippetDef.new(
            label: "map",
            insert_text: "${1:collection}.map { |${2:item}| ${3:item} }",
            detail: "map transform",
            documentation: "Transform each element of a collection.",
          ),
          SnippetDef.new(
            label: "select",
            insert_text: "${1:collection}.select { |${2:item}| ${3:condition} }",
            detail: "select filter",
            documentation: "Filter elements that satisfy a condition.",
          ),
          SnippetDef.new(
            label: "reject",
            insert_text: "${1:collection}.reject { |${2:item}| ${3:condition} }",
            detail: "reject filter",
            documentation: "Reject elements that satisfy a condition.",
          ),
          SnippetDef.new(
            label: "reduce",
            insert_text: "${1:collection}.reduce(${2:initial}) { |${3:acc}, ${4:item}| ${5:acc} }",
            detail: "reduce/inject fold",
            documentation: "Fold a collection into a single value.",
          ),

          # --- Error handling ---
          SnippetDef.new(
            label: "begin",
            insert_text: "begin\n  ${1:# body}\nrescue ${2:ex} : ${3:Exception}\n  ${4:# handle}\nensure\n  ${5:# always runs}\nend",
            detail: "Begin/rescue/ensure block",
            documentation: "Handle exceptions with optional ensure clause.",
          ),
          SnippetDef.new(
            label: "rescue",
            insert_text: "begin\n  ${1:# body}\nrescue ${2:ex} : ${3:Exception}\n  ${4:# handle}\nend",
            detail: "Begin/rescue block",
            documentation: "Handle exceptions.",
          ),
          SnippetDef.new(
            label: "raise",
            insert_text: "raise ${1:Exception}.new(${2:\"message\"})",
            detail: "raise statement",
            documentation: "Raise an exception.",
          ),

          # --- Concurrency ---
          SnippetDef.new(
            label: "spawn",
            insert_text: "spawn do\n  ${1:# body}\nend",
            detail: "Spawn a fiber",
            documentation: "Spawn a new lightweight fiber.",
          ),
          SnippetDef.new(
            label: "chan",
            insert_text: "Channel(${1:Type}).new(${2:capacity})",
            detail: "Buffered channel",
            documentation: "Create a buffered channel.",
          ),
          SnippetDef.new(
            label: "chanu",
            insert_text: "Channel(${1:Type}).new",
            detail: "Unbuffered channel",
            documentation: "Create an unbuffered (synchronous) channel.",
          ),
          SnippetDef.new(
            label: "proc",
            insert_text: "->(${1:arg} : ${2:Type}) { ${3:# body} }",
            detail: "Proc literal",
            documentation: "Create a Proc (lambda) literal.",
          ),

          # --- I/O and debugging ---
          SnippetDef.new(
            label: "puts",
            insert_text: "puts ${1:value}",
            detail: "puts",
            documentation: "Print a value followed by a newline.",
          ),
          SnippetDef.new(
            label: "pp",
            insert_text: "pp ${1:value}",
            detail: "pp (structured inspect)",
            documentation: "Pretty-print a value with its type.",
          ),
          SnippetDef.new(
            label: "heredoc",
            insert_text: "<<-${1:HEREDOC}\n  ${2:content}\n  ${1:HEREDOC}",
            detail: "Heredoc string",
            documentation: "A heredoc (multi-line) string literal.",
          ),
          SnippetDef.new(
            label: "const",
            insert_text: "${1:CONSTANT_NAME} = ${2:value}",
            detail: "Constant declaration",
            documentation: "Declare a constant.",
          ),

          # --- Specs ---
          SnippetDef.new(
            label: "describe",
            insert_text: "describe ${1:Subject} do\n  ${2:# examples}\nend",
            detail: "Spec describe block",
            documentation: "Group spec examples under a subject.",
          ),
          SnippetDef.new(
            label: "context",
            insert_text: "context \"${1:when condition}\" do\n  ${2:# examples}\nend",
            detail: "Spec context block",
            documentation: "Group spec examples under a shared context.",
          ),
          SnippetDef.new(
            label: "it",
            insert_text: "it \"${1:does something}\" do\n  ${2:# assertions}\nend",
            detail: "Spec it block",
            documentation: "Define a single spec example.",
          ),

          # --- Require ---
          SnippetDef.new(
            label: "require",
            insert_text: "require \"${1:path}\"",
            detail: "require statement",
            documentation: "Require a Crystal source file or shard.",
          ),
          SnippetDef.new(
            label: "include",
            insert_text: "include ${1:ModuleName}",
            detail: "include statement",
            documentation: "Include a module's instance methods.",
          ),
          SnippetDef.new(
            label: "extend",
            insert_text: "extend ${1:ModuleName}",
            detail: "extend statement",
            documentation: "Extend a module's methods as class methods.",
          ),

          # --- C bindings ---
          SnippetDef.new(
            label: "lib",
            insert_text: "lib ${1:LibName}\n  fun ${2:function_name}(${3:arg} : ${4:Type}) : ${5:ReturnType}\nend",
            detail: "lib block for C binding",
            documentation: "Declare a C library binding.",
          ),

          # --- Utility ---
          SnippetDef.new(
            label: "todo",
            insert_text: "# TODO: ${1:description}",
            detail: "TODO comment",
            documentation: "Leave a TODO marker.",
          ),
          SnippetDef.new(
            label: "fixme",
            insert_text: "# FIXME: ${1:description}",
            detail: "FIXME comment",
            documentation: "Leave a FIXME marker.",
          ),
        ] of SnippetDef

        def completions(
          uri : String,
          source : String,
          position : ::LSP::Position,
        ) : Array(::LSP::CompletionItem)
          lines = source.split('\n')
          return [] of ::LSP::CompletionItem if position.line >= lines.size

          line = lines[position.line]
          prefix = line[0...position.character]

          # Suppress snippets inside strings, comments, or after `.` / `::`.
          return [] of ::LSP::CompletionItem if inside_string_or_comment?(line, position.character)
          return [] of ::LSP::CompletionItem if prefix.match(/([\w@]+)\.$/) || prefix.match(/::[\w]*$/)

          # Extract the word fragment immediately before the cursor.
          fragment = word_before(source, position)

          items = [] of ::LSP::CompletionItem

          SNIPPETS.each do |snip|
            next unless snip.label.starts_with?(fragment)

            item = ::LSP::CompletionItem.new(snip.label)
            item.kind            = ::LSP::CompletionItemKind::Snippet
            item.detail          = snip.detail
            item.doc            = snip.documentation
            item.insert_text     = snip.insert_text
            item.insert_text_format = ::LSP::InsertTextFormat::Snippet
            items << item
          end

          items
        end

        # Returns true when *char* in *line* is inside a string literal or
        # comment.  This is an approximation based on character-level scanning;
        # it handles the common cases without a full parse.
        private def inside_string_or_comment?(line : String, char : Int32) : Bool
          in_double = false
          in_single = false
          i = 0
          while i < char && i < line.size
            c = line[i]
            case c
            when '#'
              return true unless in_double || in_single
            when '"'
              in_double = !in_double unless in_single
            when '\''
              in_single = !in_single unless in_double
            when '\\'
              i += 1  # skip escaped character
            end
            i += 1
          end
          in_double || in_single
        end
      end
    end
  end
end
