# typed: true
# frozen_string_literal: true

require "dependency"
require "dependencies"
require "requirement"
require "requirements"
require "extend/cachable"

## A dependency is a formula that another formula needs to install.
## A requirement is something other than a formula that another formula
## needs to be present. This includes external language modules,
## command-line tools in the path, or any arbitrary predicate.
##
## The `depends_on` method in the formula DSL is used to declare
## dependencies and requirements.

# This class is used by `depends_on` in the formula DSL to turn dependency
# specifications into the proper kinds of dependencies and requirements.
class DependencyCollector
  extend Cachable

  attr_reader :deps, :requirements

  sig { void }
  def initialize
    # Ensure this is synced with `initialize_dup` and `freeze` (excluding simple objects like integers and booleans)
    @deps = Dependencies.new
    @requirements = Requirements.new

    init_global_dep_tree_if_needed!
  end

  def initialize_dup(other)
    super
    @deps = @deps.dup
    @requirements = @requirements.dup
  end

  def freeze
    @deps.freeze
    @requirements.freeze
    super
  end

  def add(spec)
    case dep = fetch(spec)
    when Array
      dep.compact.each { |dep| @deps << dep }
    when Dependency
      @deps << dep
    when Requirement
      @requirements << dep
    when nil
      # no-op when we have a nil value
      nil
    else
      raise ArgumentError, "DependencyCollector#add passed something that isn't a Dependency or Requirement!"
    end
    dep
  end

  def fetch(spec)
    self.class.cache.fetch(cache_key(spec)) { |key| self.class.cache[key] = build(spec) }
  end

  def cache_key(spec)
    if spec.is_a?(Resource)
      if spec.download_strategy <= CurlDownloadStrategy
        return "#{spec.download_strategy}#{File.extname(spec.url).split("?").first}"
      end

      return spec.download_strategy
    end
    spec
  end

  def build(spec)
    spec, tags = spec.is_a?(Hash) ? spec.first : spec
    parse_spec(spec, Array(tags))
  end

  sig { params(related_formula_names: T::Array[String]).returns(T.nilable(Dependency)) }
  def gcc_dep_if_needed(related_formula_names); end

  sig { params(related_formula_names: T::Array[String]).returns(T.nilable(Dependency)) }
  def glibc_dep_if_needed(related_formula_names); end

  def git_dep_if_needed(tags)
    return if Utils::Git.available?

    Dependency.new("git", [*tags, :implicit])
  end

  def curl_dep_if_needed(tags)
    Dependency.new("curl", [*tags, :implicit])
  end

  def subversion_dep_if_needed(tags)
    return if Utils::Svn.available?

    Dependency.new("subversion", [*tags, :implicit])
  end

  def cvs_dep_if_needed(tags)
    Dependency.new("cvs", [*tags, :implicit]) unless which("cvs")
  end

  def xz_dep_if_needed(tags)
    Dependency.new("xz", [*tags, :implicit]) unless which("xz")
  end

  def zstd_dep_if_needed(tags)
    Dependency.new("zstd", [*tags, :implicit]) unless which("zstd")
  end

  def unzip_dep_if_needed(tags)
    Dependency.new("unzip", [*tags, :implicit]) unless which("unzip")
  end

  def bzip2_dep_if_needed(tags)
    Dependency.new("bzip2", [*tags, :implicit]) unless which("bzip2")
  end

  def self.tar_needs_xz_dependency?
    !new.xz_dep_if_needed([]).nil?
  end

  private

  sig { void }
  def init_global_dep_tree_if_needed!; end

  sig {
    params(spec: T.any(String, Resource, Symbol, Requirement, Dependency, Class),
           tags: T::Array[Symbol]).returns(T.any(Dependency, Requirement, Array, NilClass))
  }
  def parse_spec(spec, tags)
    raise ArgumentError, "Implicit dependencies cannot be manually specified" if tags.include?(:implicit)

    case spec
    when String
      parse_string_spec(spec, tags)
    when Resource
      resource_dep(spec, tags)
    when Symbol
      parse_symbol_spec(spec, tags)
    when Requirement, Dependency
      spec
    when Class
      parse_class_spec(spec, tags)
    end
  end

  def parse_string_spec(spec, tags)
    Dependency.new(spec, tags)
  end

  def parse_symbol_spec(spec, tags)
    # When modifying this list of supported requirements, consider
    # whether Formulary::API_SUPPORTED_REQUIREMENTS should also be changed.
    case spec
    when :arch          then ArchRequirement.new(tags)
    when :codesign      then CodesignRequirement.new(tags)
    when :linux         then LinuxRequirement.new(tags)
    when :macos         then MacOSRequirement.new(tags)
    when :maximum_macos then MacOSRequirement.new(tags, comparator: "<=")
    when :xcode         then XcodeRequirement.new(tags)
    else
      raise ArgumentError, "Unsupported special dependency #{spec.inspect}"
    end
  end

  def parse_class_spec(spec, tags)
    raise TypeError, "#{spec.inspect} is not a Requirement subclass" unless spec < Requirement

    spec.new(tags)
  end

  def resource_dep(spec, tags)
    tags << :build << :test
    strategy = spec.download_strategy

    if strategy <= HomebrewCurlDownloadStrategy
      [curl_dep_if_needed(tags), parse_url_spec(spec.url, tags)]
    elsif strategy <= NoUnzipCurlDownloadStrategy
      # ensure NoUnzip never adds any dependencies
    elsif strategy <= CurlDownloadStrategy
      parse_url_spec(spec.url, tags)
    elsif strategy <= GitDownloadStrategy
      git_dep_if_needed(tags)
    elsif strategy <= SubversionDownloadStrategy
      subversion_dep_if_needed(tags)
    elsif strategy <= MercurialDownloadStrategy
      Dependency.new("mercurial", [*tags, :implicit])
    elsif strategy <= FossilDownloadStrategy
      Dependency.new("fossil", [*tags, :implicit])
    elsif strategy <= BazaarDownloadStrategy
      Dependency.new("breezy", [*tags, :implicit])
    elsif strategy <= CVSDownloadStrategy
      cvs_dep_if_needed(tags)
    elsif strategy < AbstractDownloadStrategy
      # allow unknown strategies to pass through
    else
      raise TypeError, "#{strategy.inspect} is not an AbstractDownloadStrategy subclass"
    end
  end

  def parse_url_spec(url, tags)
    case File.extname(url)
    when ".xz"          then xz_dep_if_needed(tags)
    when ".zst"         then zstd_dep_if_needed(tags)
    when ".zip"         then unzip_dep_if_needed(tags)
    when ".bz2"         then bzip2_dep_if_needed(tags)
    when ".lha", ".lzh" then Dependency.new("lha", [*tags, :implicit])
    when ".lz"          then Dependency.new("lzip", [*tags, :implicit])
    when ".rar"         then Dependency.new("libarchive", [*tags, :implicit])
    when ".7z"          then Dependency.new("p7zip", [*tags, :implicit])
    end
  end
end

require "extend/os/dependency_collector"
