module Puppet::Util::Package::Version
  class Pip
    include Comparable

    VERSION_PATTERN = "
      v?
      (?:
        (?:(?<epoch>[0-9]+)!)?                              # epoch
        (?<release>[0-9]+(?:\\.[0-9]+)*)                    # release segment
        (?<pre>                                             # pre-release
          [-_\\.]?
          (?<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
          [-_\\.]?
          (?<pre_n>[0-9]+)?
        )?
        (?<post>                                            # post release
          (?:-(?<post_n1>[0-9]+))
          |
          (?:
            [-_\\.]?
            (?<post_l>post|rev|r)
            [-_\\.]?
            (?<post_n2>[0-9]+)?
          )
        )?
        (?<dev>                                             # dev release
          [-_\\.]?
          (?<dev_l>dev)
          [-_\\.]?
          (?<dev_n>[0-9]+)?
        )?
      )
      (?:\\+(?<local>[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?      # local version
    ".freeze

    def self.parse(version)
      raise ValidationFailure, version.to_s unless version.is_a? String

      matched = version.match(Regexp.new(("^\\s*") + VERSION_PATTERN + ("\\s*$"), Regexp::EXTENDED | Regexp::MULTILINE | Regexp::IGNORECASE))
      raise ValidationFailure, version unless matched

      new(matched)
    end

    def self.compare(version_a, version_b)
      version_a = parse(version_a) unless version_a.is_a?(self)
      version_b = parse(version_b) unless version_b.is_a?(self)

      version_a <=> version_b
    end

    def to_s
      parts = []

      parts.push("#{@epoch_data}!")           if @epoch_data && @epoch_data != 0
      parts.push(@release_data.join("."))     if @release_data
      parts.push(@pre_data.join)              if @pre_data
      parts.push(".post#{@post_data[1]}")     if @post_data
      parts.push(".dev#{@dev_data[1]}")       if @dev_data
      parts.push("+#{@local_data.join(".")}") if @local_data

      parts.join
    end
    alias inspect to_s

    def eql?(other)
      other.is_a?(self.class) && key.eql?(other.key)
    end
    alias == eql?

    def <=>(other)
      raise ValidationFailure, other.to_s unless other.is_a?(self.class)
      compare(key, other.key)
    end

    attr_reader :key

    private

    def initialize(matched)
      @epoch_data   = matched[:epoch].to_i
      @release_data = matched[:release].split('.').map(&:to_i)                                       if matched[:release]
      @pre_data     = parse_letter_version(matched[:pre_l], matched[:pre_n])                         if matched[:pre_l]  || matched[:pre_n]
      @post_data    = parse_letter_version(matched[:post_l], matched[:post_n1] || matched[:post_n2]) if matched[:post_l] || matched[:post_n1] || matched[:post_n2]
      @dev_data     = parse_letter_version(matched[:dev_l], matched[:dev_n])                         if matched[:dev_l]  || matched[:dev_n]
      @local_data   = parse_local_version(matched[:local])                                           if matched[:local]

      @key = compose_key(@epoch_data, @release_data, @pre_data, @post_data, @dev_data, @local_data)
    end

    def parse_letter_version(letter, number)
      if letter
        number = 0 if !number
        letter.downcase!

        if letter == "alpha"
          letter = "a"
        elsif letter == "beta"
          letter = "b"
        elsif ["c", "pre", "preview"].include?(letter)
          letter = "rc"
        elsif ["rev", "r"].include?(letter)
          letter = "post"
        end

        return [letter, number.to_i]
      end

      ["post", number.to_i] if !letter && number
    end

    def parse_local_version(local_version)
      local_version.split(/[\\._-]/).map{|part| part =~ /[0-9]+/ && part !~ /[a-zA-Z]+/ ? part.to_i : part.downcase} if local_version
    end

    def compose_key(epoch, release, pre, post, dev, local)
      release_key = release.reverse
      release_key.each_with_index do |element, index|
        break unless element == 0
        release_key.delete_at(index) unless release_key.at(index + 1) != 0
      end
      release_key.reverse!

      if !pre && !post && dev
        pre_key = -Float::INFINITY
      else
        pre_key = pre || Float::INFINITY
      end

      post_key = post || -Float::INFINITY

      dev_key = dev || Float::INFINITY

      if !local
        local_key = [[-Float::INFINITY, ""]]
      else
        local_key = local.map{|i| (i.is_a? Integer) ? [i, ""] : [-Float::INFINITY, i]}
      end

      [epoch, release_key, pre_key, post_key, dev_key, local_key]
    end

    def compare(this, other)
      if (this.is_a? Array) && (other.is_a? Array)
        this  << -Float::INFINITY if this.length < other.length
        other << -Float::INFINITY if this.length > other.length

        this.each_with_index do |element, index|
          return compare(element, other.at(index)) if element != other.at(index)
        end
      elsif (this.is_a? Array) && !(other.is_a? Array)
        raise Puppet::Error, "Cannot compare #{this} (Array) with #{other} (#{other.class}). Only ±Float::INFINITY accepted." unless other.abs == Float::INFINITY
        return other == -Float::INFINITY ? 1 : -1
      elsif !(this.is_a? Array) && (other.is_a? Array)
        raise Puppet::Error, "Cannot compare #{this} (#{this.class}) with #{other} (Array). Only ±Float::INFINITY accepted." unless this.abs == Float::INFINITY
        return this == -Float::INFINITY ? -1 : 1
      end
      this <=> other
    end

    class ValidationFailure < ArgumentError
      def initialize(version)
        super("#{version} is not a valid python package version. Please refer to https://www.python.org/dev/peps/pep-0440/.")
      end
    end
  end
end
