# typed: strict
# frozen_string_literal: true

require "requirement"
require "utils/output"

# A requirement on macOS.
class MacOSRequirement < Requirement
  extend Utils::Output::Mixin

  Cache = type_template { { fixed: T::Hash[String, T.untyped] } }

  fatal true

  sig { returns(String) }
  attr_reader :comparator

  sig { returns(T.nilable(T.any(MacOSVersion, T::Array[MacOSVersion]))) }
  attr_reader :version

  # Keep these around as empty arrays so we can keep the deprecation/disabling code the same.
  # Treat these like odeprecated/odisabled in terms of deprecation/disabling.
  DISABLED_MACOS_VERSIONS = [
    :mojave,
    :high_sierra,
    :sierra,
    :el_capitan,
  ].freeze
  DEPRECATED_MACOS_VERSIONS = T.let([].freeze, T::Array[Symbol])

  sig { params(args: T::Array[T.any(String, Symbol)], comparator: String).returns(MacOSRequirement) }
  def self.parse(args, comparator:)
    first_arg = args.first
    first_arg_s = first_arg&.to_s

    if first_arg == :any
      new
    elsif args.count > 1
      new([args], comparator: "==")
    elsif first_arg.is_a?(Symbol) && MacOSVersion::SYMBOLS.key?(first_arg)
      new([first_arg], comparator:)
    elsif (md = /^\s*(?<comparator><|>|[=<>]=)\s*:(?<version>\S+)\s*$/.match(first_arg_s))
      replacement = if md[:comparator] == "<="
        "`depends_on maximum_macos: :#{md[:version]}`"
      elsif md[:comparator] == ">="
        "`depends_on macos: :#{md[:version]}`"
      end
      odeprecated "string comparison format for `depends_on macos:`", replacement
      new([T.must(md[:version]).to_sym], comparator: T.must(md[:comparator]))
    elsif (md = /^\s*(?<comparator><|>|[=<>]=)\s*(?<version>\S+)\s*$/.match(first_arg_s))
      odeprecated "string comparison format for `depends_on macos:`"
      new([md[:version]], comparator: T.must(md[:comparator]))
    else
      odeprecated "strict symbol format for `depends_on macos:`"
      new([first_arg], comparator: "==")
    end
  end

  sig { params(tags: T.untyped, comparator: String).void }
  def initialize(tags = [], comparator: ">=")
    @version = T.let(begin
      if comparator == "==" && tags.first.respond_to?(:map)
        tags.first.map { |s| MacOSVersion.from_symbol(s) }
      else
        MacOSVersion.from_symbol(tags.first) unless tags.empty?
      end
    rescue MacOSVersion::Error => e
      if DISABLED_MACOS_VERSIONS.include?(e.version)
        # This odisabled should stick around indefinitely.
        odisabled "`depends_on macos: :#{e.version}`"
      elsif DEPRECATED_MACOS_VERSIONS.include?(e.version)
        # This odeprecated should stick around indefinitely.
        odeprecated "`depends_on macos: :#{e.version}`"
      else
        raise
      end

      # Array of versions: remove the bad ones and try again.
      if tags.first.respond_to?(:reject)
        tags = [tags.first.reject { |s| s == e.version }, tags[1..]]
        retry
      end

      # Otherwise fallback to the oldest allowed if comparator is >=.
      MacOSVersion.new(HOMEBREW_MACOS_OLDEST_ALLOWED) if comparator == ">="
    end, T.nilable(T.any(MacOSVersion, T::Array[MacOSVersion])))

    @comparator = comparator
    super(tags.drop(1))
  end

  sig { returns(T::Boolean) }
  def version_specified?
    @version.present?
  end

  satisfy(build_env: false) do
    T.bind(self, MacOSRequirement)
    macos_version_satisfied?
  end

  sig { returns(T::Boolean) }
  def macos_version_satisfied? = false

  sig { returns(T.nilable(MacOSVersion)) }
  def minimum_version
    return MacOSVersion.new(HOMEBREW_MACOS_OLDEST_ALLOWED) if @comparator == "<=" || !version_specified?
    return T.unsafe(@version).min if @version.respond_to?(:to_ary) || @version.is_a?(Array)

    @version
  end

  sig { returns(T.nilable(MacOSVersion)) }
  def maximum_version
    return MacOSVersion.new(HOMEBREW_MACOS_NEWEST_UNSUPPORTED) if @comparator == ">=" || !version_specified?
    return T.unsafe(@version).max if @version.respond_to?(:to_ary) || @version.is_a?(Array)

    @version
  end

  sig { params(other: MacOSVersion).returns(T::Boolean) }
  def allows?(other)
    return true unless version_specified?

    version = @version
    case @comparator
    when ">=" then other >= T.cast(version, MacOSVersion)
    when "<=" then other <= T.cast(version, MacOSVersion)
    else
      return T.unsafe(version).include?(other) if version.respond_to?(:to_ary) || version.is_a?(Array)

      version == other
    end
  end

  sig { override.params(type: Symbol).returns(String) }
  def message(type: :formula)
    subject = (type == :cask) ? "This cask" : "This formula"
    "#{subject} requires macOS."
  end

  sig { override.params(other: T.untyped).returns(T::Boolean) }
  def ==(other)
    super && comparator == other.comparator && version == other.version
  end
  alias eql? ==

  sig { override.returns(Integer) }
  def hash
    [super, comparator, version].hash
  end

  sig { returns(String) }
  def inspect
    "#<#{self.class.name}: version#{@comparator}#{@version.to_s.inspect} #{tags.inspect}>"
  end

  sig { returns(String) }
  def display_s
    if version_specified?
      if @version.respond_to?(:to_ary) || @version.is_a?(Array)
        "macOS #{@comparator} #{T.unsafe(@version).join(" / ")}"
      else
        "macOS #{@comparator} #{@version}"
      end
    else
      "macOS"
    end
  end

  sig { returns(T::Hash[String, T::Array[String]]) }
  def to_h
    return {} unless version_specified?

    comp = @comparator.to_s
    return { comp => @version.map(&:to_s) } if @version.is_a?(Array)

    { comp => [@version.to_s] }
  end

  sig { params(options: T.untyped).returns(String) }
  def to_json(options)
    to_h.to_json(options)
  end
end

require "extend/os/requirements/macos_requirement"
