require "set"
require "formula"
require "formula_versions"

class Descriptions
  CACHE_FILE = HOMEBREW_CACHE + "desc_cache.json"

  def self.cache
    @cache || load_cache
  end

  # If the cache file exists, load it into, and return, a hash; otherwise,
  # return nil.
  def self.load_cache
    @cache = JSON.parse(CACHE_FILE.read) if CACHE_FILE.exist?
  end

  # Write the cache to disk after ensuring the existence of the containing
  # directory.
  def self.save_cache
    HOMEBREW_CACHE.mkpath
    CACHE_FILE.atomic_write JSON.dump(@cache)
  end

  # Create a hash mapping all formulae to their descriptions;
  # save it for future use.
  def self.generate_cache
    @cache = {}
    Formula.each do |f|
      @cache[f.full_name] = f.desc
    end
    save_cache
  end

  # Return true if the cache exists, and none of the Taps
  # repos were updated more recently than it was.
  def self.cache_fresh?
    return false unless CACHE_FILE.exist?
    cache_mtime = File.mtime(CACHE_FILE)

    Tap.each do |tap|
      next unless tap.git?
      repo_mtime = File.mtime(tap.path/".git/refs/heads/master")
      return false if repo_mtime > cache_mtime
    end

    true
  end

  # Create the cache if it doesn't already exist.
  def self.ensure_cache
    generate_cache unless cache_fresh? && cache
  end

  # Take a {Report}, as generated by cmd/update.rb.
  # Unless the cache file exists, do nothing.
  # If it does exist, but the Report is empty, just touch the cache file.
  # Otherwise, use the report to update the cache.
  def self.update_cache(report)
    return unless CACHE_FILE.exist?

    if report.empty?
      FileUtils.touch CACHE_FILE
    else
      renamings = report.select_formula(:R)
      alterations = report.select_formula(:A) + report.select_formula(:M) +
                    renamings.map(&:last)
      cache_formulae(alterations, save: false)
      uncache_formulae(report.select_formula(:D) +
                            renamings.map(&:first))
    end
  end

  # Given an array of formula names, add them and their descriptions to the
  # cache. Save the updated cache to disk, unless explicitly told not to.
  def self.cache_formulae(formula_names, options = { save: true })
    return unless cache

    formula_names.each do |name|
      begin
        desc = Formulary.factory(name).desc
      rescue FormulaUnavailableError, *FormulaVersions::IGNORED_EXCEPTIONS
      end
      @cache[name] = desc
    end
    save_cache if options[:save]
  end

  # Given an array of formula names, remove them and their descriptions from
  # the cache. Save the updated cache to disk, unless explicitly told not to.
  def self.uncache_formulae(formula_names, options = { save: true })
    return unless cache
    formula_names.each { |name| @cache.delete(name) }
    save_cache if options[:save]
  end

  # Given a regex, find all formulae whose specified fields contain a match.
  def self.search(regex, field = :either)
    ensure_cache

    results = case field
    when :name
      @cache.select { |name, _| name =~ regex }
    when :desc
      @cache.select { |_, desc| desc =~ regex }
    when :either
      @cache.select { |name, desc| (name =~ regex) || (desc =~ regex) }
    end

    new(results)
  end

  # Create an actual instance.
  def initialize(descriptions)
    @descriptions = descriptions
  end

  # Take search results -- a hash mapping formula names to descriptions -- and
  # print them.
  def print
    blank = Formatter.warning("[no description]")
    @descriptions.keys.sort.each do |full_name|
      short_name = short_names[full_name]
      printed_name = short_name_counts[short_name] == 1 ? short_name : full_name
      description = @descriptions[full_name] || blank
      puts "#{Tty.bold}#{printed_name}:#{Tty.reset} #{description}"
    end
  end

  private

  def short_names
    @short_names ||= Hash[@descriptions.keys.map { |k| [k, k.split("/").last] }]
  end

  def short_name_counts
    @short_name_counts ||=
      short_names.values.each_with_object(Hash.new(0)) { |name, counts| counts[name] += 1 }
  end
end
