# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.


# Rubygems 1.3.6 removed the 'version' accessor so monkey-patch it back to
# circumvent version validation.  This is needed because Gem::Version doesn't
# accept version specs with dashes.
unless Gem::Version.new("0").respond_to?(:version=)
  class Gem::Version
    def version=(version)
      @version = version.to_s.strip

      # re-prime @segments
      @segments = nil
      segments
    end
  end
end

module Buildr #:nodoc:

  #
  # See ArtifactNamespace#need
  class VersionRequirement

    CMP_PROCS = Gem::Requirement::OPS.dup
    CMP_REGEX = if defined?(Gem::Requirement::OP_RE)
      Gem::Requirement::OP_RE
    else
      Gem::Requirement::OPS.keys.map { |k| Regexp.quote k }.join "|"
    end
    CMP_CHARS = CMP_PROCS.keys.join
    BOOL_CHARS = '\|\&\!'
    VER_CHARS = '\w\.\-'

    class << self
      # is +str+ a version string?
      def version?(str)
        /^\s*r?\d[#{VER_CHARS}]*\s*$/ === str
      end

      # is +str+ a version requirement?
      def requirement?(str)
        /[#{BOOL_CHARS}#{CMP_CHARS}\(\)]/ === str
      end

      # :call-seq:
      #    VersionRequirement.create(" >1 <2 !(1.5) ") -> requirement
      #
      # parse the +str+ requirement
      def create(str)
        instance_eval normalize(str)
      rescue StandardError => e
        raise "Failed to parse #{str.inspect} due to: #{e}"
      end

      private
      def requirement(req)
        unless req =~ /^\s*(#{CMP_REGEX})?\s*([#{VER_CHARS}]+)\s*$/
          raise "Invalid requirement string: #{req}"
        end
        comparator, version = $1, $2
        # dup required due to jruby 1.7.13 bug/feature that caches versions?
        version = Gem::Version.new(0).dup.tap { |v| v.version = version }
        VersionRequirement.new(nil, [$1, version])
      end

      def negate(vreq)
        vreq.negative = !vreq.negative
        vreq
      end

      def normalize(str)
        str = str.strip
        if str[/[^\s\(\)#{BOOL_CHARS + VER_CHARS + CMP_CHARS}]/]
          raise "version string #{str.inspect} contains invalid characters"
        end
        str.gsub!(/\s+(and|\&\&)\s+/, ' & ')
        str.gsub!(/\s+(or|\|\|)\s+/, ' | ')
        str.gsub!(/(^|\s*)not\s+/, ' ! ')
        pattern = /(#{CMP_REGEX})?\s*[#{VER_CHARS}]+/
        left_pattern = /[#{VER_CHARS}\)]$/
        right_pattern = /^(#{pattern}|\()/
        str = str.split.inject([]) do |ary, i|
          ary << '&' if ary.last =~ left_pattern  && i =~ right_pattern
          ary << i
        end
        str = str.join(' ')
        str.gsub!(/!([^=])?/, ' negate \1')
        str.gsub!(pattern) do |expr|
          case expr.strip
          when 'not', 'negate' then 'negate '
          else 'requirement("' + expr + '")'
          end
        end
        str.gsub!(/negate\s+\(/, 'negate(')
        str
      end
    end

    def initialize(op, *requirements) #:nodoc:
      @op, @requirements = op, requirements
    end

    # Is this object a composed requirement?
    #   VersionRequirement.create('1').composed? -> false
    #   VersionRequirement.create('1 | 2').composed? -> true
    #   VersionRequirement.create('1 & 2').composed? -> true
    def composed?
      requirements.size > 1
    end

    # Return the last requirement on this object having an = operator.
    def default
      default = nil
      requirements.reverse.find do |r|
        if Array === r
          if !negative && (r.first.nil? || r.first.include?('='))
            default = r.last.to_s
          end
        else
          default = r.default
        end
      end
      default
    end

    # Test if this requirement can be satisfied by +version+
    def satisfied_by?(version)
      return false unless version
      unless version.kind_of?(Gem::Version)
        raise "Invalid version: #{version.inspect}" unless self.class.version?(version)
        # dup required due to jruby 1.7.13 bug/feature that caches versions?
        version = Gem::Version.new(0).dup.tap { |v| v.version = version.strip }
      end
      message = op == :| ? :any? : :all?
      result = requirements.send message do |req|
        if Array === req
          cmp, rv = *req
          CMP_PROCS[cmp || '='].call(version, rv)
        else
          req.satisfied_by?(version)
        end
      end
      negative ? !result : result
    end

    # Either modify the current requirement (if it's already an or operation)
    # or create a new requirement
    def |(other)
      operation(:|, other)
    end

    # Either modify the current requirement (if it's already an and operation)
    # or create a new requirement
    def &(other)
      operation(:&, other)
    end

    # return the parsed expression
    def to_s
      str = requirements.map(&:to_s).join(" " + @op.to_s + " ").to_s
      str = "( " + str + " )" if negative || requirements.size > 1
      str = "!" + str if negative
      str
    end

    attr_accessor :negative
    protected
    attr_reader :requirements, :op
    def operation(op, other)
      @op ||= op
      if negative == other.negative && @op == op && other.requirements.size == 1
        @requirements << other.requirements.first
        self
      else
        self.class.new(op, self, other)
      end
    end
  end # VersionRequirement
end
