#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++

require 'rubygems/version'

##
# Requirement version includes a prefaced comparator in addition
# to a version number.
#
# A Requirement object can actually contain multiple, er,
# requirements, as in (> 1.2, < 2.0).

class Gem::Requirement

  include Comparable

  attr_reader :requirements

  OPS = {
    "="  =>  lambda { |v, r| v == r },
    "!=" =>  lambda { |v, r| v != r },
    ">"  =>  lambda { |v, r| v > r },
    "<"  =>  lambda { |v, r| v < r },
    ">=" =>  lambda { |v, r| v >= r },
    "<=" =>  lambda { |v, r| v <= r },
    "~>" =>  lambda { |v, r| v >= r && v < r.bump }
  }

  OP_RE = /#{OPS.keys.map{ |k| Regexp.quote k }.join '|'}/o

  ##
  # Factory method to create a Gem::Requirement object.  Input may be a
  # Version, a String, or nil.  Intended to simplify client code.
  #
  # If the input is "weird", the default version requirement is returned.

  def self.create(input)
    case input
    when Gem::Requirement then
      input
    when Gem::Version, Array then
      new input
    else
      if input.respond_to? :to_str then
        self.new [input.to_str]
      else
        self.default
      end
    end
  end

  ##
  # A default "version requirement" can surely _only_ be '>= 0'.
  #--
  # This comment once said:
  #
  # "A default "version requirement" can surely _only_ be '> 0'."

  def self.default
    self.new ['>= 0']
  end

  ##
  # Constructs a Requirement from +requirements+ which can be a String, a
  # Gem::Version, or an Array of those.  See parse for details on the
  # formatting of requirement strings.

  def initialize(requirements)
    @requirements = case requirements
                    when Array then
                      requirements.map do |requirement|
                        parse(requirement)
                      end
                    else
                      [parse(requirements)]
                    end
    @version = nil   # Avoid warnings.
  end

  ##
  # Marshal raw requirements, rather than the full object

  def marshal_dump # :nodoc:
    [@requirements]
  end

  ##
  # Load custom marshal format

  def marshal_load(array) # :nodoc:
    @requirements = array[0]
    @version = nil
  end

  def to_s # :nodoc:
    as_list.join(", ")
  end

  def as_list
    normalize
    @requirements.collect { |req|
      "#{req[0]} #{req[1]}"
    }
  end

  def normalize
    return if not defined? @version or @version.nil?
    @requirements = [parse(@version)]
    @nums = nil
    @version = nil
    @op = nil
  end

  ##
  # True if this requirement satisfied by the Gem::Version +version+.

  def satisfied_by?(version)
    normalize
    @requirements.all? { |op, rv| satisfy?(op, version, rv) }
  end

  ##
  # Is "+version+ +op+ +required_version+" satisfied?

  def satisfy?(op, version, required_version)
    OPS[op].call(version, required_version)
  end

  ##
  # Parse the version requirement obj returning the operator and version.
  #
  # The requirement can be a String or a Gem::Version.  A String can be an
  # operator (<, <=, =, =>, >, !=, ~>), a version number, or both, operator
  # first.

  def parse(obj)
    case obj
    when /^\s*(#{OP_RE})\s*([0-9.]+)\s*$/o then
      [$1, Gem::Version.new($2)]
    when /^\s*([0-9.]+)\s*$/ then
      ['=', Gem::Version.new($1)]
    when /^\s*(#{OP_RE})\s*$/o then
      [$1, Gem::Version.new('0')]
    when Gem::Version then
      ['=', obj]
    else
      fail ArgumentError, "Illformed requirement [#{obj.inspect}]"
    end
  end

  def <=>(other) # :nodoc:
    to_s <=> other.to_s
  end

  def hash # :nodoc:
    to_s.hash
  end

end

