# frozen_string_literal: true

module Clamp
  module Completion

    # Generates bash shell completion scripts.
    #
    class BashGenerator

      def initialize(command_class, executable_name)
        @command_class = command_class
        @executable_name = executable_name
      end

      def generate
        [
          "# Bash completions for #{@executable_name}",
          "# Generated by Clamp",
          "",
          takes_value_function,
          "",
          param_count_function,
          "",
          canonical_function,
          "",
          main_function,
          "",
          "complete -F #{completion_function} #{@executable_name}"
        ].push("").join("\n")
      end

      private

      def completion_function
        "_clamp_complete_#{Completion.encode_name(@executable_name)}"
      end

      def main_function
        [
          "#{completion_function}() {",
          "    local cur prev",
          "    if type _init_completion &>/dev/null; then",
          "        _init_completion",
          "    else",
          '        cur="${COMP_WORDS[COMP_CWORD]}"',
          '        prev="${COMP_WORDS[COMP_CWORD-1]}"',
          "    fi",
          "",
          "    local subcmd_info subcmd params_remaining",
          "    subcmd_info=$(#{completion_function}_find_subcmd)",
          '    subcmd="${subcmd_info%% *}"',
          '    params_remaining="${subcmd_info##* }"',
          "    if #{completion_function}_takes_value \"$prev\" \"$subcmd\"; then",
          "        return",
          "    fi",
          "",
          '    if [[ "$cur" == -* ]]; then',
          options_case("$subcmd"),
          "    fi",
          "",
          '    if [ "$params_remaining" -eq 0 ]; then',
          subcommands_case("$subcmd"),
          "    fi",
          "}",
          "",
          find_subcmd_function
        ].join("\n")
      end

      def find_subcmd_function
        [
          "#{completion_function}_find_subcmd() {",
          "    local i=1 word subcmd skip",
          "    skip=$(#{completion_function}_param_count \"\")",
          '    while [ "$i" -lt "$COMP_CWORD" ]; do',
          '        word="${COMP_WORDS[$i]}"',
          '        case "$word" in',
          "            -*)",
          "                if #{completion_function}_takes_value \"$word\" \"$subcmd\"; then",
          "                    ((i++))",
          "                fi",
          "                ;;",
          "            *)",
          find_subcmd_match_word,
          "                ;;",
          "        esac",
          "        ((i++))",
          "    done",
          '    echo "$subcmd $skip"',
          "}"
        ].join("\n")
      end

      def find_subcmd_match_word
        subcmds = Completion.collect_subcommand_names(@command_class).join("|")
        [
          '                if [ "$skip" -gt 0 ]; then',
          "                    ((skip--))",
          "                else",
          '                    case "$word" in',
          "                        #{subcmds})",
          "                            local canonical=$(#{completion_function}_canonical \"$word\")",
          '                            if [ -z "$subcmd" ]; then',
          '                                subcmd="$canonical"',
          "                            else",
          '                                subcmd="${subcmd}::${canonical}"',
          "                            fi",
          "                            skip=$(#{completion_function}_param_count \"$subcmd\")",
          "                            ;;",
          "                    esac",
          "                fi"
        ].join("\n")
      end

      def options_case(var)
        build_case(var, "    ", "COMPREPLY=") do |cmd, _has_children|
          Completion.visible_options(cmd).flat_map { |o| Completion.expanded_switches(o) }
        end
      end

      def subcommands_case(var)
        build_case(var, "        ", "COMPREPLY+=") do |cmd, has_children|
          cmd.recognised_subcommands.flat_map(&:names) if has_children
        end
      end

      def build_case(var, indent, assign)
        entries = {}
        Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
          words = yield(cmd, has_children)
          next if words.nil? || words.empty?

          path_str = path.map { |sub| sub.names.first }.join("::")
          entries[path_str] = words.join(" ")
        end
        lines = ["#{indent}case \"#{var}\" in"]
        entries.each do |path, words|
          pattern = path.empty? ? '""' : "\"#{path}\""
          lines << "#{indent}    #{pattern})"
          lines << "#{indent}        #{assign}($(compgen -W \"#{words}\" -- \"$cur\"))"
          lines << "#{indent}        ;;"
        end
        lines << "#{indent}esac"
        lines.join("\n")
      end

      def param_count_function
        entries = {}
        Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
          next unless has_children

          entries[path.map { |s| s.names.first }.join("::")] = Completion.required_parameter_count(cmd)
        end
        build_lookup_function("param_count", entries, "echo", default: "echo 0")
      end

      def canonical_function
        aliases = {}
        Completion.walk_command_tree(@command_class) do |cmd, _path, has_children|
          next unless has_children

          cmd.recognised_subcommands.each do |sub|
            sub.names.drop(1).each { |name| aliases[name] = sub.names.first }
          end
        end
        build_lookup_function("canonical", aliases, "echo", default: 'echo "$1"')
      end

      def build_lookup_function(suffix, entries, verb, default:)
        lines = ["#{completion_function}_#{suffix}() {", '    case "$1" in']
        entries.each do |key, value|
          pattern = key.empty? ? '""' : key
          lines << "        #{pattern}) #{verb} #{value.is_a?(String) ? "\"#{value}\"" : value} ;;"
        end
        lines.push("        *) #{default} ;;", "    esac", "}")
        lines.join("\n")
      end

      def takes_value_function
        entries = {}
        Completion.walk_command_tree(@command_class) do |cmd, path, _has_children|
          path_str = path.map { |sub| sub.names.first }.join("::")
          entries[path_str] = Completion.visible_options(cmd).reject(&:flag?)
                                        .flat_map { |o| Completion.expanded_switches(o) }
        end
        lines = [
          "#{completion_function}_takes_value() {",
          '    local option="$1"',
          '    local subcmd="$2"',
          '    case "$subcmd" in'
        ]
        entries.each do |path, switches|
          next if switches.empty?

          pattern = path.empty? ? '""' : "\"#{path}\""
          lines << "        #{pattern})"
          lines << '            case "$option" in'
          lines << "                #{switches.join('|')}) return 0 ;;"
          lines << "            esac"
          lines << "            ;;"
        end
        lines.push("    esac", "    return 1", "}")
        lines.join("\n")
      end

    end

  end
end
