# frozen_string_literal: true

module Toys
  module Utils
    ##
    # Implementations of tab completion.
    #
    # This module is not loaded by default. Before using it directly, you must
    # `require "toys/utils/completion_engine"`
    #
    module CompletionEngine
      ##
      # Base class for shell completion engines that use a
      # `COMP_LINE` / `COMP_POINT` protocol.
      #
      # Subclasses must implement the private methods `#shell_name` and
      # `#output_completions`.
      #
      class Base
        ##
        # Create a completion engine.
        #
        # @param cli [Toys::CLI] The CLI.
        #
        def initialize(cli)
          @cli = cli
        end

        ##
        # Perform completion in the current shell environment, which must
        # include settings for the `COMP_LINE` and `COMP_POINT` environment
        # variables. Prints out completion candidates and returns a status code
        # indicating the result.
        #
        #  *  **0** for success.
        #  *  **1** if completion failed.
        #  *  **2** if the environment is incorrect (e.g. expected environment
        #     variables not found)
        #
        # @return [Integer] status code
        #
        def run
          return 2 if !::ENV.key?("COMP_LINE") || !::ENV.key?("COMP_POINT")
          line = ::ENV["COMP_LINE"].to_s
          point = ::ENV["COMP_POINT"].to_i
          point = line.length if point.negative?
          line = line[0, point]
          result = run_internal(line)
          if result
            output_completions(*result)
            0
          else
            1
          end
        end

        ##
        # Internal completion method designed for testing.
        # Returns `[quote_type, Array<Toys::Completion::Candidate>]`, or `nil`
        # if the line cannot be parsed (e.g. the executable name is missing).
        #
        # @private
        #
        def run_internal(line)
          words = CompletionEngine.split(line)
          quote_type, last = words.pop
          return nil unless words.shift
          words.map! { |_type, word| word }
          prefix = ""
          if (match = /\A(.*[=:])(.*)\z/.match(last))
            prefix = match[1]
            last = match[2]
          end
          context = Completion::Context.new(
            cli: @cli, previous_words: words, fragment_prefix: prefix, fragment: last,
            params: { shell: shell_name, quote_type: quote_type }
          )
          [quote_type, @cli.completion.call(context).uniq.sort]
        end

        private

        def shell_name
          raise ::NotImplementedError
        end

        def output_completions(_quote_type, _candidates)
          raise ::NotImplementedError
        end
      end

      ##
      # A completion engine for bash.
      #
      class Bash < Base
        ##
        # Create a bash completion engine.
        #
        # @param cli [Toys::CLI] The CLI.
        #
        def initialize(cli)
          require "shellwords"
          super
        end

        ##
        # @private
        # Accessible only for testing
        #
        def format_candidate(candidate, quote_type)
          str = candidate.to_s
          partial = candidate.is_a?(Completion::Candidate) ? candidate.partial? : false
          quote_type = nil if candidate.string.include?("'") && quote_type == :single
          case quote_type
          when :single
            partial ? "'#{str}" : "'#{str}' "
          when :double
            str = str.gsub(/[$`"\\\n]/, '\\\\\\1')
            partial ? "\"#{str}" : "\"#{str}\" "
          else
            str = ::Shellwords.escape(str)
            partial ? str : "#{str} "
          end
        end

        private

        def shell_name
          :bash
        end

        def output_completions(quote_type, candidates)
          candidates.each { |c| puts format_candidate(c, quote_type) }
        end
      end

      ##
      # A completion engine for zsh.
      #
      class Zsh < Base
        private

        def shell_name
          :zsh
        end

        def output_completions(_quote_type, candidates)
          finals, partials = candidates.partition(&:final?)
          finals.each { |c| puts c.string unless c.string.empty? }
          puts ""
          partials.each { |c| puts c.string unless c.string.empty? }
        end
      end

      class << self
        ##
        # @private
        #
        def split(line)
          words = []
          field = ::String.new
          quote_type = nil
          line.scan(split_regex) do |word, sqw, dqw, esc, garbage, sep|
            raise ArgumentError, "Didn't expect garbage: #{line.inspect}" if garbage
            field << field_str(word, sqw, dqw, esc)
            quote_type = update_quote_type(quote_type, sqw, dqw)
            if sep
              words << [quote_type, field]
              quote_type = nil
              field = sep.empty? ? nil : ::String.new
            end
          end
          words << [quote_type, field] if field
          words
        end

        private

        def split_regex
          word_re = "([^\\s\\\\\\'\\\"]+)"
          sq_re = "'([^\\']*)(?:'|\\z)"
          dq_re = "\"((?:[^\\\"\\\\]|\\\\.)*)(?:\"|\\z)"
          esc_re = "(\\\\.?)"
          sep_re = "(\\s|\\z)"
          /\G\s*(?>#{word_re}|#{sq_re}|#{dq_re}|#{esc_re}|(\S))#{sep_re}?/m
        end

        def field_str(word, sqw, dqw, esc)
          word ||
            sqw ||
            dqw&.gsub(/\\([$`"\\\n])/, '\\1') ||
            esc&.gsub(/\\(.)/, '\\1') ||
            ""
        end

        def update_quote_type(quote_type, sqw, dqw)
          if quote_type
            :multi
          elsif sqw
            :single
          elsif dqw
            :double
          else
            :bare
          end
        end
      end
    end
  end
end
