# frozen_string_literal: true

module Puppet::Pops
module Types
# An unparameterized type that represents all VersionRange instances
#
# @api public
class PSemVerRangeType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType')
  end

  # Check if a version is included in a version range. The version can be a string or
  # a `SemanticPuppet::SemVer`
  #
  # @param range [SemanticPuppet::VersionRange] the range to match against
  # @param version [SemanticPuppet::Version,String] the version to match
  # @return [Boolean] `true` if the range includes the given version
  #
  # @api public
  def self.include?(range, version)
    case version
    when SemanticPuppet::Version
      range.include?(version)
    when String
      begin
        range.include?(SemanticPuppet::Version.parse(version))
      rescue SemanticPuppet::Version::ValidationFailure
        false
      end
    else
      false
    end
  end

  # Creates a {SemanticPuppet::VersionRange} from the given _version_range_ argument. If the argument is `nil` or
  # a {SemanticPuppet::VersionRange}, it is returned. If it is a {String}, it will be parsed into a
  # {SemanticPuppet::VersionRange}. Any other class will raise an {ArgumentError}.
  #
  # @param version_range [SemanticPuppet::VersionRange,String,nil] the version range to convert
  # @return [SemanticPuppet::VersionRange] the converted version range
  # @raise [ArgumentError] when the argument cannot be converted into a version range
  #
  def self.convert(version_range)
    case version_range
    when nil, SemanticPuppet::VersionRange
      version_range
    when String
      SemanticPuppet::VersionRange.parse(version_range)
    else
      raise ArgumentError, "Unable to convert a #{version_range.class.name} to a SemVerRange"
    end
  end

  # Checks if range _a_ is a sub-range of (i.e. completely covered by) range _b_
  # @param a [SemanticPuppet::VersionRange] the first range
  # @param b [SemanticPuppet::VersionRange] the second range
  #
  # @return [Boolean] `true` if _a_ is completely covered by _b_
  def self.covered_by?(a, b)
    b.begin <= a.begin && (b.end > a.end || b.end == a.end && (!b.exclude_end? || a.exclude_end?))
  end

  # Merge two ranges so that the result matches all versions matched by both. A merge
  # is only possible when the ranges are either adjacent or have an overlap.
  #
  # @param a [SemanticPuppet::VersionRange] the first range
  # @param b [SemanticPuppet::VersionRange] the second range
  # @return [SemanticPuppet::VersionRange,nil] the result of the merge
  #
  # @api public
  def self.merge(a, b)
    if a.include?(b.begin) || b.include?(a.begin)
      max = [a.end, b.end].max
      exclude_end = false
      if a.exclude_end?
        exclude_end = max == a.end && (max > b.end || b.exclude_end?)
      elsif b.exclude_end?
        exclude_end = max == b.end && (max > a.end || a.exclude_end?)
      end
      SemanticPuppet::VersionRange.new([a.begin, b.begin].min, max, exclude_end)
    elsif a.exclude_end? && a.end == b.begin
      # Adjacent, a before b
      SemanticPuppet::VersionRange.new(a.begin, b.end, b.exclude_end?)
    elsif b.exclude_end? && b.end == a.begin
      # Adjacent, b before a
      SemanticPuppet::VersionRange.new(b.begin, a.end, a.exclude_end?)
    else
      # No overlap
      nil
    end
  end

  def roundtrip_with_string?
    true
  end

  def instance?(o, guard = nil)
    o.is_a?(SemanticPuppet::VersionRange)
  end

  def eql?(o)
    self.class == o.class
  end

  def hash?
    super ^ @version_range.hash
  end

  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_VersionRange, type.loader) do
      local_types do
        type 'SemVerRangeString = String[1]'
        type 'SemVerRangeHash = Struct[{min=>Variant[Default,SemVer],Optional[max]=>Variant[Default,SemVer],Optional[exclude_max]=>Boolean}]'
      end

      # Constructs a VersionRange from a String with a format specified by
      #
      # https://github.com/npm/node-semver#range-grammar
      #
      # The logical or || operator is not implemented since it effectively builds
      # an array of ranges that may be disparate. The {{SemanticPuppet::VersionRange}} inherits
      # from the standard ruby range. It must be possible to describe that range in terms
      # of min, max, and exclude max.
      #
      # The Puppet Version type is parameterized and accepts multiple ranges so creating such
      # constraints is still possible. It will just require several parameters rather than one
      # parameter containing the '||' operator.
      #
      dispatch :from_string do
        param 'SemVerRangeString', :str
      end

      # Constructs a VersionRange from a min, and a max version. The Boolean argument denotes
      # whether or not the max version is excluded or included in the range. It is included by
      # default.
      #
      dispatch :from_versions do
        param 'Variant[Default,SemVer]', :min
        param 'Variant[Default,SemVer]', :max
        optional_param 'Boolean', :exclude_max
      end

      # Same as #from_versions but each argument is instead given in a Hash
      #
      dispatch :from_hash do
        param 'SemVerRangeHash', :hash_args
      end

      def from_string(str)
        SemanticPuppet::VersionRange.parse(str)
      end

      def from_versions(min, max = :default, exclude_max = false)
        min = SemanticPuppet::Version::MIN if min == :default
        max = SemanticPuppet::Version::MAX if max == :default
        SemanticPuppet::VersionRange.new(min, max, exclude_max)
      end

      def from_hash(hash)
        from_versions(hash['min'], hash.fetch('max', :default), hash.fetch('exclude_max', false))
      end
    end
  end

  DEFAULT = PSemVerRangeType.new

  protected

  def _assignable?(o, guard)
    self == o
  end

  def self.range_pattern
    part = '(?<part>[0-9A-Za-z-]+)'
    parts = "(?<parts>#{part}(?:\\.\\g<part>)*)"

    qualifier = "(?:-#{parts})?(?:\\+\\g<parts>)?"

    xr = '(?<xr>[xX*]|0|[1-9][0-9]*)'
    partial = "(?<partial>#{xr}(?:\\.\\g<xr>(?:\\.\\g<xr>#{qualifier})?)?)"

    hyphen = "(?:#{partial}\\s+-\\s+\\g<partial>)"
    simple = "(?<simple>(?:<|>|>=|<=|~|\\^)?\\g<partial>)"

    "#{hyphen}|#{simple}(?:\\s+\\g<simple>)*"
  end
  private_class_method :range_pattern
end
end
end
