# frozen_string_literal: true
#
#   irb/completion.rb -
#   	by Keiju ISHITSUKA(keiju@ishitsuka.com)
#       From Original Idea of shugo@ruby-lang.org
#

require_relative 'ruby-lex'

module IRB
  class DocumentTarget # :nodoc:
    attr_reader :name

    def initialize(name)
      @name = name
    end
  end

  class CommandDocument < DocumentTarget # :nodoc:
  end

  # Represents a method/class documentation target. May hold multiple names
  # when the receiver is ambiguous (e.g. `{}.any?` could be Hash#any? or Proc#any?).
  # The dialog popup uses only the first name; the full-screen display renders all.
  class MethodDocument < DocumentTarget # :nodoc:
    attr_reader :names

    def initialize(*names)
      super(names.first)
      @names = names
    end
  end

  class BaseCompletor # :nodoc:

    # Set of reserved words used by Ruby, you should not use these for
    # constants or variables

    HELP_COMMAND_PREPOSING = /\Ahelp\s+/

    def completion_candidates(preposing, target, postposing, bind:)
      raise NotImplementedError
    end

    def doc_namespace(preposing, matched, postposing, bind:)
      raise NotImplementedError
    end

    GEM_PATHS =
      if defined?(Gem::Specification)
        Gem::Specification.latest_specs(true).map { |s|
          s.require_paths.map { |p|
            if File.absolute_path?(p)
              p
            else
              File.join(s.full_gem_path, p)
            end
          }
        }.flatten
      else
        []
      end.freeze

    def retrieve_gem_and_system_load_path
      candidates = (GEM_PATHS | $LOAD_PATH)
      candidates.filter_map do |p|
        if p.respond_to?(:to_path)
          p.to_path
        else
          String(p) rescue nil
        end
      end.sort
    end

    def retrieve_files_to_require_from_load_path
      @files_from_load_path ||=
        (
          shortest = []
          rest = retrieve_gem_and_system_load_path.each_with_object([]) { |path, result|
            begin
              names = Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: path)
            rescue Errno::ENOENT
              nil
            end
            next if names.empty?
            names.map! { |n| n.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '') }.sort!
            shortest << names.shift
            result.concat(names)
          }
          shortest.sort! | rest
        )
    end

    def command_candidates(target)
      if !target.empty?
        IRB::Command.command_names.select { _1.start_with?(target) }
      else
        []
      end
    end

    def command_document_target(preposing, matched)
      if preposing.empty? && IRB::Command.command_names.include?(matched)
        CommandDocument.new(matched)
      end
    end

    def retrieve_files_to_require_relative_from_current_dir
      @files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path|
        path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '')
      }
    end
  end

  class TypeCompletor < BaseCompletor # :nodoc:
    def initialize(context)
      @context = context
    end

    def inspect
      ReplTypeCompletor.info
    end

    def completion_candidates(preposing, target, _postposing, bind:)
      # When completing the argument of `help` command, only commands should be candidates
      return command_candidates(target) if preposing.match?(HELP_COMMAND_PREPOSING)

      commands = if preposing.empty?
        command_candidates(target)
      # It doesn't make sense to propose commands with other preposing
      else
        []
      end

      result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path)

      return commands unless result

      encoded_candidates = result.completion_candidates.filter_map do |i|
        encoded = i.encode(Encoding.default_external)
        target + encoded
      rescue Encoding::UndefinedConversionError
        # If the string cannot be converted, we just ignore it
        nil
      end
      commands | encoded_candidates
    end

    def doc_namespace(preposing, matched, _postposing, bind:)
      command_document_target(preposing, matched) || begin
        result = ReplTypeCompletor.analyze(preposing + matched, binding: bind, filename: @context.irb_path)
        result&.doc_namespace('')
      end
    end
  end

  class RegexpCompletor < BaseCompletor # :nodoc:
    KERNEL_METHODS = ::Kernel.instance_method(:methods)
    KERNEL_PRIVATE_METHODS = ::Kernel.instance_method(:private_methods)
    KERNEL_INSTANCE_VARIABLES = ::Kernel.instance_method(:instance_variables)
    OBJECT_CLASS_INSTANCE_METHOD = ::Object.instance_method(:class)
    MODULE_CONSTANTS_INSTANCE_METHOD = ::Module.instance_method(:constants)

    using Module.new {
      refine ::Binding do
        def eval_methods
          KERNEL_METHODS.bind_call(receiver)
        end

        def eval_private_methods
          KERNEL_PRIVATE_METHODS.bind_call(receiver)
        end

        def eval_instance_variables
          KERNEL_INSTANCE_VARIABLES.bind_call(receiver)
        end

        def eval_global_variables
          ::Kernel.global_variables
        end

        def eval_class_constants
          klass = OBJECT_CLASS_INSTANCE_METHOD.bind_call(receiver)
          MODULE_CONSTANTS_INSTANCE_METHOD.bind_call(klass)
        end
      end
    }

    def inspect
      'RegexpCompletor'
    end

    def complete_require_path(target, preposing, postposing)
      if target =~ /\A(['"])([^'"]+)\Z/
        quote = $1
        actual_target = $2
      else
        return nil # It's not String literal
      end

      case preposing
      when /(^|[^\w])require\(? *\z/
        retrieve_files_to_require_from_load_path.filter_map { |path|
          quote + path if path.start_with?(actual_target)
        }
      when /(^|[^\w])require_relative\(? *\z/
        retrieve_files_to_require_relative_from_current_dir.filter_map { |path|
          quote + path if path.start_with?(actual_target)
        }
      end
    end

    def completion_candidates(preposing, target, postposing, bind:)
      if result = complete_require_path(target, preposing, postposing)
        return result
      end

      commands = command_candidates(target)

      # When completing the argument of `help` command, only commands should be candidates
      return commands if preposing.match?(HELP_COMMAND_PREPOSING)

      # It doesn't make sense to propose commands with other preposing
      commands = [] unless preposing.empty?

      completion_data = retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.filter_map do |i|
        i.encode(Encoding.default_external)
      rescue Encoding::UndefinedConversionError
        # If the string cannot be converted, we just ignore it
        nil
      end
      commands | completion_data
    end

    def doc_namespace(preposing, matched, _postposing, bind:)
      command_document_target(preposing, matched) || retrieve_completion_data(matched, bind: bind, doc_namespace: true)
    end

    def retrieve_completion_data(input, bind:, doc_namespace:)
      case input
      # this regexp only matches the closing character because of irb's Reline.completer_quote_characters setting
      # details are described in: https://github.com/ruby/irb/pull/523
      when /^(.*["'`])\.([^.]*)$/
        # String
        receiver = $1
        message = $2

        if doc_namespace
          "String.#{message}"
        else
          candidates = String.instance_methods.collect{|m| m.to_s}
          select_message(receiver, message, candidates)
        end

      # this regexp only matches the closing character because of irb's Reline.completer_quote_characters setting
      # details are described in: https://github.com/ruby/irb/pull/523
      when /^(.*\/)\.([^.]*)$/
        # Regexp
        receiver = $1
        message = $2

        if doc_namespace
          "Regexp.#{message}"
        else
          candidates = Regexp.instance_methods.collect{|m| m.to_s}
          select_message(receiver, message, candidates)
        end

      when /^([^\]]*\])\.([^.]*)$/
        # Array
        receiver = $1
        message = $2

        if doc_namespace
          "Array.#{message}"
        else
          candidates = Array.instance_methods.collect{|m| m.to_s}
          select_message(receiver, message, candidates)
        end

      when /^([^\}]*\})\.([^.]*)$/
        # Hash or Proc
        receiver = $1
        message = $2

        if doc_namespace
          ["Hash.#{message}", "Proc.#{message}"]
        else
          hash_candidates = Hash.instance_methods.collect{|m| m.to_s}
          proc_candidates = Proc.instance_methods.collect{|m| m.to_s}
          select_message(receiver, message, hash_candidates | proc_candidates)
        end

      when /^(:[^:.]+)$/
        # Symbol
        if doc_namespace
          nil
        else
          sym = $1
          candidates = Symbol.all_symbols.filter_map do |s|
            s.inspect
          rescue EncodingError
            # ignore
          end
          candidates.grep(/^#{Regexp.quote(sym)}/).sort
        end
      when /^::([A-Z][^:\.\(\)]*)$/
        # Absolute Constant or class methods
        receiver = $1

        candidates = Object.constants.collect{|m| m.to_s}

        if doc_namespace
          candidates.find { |i| i == receiver }
        else
          candidates.grep(/^#{Regexp.quote(receiver)}/).sort.collect{|e| "::" + e}
        end

      when /^([A-Z].*)::([^:.]*)$/
        # Constant or class methods
        receiver = $1
        message = $2

        if doc_namespace
          "#{receiver}::#{message}"
        else
          begin
            candidates = eval("#{receiver}.constants.collect{|m| m.to_s}", bind)
            candidates |= eval("#{receiver}.methods.collect{|m| m.to_s}", bind)
          rescue Exception
            candidates = []
          end

          select_message(receiver, message, candidates, "::")
        end

      when /^(:[^:.]+)(\.|::)([^.]*)$/
        # Symbol
        receiver = $1
        sep = $2
        message = $3

        if doc_namespace
          "Symbol.#{message}"
        else
          candidates = Symbol.instance_methods.collect{|m| m.to_s}
          select_message(receiver, message, candidates, sep)
        end

      when /^(?<num>-?(?:0[dbo])?[0-9_]+(?:\.[0-9_]+)?(?:(?:[eE][+-]?[0-9]+)?i?|r)?)(?<sep>\.|::)(?<mes>[^.]*)$/
        # Numeric
        receiver = $~[:num]
        sep = $~[:sep]
        message = $~[:mes]

        begin
          instance = eval(receiver, bind)

          if doc_namespace
            "#{instance.class.name}.#{message}"
          else
            candidates = instance.methods.collect{|m| m.to_s}
            select_message(receiver, message, candidates, sep)
          end
        rescue Exception
          if doc_namespace
            nil
          else
            []
          end
        end

      when /^(-?0x[0-9a-fA-F_]+)(\.|::)([^.]*)$/
        # Numeric(0xFFFF)
        receiver = $1
        sep = $2
        message = $3

        begin
          instance = eval(receiver, bind)
          if doc_namespace
            "#{instance.class.name}.#{message}"
          else
            candidates = instance.methods.collect{|m| m.to_s}
            select_message(receiver, message, candidates, sep)
          end
        rescue Exception
          if doc_namespace
            nil
          else
            []
          end
        end

      when /^(\$[^.]*)$/
        # global var
        gvar = $1
        all_gvars = global_variables.collect{|m| m.to_s}

        if doc_namespace
          all_gvars.find{ |i| i == gvar }
        else
          all_gvars.grep(Regexp.new(Regexp.quote(gvar))).sort
        end

      when /^([^.:"].*)(\.|::)([^.]*)$/
        # variable.func or func.func
        receiver = $1
        sep = $2
        message = $3

        gv = bind.eval_global_variables.collect{|m| m.to_s}.push("true", "false", "nil")
        lv = bind.local_variables.collect{|m| m.to_s}
        iv = bind.eval_instance_variables.collect{|m| m.to_s}
        cv = bind.eval_class_constants.collect{|m| m.to_s}

        if (gv | lv | iv | cv).include?(receiver) or /^[A-Z]/ =~ receiver && /\./ !~ receiver
          # foo.func and foo is var. OR
          # foo::func and foo is var. OR
          # foo::Const and foo is var. OR
          # Foo::Bar.func
          begin
            candidates = []
            rec = eval(receiver, bind)
            if sep == "::" and rec.kind_of?(Module)
              candidates = rec.constants.collect{|m| m.to_s}
            end
            candidates |= rec.methods.collect{|m| m.to_s}
          rescue Exception
            candidates = []
          end
        else
          # func1.func2
          candidates = []
        end

        if doc_namespace
          rec_class = rec.is_a?(Module) ? rec : rec.class
          "#{rec_class.name}#{sep}#{candidates.find{ |i| i == message }}" rescue nil
        else
          select_message(receiver, message, candidates, sep)
        end

      when /^\.([^.]*)$/
        # unknown(maybe String)

        receiver = ""
        message = $1

        candidates = String.instance_methods(true).collect{|m| m.to_s}

        if doc_namespace
          "String.#{candidates.find{ |i| i == message }}"
        else
          select_message(receiver, message, candidates)
        end
      when /^\s*$/
        # empty input
        if doc_namespace
          nil
        else
          []
        end
      else
        if doc_namespace
          vars = (bind.local_variables | bind.eval_instance_variables).collect{|m| m.to_s}
          perfect_match_var = vars.find{|m| m.to_s == input}
          if perfect_match_var
            eval("#{perfect_match_var}.class.name", bind) rescue nil
          else
            candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
            candidates |= RubyLex::RESERVED_WORDS.map(&:to_s)
            candidates.find{ |i| i == input }
          end
        else
          candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
          candidates |= RubyLex::RESERVED_WORDS.map(&:to_s)

          target_encoding = Encoding.default_external
          candidates = candidates.compact.filter_map do |candidate|
            candidate.encoding == target_encoding ? candidate : candidate.encode(target_encoding)
          rescue EncodingError
            nil
          end
          candidates.grep(/^#{Regexp.quote(input)}/).sort
        end
      end
    end

    # Set of available operators in Ruby
    Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~]

    def select_message(receiver, message, candidates, sep = ".")
      sorted_candidates = candidates.grep(/^#{Regexp.quote(message)}/).sort_by do |e|
        # Alphabetical order, but internal methods (leading underscore) last
        [e.start_with?('_') ? 1 : 0, e]
      end
      sorted_candidates.collect do |e|
        case e
        when /^[a-zA-Z_]/
          receiver + sep + e
        when /^[0-9]/
        when *Operators
          #receiver + " " + e
        end
      end
    end
  end

  module InputCompletor
    class << self
      private def regexp_completor
        @regexp_completor ||= RegexpCompletor.new
      end

      def retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false)
        regexp_completor.retrieve_completion_data(input, bind: bind, doc_namespace: doc_namespace)
      end
    end
    CompletionProc = ->(target, preposing = nil, postposing = nil) {
      regexp_completor.completion_candidates(preposing || '', target, postposing || '', bind: IRB.conf[:MAIN_CONTEXT].workspace.binding)
    }
  end
  deprecate_constant :InputCompletor
end
