#
# Author:: Seth Chisamore (<schisamo@chef.io>)
# Copyright:: Copyright (c) 2013 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed 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.
#

require 'mixlib/versioning/format/git_describe'
require 'mixlib/versioning/format/opscode_semver'
require 'mixlib/versioning/format/rubygems'
require 'mixlib/versioning/format/semver'

module Mixlib
  class Versioning
    # @author Seth Chisamore (<schisamo@chef.io>)
    #
    # @!attribute [r] major
    #   @return [Integer] major identifier
    # @!attribute [r] minor
    #   @return [Integer] minor identifier
    # @!attribute [r] patch
    #   @return [Integer] patch identifier
    # @!attribute [r] prerelease
    #   @return [String] pre-release identifier
    # @!attribute [r] build
    #   @return [String] build identifier
    # @!attribute [r] iteration
    #   @return [String] build interation
    # @!attribute [r] input
    #   @return [String] original input version string that was parsed
    class Format
      include Comparable

      # Returns the {Mixlib::Versioning::Format} class that maps to the given
      # format type.
      #
      # @example
      #   Mixlib::Versioning::Format.for(:semver)
      #   Mixlib::Versioning::Format.for('semver')
      #   Mixlib::Versioning::Format.for(Mixlib::Versioning::Format::SemVer)
      #
      # @param format_type [String, Symbol, Mixlib::Versioning::Format] Name of
      #   a valid +Mixlib::Versioning::Format+ in Class or snake-case form.
      #
      # @raise [Mixlib::Versioning::UnknownFormatError] if the given format
      #   type doesn't exist
      #
      # @return [Class] the {Mixlib::Versioning::Format} class
      #
      def self.for(format_type)
        if format_type.is_a?(Class) &&
           format_type.ancestors.include?(Mixlib::Versioning::Format)
          format_type
        else
          case format_type.to_s
          when 'semver' then Mixlib::Versioning::Format::SemVer
          when 'opscode_semver' then Mixlib::Versioning::Format::OpscodeSemVer
          when 'git_describe' then Mixlib::Versioning::Format::GitDescribe
          when 'rubygems' then Mixlib::Versioning::Format::Rubygems
          else
            msg = "'#{format_type}' is not a supported Mixlib::Versioning format"
            fail Mixlib::Versioning::UnknownFormatError, msg
          end
        end
      end

      attr_reader :major, :minor, :patch, :prerelease, :build, :iteration, :input

      # @param version_string [String] string representation of the version
      def initialize(version_string)
        parse(version_string)
        @input = version_string
      end

      # Parses the version string splitting it into it's component version
      # identifiers for easy comparison and sorting of versions. This method
      # **MUST** be overriden by all descendants of this class.
      #
      # @param version_string [String] string representation of the version
      # @raise [Mixlib::Versioning::ParseError] raised if parsing fails
      def parse(_version_string)
        fail Error, 'You must override the #parse'
      end

      # @return [Boolean] Whether or not this is a release version
      def release?
        @prerelease.nil? && @build.nil?
      end

      # @return [Boolean] Whether or not this is a pre-release version
      def prerelease?
        !@prerelease.nil? && @build.nil?
      end

      # @return [Boolean] Whether or not this is a release build version
      def release_build?
        @prerelease.nil? && !@build.nil?
      end

      # @return [Boolean] Whether or not this is a pre-release build version
      def prerelease_build?
        !@prerelease.nil? && !@build.nil?
      end

      # @return [Boolean] Whether or not this is a build version
      def build?
        !@build.nil?
      end

      # Returns `true` if `other` and this {Format} share the same `major`,
      # `minor`, and `patch` values. Pre-release and build specifiers are not
      # taken into consideration.
      #
      # @return [Boolean]
      def in_same_release_line?(other)
        @major == other.major &&
          @minor == other.minor &&
          @patch == other.patch
      end

      # Returns `true` if `other` an share the same
      # `major`, `minor`, and `patch` values. Pre-release and build specifiers
      # are not taken into consideration.
      #
      # @return [Boolean]
      def in_same_prerelease_line?(other)
        @major == other.major &&
          @minor == other.minor &&
          @patch == other.patch &&
          @prerelease == other.prerelease
      end

      # @return [String] String representation of this {Format} instance
      def to_s
        @input
      end

      # Since the default implementation of `Object#inspect` uses `Object#to_s`
      # under the covers (which we override) we need to also override `#inspect`
      # to ensure useful debug information.
      def inspect
        vars = instance_variables.map do |n|
          "#{n}=#{instance_variable_get(n).inspect}"
        end
        format('#<%s:0x%x %s>', self.class, object_id, vars.join(', '))
      end

      # Returns SemVer compliant string representation of this {Format}
      # instance. The string returned will take on the form:
      #
      # ```text
      # MAJOR.MINOR.PATCH-PRERELEASE+BUILD
      # ```
      #
      # @return [String] SemVer compliant string representation of this
      #   {Format} instance
      # @todo create a proper serialization abstraction
      def to_semver_string
        s = [@major, @minor, @patch].join('.')
        s += "-#{@prerelease}" if @prerelease
        s += "+#{@build}" if @build
        s
      end

      # Returns Rubygems compliant string representation of this {Format}
      # instance. The string returned will take on the form:
      #
      # ```text
      # MAJOR.MINOR.PATCH.PRERELEASE
      # ```
      #
      # @return [String] Rubygems compliant string representation of this
      #   {Format} instance
      # @todo create a proper serialization abstraction
      def to_rubygems_string
        s = [@major, @minor, @patch].join('.')
        s += ".#{@prerelease}" if @prerelease
        s
      end

      # Compare this version number with the given version number, following
      # Semantic Versioning 2.0.0-rc.1 semantics.
      #
      # @param other [Mixlib::Versioning::Format]
      # @return [Integer] -1, 0, or 1 depending on whether the this version is
      #   less than, equal to, or greater than the other version
      def <=>(other)
        # Check whether the `other' is a String and if so, then get an
        # instance of *this* class (e.g., GitDescribe, OpscodeSemVer,
        # SemVer, Rubygems, etc.), so we can compare against it.
        other = self.class.new(other) if other.is_a?(String)

        # First, perform comparisons based on major, minor, and patch
        # versions.  These are always presnt and always non-nil
        maj = @major <=> other.major
        return maj unless maj == 0

        min = @minor <=> other.minor
        return min unless min == 0

        pat = @patch <=> other.patch
        return pat unless pat == 0

        # Next compare pre-release specifiers.  A pre-release sorts
        # before a release (e.g. 1.0.0-alpha.1 comes before 1.0.0), so
        # we need to take nil into account in our comparison.
        #
        # If both have pre-release specifiers, we need to compare both
        # on the basis of each component of the specifiers.
        if @prerelease && other.prerelease.nil?
          return -1
        elsif @prerelease.nil? && other.prerelease
          return 1
        elsif @prerelease && other.prerelease
          pre = compare_dot_components(@prerelease, other.prerelease)
          return pre unless pre == 0
        end

        # Build specifiers are compared like pre-release specifiers,
        # except that builds sort *after* everything else
        # (e.g. 1.0.0+build.123 comes after 1.0.0, and
        # 1.0.0-alpha.1+build.123 comes after 1.0.0-alpha.1)
        if @build.nil? && other.build
          return -1
        elsif @build && other.build.nil?
          return 1
        elsif @build && other.build
          build_ver = compare_dot_components(@build, other.build)
          return build_ver unless build_ver == 0
        end

        # Some older version formats improperly include a package iteration in
        # the version string. This is different than a build specifier and
        # valid release versions may include an iteration. We'll transparently
        # handle this case and compare iterations if it was parsed by the
        # implementation class.
        if @iteration.nil? && other.iteration
          return -1
        elsif @iteration && other.iteration.nil?
          return 1
        elsif @iteration && other.iteration
          return @iteration <=> other.iteration
        end

        # If we get down here, they're both equal
        0
      end

      # @param other [Mixlib::Versioning::Format]
      # @return [Boolean] returns true if the versions are equal, false
      #   otherwise.
      def eql?(other)
        @major == other.major &&
          @minor == other.minor &&
          @patch == other.patch &&
          @prerelease == other.prerelease &&
          @build == other.build
      end

      def hash
        [@major, @minor, @patch, @prerelease, @build].compact.join('.').hash
      end

      #########################################################################

      private

      # If a String `n` can be parsed as an Integer do so; otherwise, do
      # nothing.
      #
      # @param n [String, nil]
      # @return [Integer] the parsed {Integer}
      def maybe_int(n)
        Integer(n)
      rescue
        n
      end

      # Compares prerelease and build version component strings
      # according to SemVer 2.0.0-rc.1 semantics.
      #
      # Returns -1, 0, or 1, just like the spaceship operator (`<=>`),
      # and is used in the implemntation of `<=>` for this class.
      #
      # Pre-release and build specifiers are dot-separated strings.
      # Numeric components are sorted numerically; otherwise, sorting is
      # standard ASCII order.  Numerical components have a lower
      # precedence than string components.
      #
      # See http://www.semver.org for more.
      #
      # Both `a_item` and `b_item` should be Strings; `nil` is not a
      # valid input.
      def compare_dot_components(a_item, b_item)
        a_components = a_item.split('.')
        b_components = b_item.split('.')

        max_length = [a_components.length, b_components.length].max

        (0..(max_length - 1)).each do |i|
          # Convert the ith component into a number if possible
          a = maybe_int(a_components[i])
          b = maybe_int(b_components[i])

          # Since the components may be of differing lengths, the
          # shorter one will yield +nil+ at some point as we iterate.
          if a.nil? && !b.nil?
            # a_item was shorter
            return -1
          elsif !a.nil? && b.nil?
            # b_item was shorter
            return 1
          end

          # Now we need to compare appropriately based on type.
          #
          # Numbers have lower precedence than strings; therefore, if
          # the components are of differnt types (String vs. Integer),
          # we just return -1 for the numeric one and we're done.
          #
          # If both are the same type (Integer vs. Integer, or String
          # vs. String), we can just use the native comparison.
          #
          if a.is_a?(Integer) && b.is_a?(String)
            # a_item was "smaller"
            return -1
          elsif a.is_a?(String) && b.is_a?(Integer)
            # b_item was "smaller"
            return 1
          else
            comp = a <=> b
            return comp unless comp == 0
          end
        end # each

        # We've compared all components of both strings; if we've gotten
        # down here, they're totally the same
        0
      end
    end # Format
  end # Versioning
end # Mixlib
