# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true

require "description_cache_store"

module Homebrew
  # Helper module for searching formulae or casks.
  module Search
    def self.query_regexp(query)
      if (m = query.match(%r{^/(.*)/$}))
        Regexp.new(m[1])
      else
        query
      end
    rescue RegexpError
      raise "#{query} is not a valid regex."
    end

    def self.search_descriptions(string_or_regex, args, search_type: :desc)
      both = !args.formula? && !args.cask?
      eval_all = args.eval_all? || Homebrew::EnvConfig.eval_all?

      if args.formula? || both
        ohai "Formulae"
        if eval_all
          CacheStoreDatabase.use(:descriptions) do |db|
            cache_store = DescriptionCacheStore.new(db)
            Descriptions.search(string_or_regex, search_type, cache_store, eval_all).print
          end
        else
          unofficial = Tap.all.sum { |tap| tap.official? ? 0 : tap.formula_files.size }
          if unofficial.positive?
            opoo "Use `--eval-all` to search #{unofficial} additional " \
                 "#{Utils.pluralize("formula", unofficial, plural: "e")} in third party taps."
          end
          descriptions = Homebrew::API::Formula.all_formulae.transform_values { |data| data["desc"] }
          Descriptions.search(string_or_regex, search_type, descriptions, eval_all, cache_store_hash: true).print
        end
      end
      return if !args.cask? && !both

      puts if both

      ohai "Casks"
      if eval_all
        CacheStoreDatabase.use(:cask_descriptions) do |db|
          cache_store = CaskDescriptionCacheStore.new(db)
          Descriptions.search(string_or_regex, search_type, cache_store, eval_all).print
        end
      else
        unofficial = Tap.all.sum { |tap| tap.official? ? 0 : tap.cask_files.size }
        if unofficial.positive?
          opoo "Use `--eval-all` to search #{unofficial} additional " \
               "#{Utils.pluralize("cask", unofficial)} in third party taps."
        end
        descriptions = Homebrew::API::Cask.all_casks.transform_values { |c| [c["name"].join(", "), c["desc"]] }
        Descriptions.search(string_or_regex, search_type, descriptions, eval_all, cache_store_hash: true).print
      end
    end

    def self.search_formulae(string_or_regex)
      if string_or_regex.is_a?(String) && string_or_regex.match?(HOMEBREW_TAP_FORMULA_REGEX)
        return begin
          [Formulary.factory(string_or_regex).name]
        rescue FormulaUnavailableError
          []
        end
      end

      aliases = Formula.alias_full_names
      results = search(Formula.full_names + aliases, string_or_regex).sort
      results |= Formula.fuzzy_search(string_or_regex).map { |n| Formulary.factory(n).full_name }

      results.filter_map do |name|
        formula, canonical_full_name = begin
          f = Formulary.factory(name)
          [f, f.full_name]
        rescue
          [nil, name]
        end

        # Ignore aliases from results when the full name was also found
        next if aliases.include?(name) && results.include?(canonical_full_name)

        if formula&.any_version_installed?
          pretty_installed(name)
        elsif formula.nil? || formula.valid_platform?
          name
        end
      end
    end

    def self.search_casks(string_or_regex)
      if string_or_regex.is_a?(String) && string_or_regex.match?(HOMEBREW_TAP_CASK_REGEX)
        return begin
          [Cask::CaskLoader.load(string_or_regex).token]
        rescue Cask::CaskUnavailableError
          []
        end
      end

      cask_tokens = Tap.each_with_object([]) do |tap, array|
        # We can exclude the core cask tap because `CoreCaskTap#cask_tokens` returns short names by default.
        if tap.official? && !tap.core_cask_tap?
          tap.cask_tokens.each { |token| array << token.sub(%r{^homebrew/cask.*/}, "") }
        else
          tap.cask_tokens.each { |token| array << token }
        end
      end.uniq

      results = search(cask_tokens, string_or_regex)
      results += DidYouMean::SpellChecker.new(dictionary: cask_tokens)
                                         .correct(string_or_regex)

      results.sort.map do |name|
        cask = Cask::CaskLoader.load(name)
        if cask.installed?
          pretty_installed(cask.full_name)
        else
          cask.full_name
        end
      end.uniq
    end

    def self.search_names(string_or_regex, args)
      both = !args.formula? && !args.cask?

      all_formulae = if args.formula? || both
        search_formulae(string_or_regex)
      else
        []
      end

      all_casks = if args.cask? || both
        search_casks(string_or_regex)
      else
        []
      end

      [all_formulae, all_casks]
    end

    def self.search(selectable, string_or_regex, &block)
      case string_or_regex
      when Regexp
        search_regex(selectable, string_or_regex, &block)
      else
        search_string(selectable, string_or_regex.to_str, &block)
      end
    end

    def self.simplify_string(string)
      string.downcase.gsub(/[^a-z\d@+]/i, "")
    end

    def self.search_regex(selectable, regex)
      selectable.select do |*args|
        args = yield(*args) if block_given?
        args = Array(args).flatten.compact
        args.any? { |arg| arg.match?(regex) }
      end
    end

    def self.search_string(selectable, string)
      simplified_string = simplify_string(string)
      selectable.select do |*args|
        args = yield(*args) if block_given?
        args = Array(args).flatten.compact
        args.any? { |arg| simplify_string(arg).include?(simplified_string) }
      end
    end
  end
end
