# frozen_string_literal: true

module Clamp
  module Completion

    # Generates fish shell completion scripts.
    class FishGenerator

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

      def generate
        lines = []
        lines << "# Fish completions for #{@executable_name}"
        lines << "# Generated by Clamp"
        lines << ""
        helpers = [subcmd_args_function]

        # Track visible option switches at each depth, so we can identify
        # which options are new at each level. Depth-first walk order means
        # the entry at depth N-1 is always the current node's parent.
        switches_at_depth = []

        Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
          generate_option_completions(lines, cmd, path, switches_at_depth)

          next unless has_children

          # Subcommand names need an exclusive condition (only at this level).
          child_names = cmd.recognised_subcommands.flat_map(&:names)
          exclusive_cond = condition_for(path, child_names)
          subcmd_cond = subcommand_condition(cmd, path, exclusive_cond, helpers)
          generate_subcommand_completions(lines, cmd, subcmd_cond)
          lines << ""
        end
        "#{helpers.join("\n\n")}\n\n#{lines.join("\n")}\n"
      end

      private

      def subcommand_condition(cmd, path, condition, helpers)
        param_count = Completion.required_parameter_count(cmd)
        return condition unless param_count.positive?

        helper = ParamsSatisfiedHelper.new(@executable_name, path, param_count)
        helpers << helper.to_s
        "#{condition}; and #{helper.function_name}"
      end

      def generate_subcommand_completions(lines, cmd, condition)
        cmd.recognised_subcommands.each do |sub|
          sub.names.each do |name|
            lines << "complete -c #{@executable_name} -f -n '#{condition}' -a #{name} " \
                     "-d '#{escape(sub.description)}'"
          end
        end
      end

      # Emit only options new at this level (not inherited from parent).
      # Use an inclusive condition so they also apply to child subcommands.
      def generate_option_completions(lines, cmd, path, switches_at_depth)
        depth = path.length
        visible = Completion.visible_options(cmd)
        parent_switches = depth.positive? ? switches_at_depth[depth - 1] : nil
        new_options = if parent_switches
                        visible.reject { |o| parent_switches.include?(o.switches) }
                      else
                        visible
                      end
        option_cond = depth.positive? ? inclusive_condition_for(path) : nil
        new_options.each do |option|
          lines << option_completion(option, option_cond)
        end
        switches_at_depth[depth] = Set.new(visible.map(&:switches))
      end

      # Condition that matches this level AND all child subcommands.
      def inclusive_condition_for(path)
        path.map { |sub| "#{completion_function}_seen_subcommand_from #{sub.names.join(' ')}" }.join("; and ")
      end

      # Condition that matches this level only (excludes child subcommands).
      def condition_for(path, child_names)
        if path.empty?
          "not #{completion_function}_subcmd_args >/dev/null"
        else
          parts = path.map { |sub| "#{completion_function}_seen_subcommand_from #{sub.names.join(' ')}" }
          parts << "not #{completion_function}_seen_subcommand_from #{child_names.join(' ')}" if child_names.any?
          parts.join("; and ")
        end
      end

      def completion_function
        "_clamp_complete_#{@executable_name}"
      end

      def subcmd_args_function
        optspecs = argparse_optspecs(@command_class)
        lines = [
          "function #{completion_function}_subcmd_args",
          "    set -l tokens (commandline -opc)",
          "    set -e tokens[1]",
          "    argparse -si #{optspecs.map { |s| "'#{s}'" }.join(' ')} -- $tokens 2>/dev/null",
          "    or return 1",
          "    test (count $argv) -gt 0",
          "    and printf '%s\\n' $argv",
          "end",
          "",
          "function #{completion_function}_seen_subcommand_from",
          "    for p in (#{completion_function}_subcmd_args)",
          "        if contains -- $p $argv",
          "            return 0",
          "        end",
          "    end",
          "    return 1",
          "end"
        ]
        lines.join("\n")
      end

      def argparse_optspecs(command_class)
        command_class.recognised_options.flat_map do |option|
          Completion.argparse_specs_for(option)
        end
      end

      def option_completion(option, condition = nil)
        parts = ["complete -c #{@executable_name} -f"]
        parts << "-n '#{condition}'" if condition

        Completion.expanded_switches(option).each do |switch|
          parts << if switch.start_with?("--")
                     "-l #{switch.sub(/^--/, '')}"
                   else
                     "-s #{switch.sub(/^-/, '')}"
                   end
        end

        parts << "-r" unless option.flag?
        parts << "-d '#{escape(option.description)}'"

        parts.join(" ")
      end

      def escape(str)
        str.gsub("'", "\\\\'")
      end

    end

    # Generates a fish function that checks whether required parameters
    # have been provided before offering subcommand completions.
    class ParamsSatisfiedHelper

      def initialize(executable_name, path, required_count)
        @executable_name = executable_name
        @path = path
        @required_count = required_count
      end

      def function_name
        parts = [@executable_name]
        @path.each { |sub| parts << sub.names.first }
        "_clamp_complete_#{parts.join('_')}_params_satisfied"
      end

      def to_s
        lines = ["function #{function_name}", "    set -l tokens (commandline -opc)", "    set -l positional 0"]
        if @path.empty?
          count_positional_root(lines)
        else
          count_positional_nested(lines)
        end
        lines.push("    test $positional -ge #{@required_count}", "end")
        lines.join("\n")
      end

      private

      def count_positional_root(lines)
        lines.push("    for token in $tokens[2..]",
                   "        if not string match -q -- '-*' $token",
                   "            set positional (math $positional + 1)",
                   "        end", "    end")
      end

      def count_positional_nested(lines)
        lines.push("    set -l depth 0", "    for token in $tokens[2..]", "        switch $depth")
        @path.each_with_index do |sub, i|
          all_names = sub.names.join(" ")
          lines.push("            case #{i}",
                     "                if contains -- $token #{all_names}",
                     "                    set depth #{i + 1}", "                end")
        end
        lines.push("            case #{@path.length}",
                   "                if not string match -q -- '-*' $token",
                   "                    set positional (math $positional + 1)",
                   "                end", "        end", "    end")
      end

    end

  end
end
