# typed: strict
# frozen_string_literal: true

module Cask
  class DSL
    # Class corresponding to the `version` stanza.
    class Version < ::String
      DIVIDERS = T.let({
        "." => :dots,
        "-" => :hyphens,
        "_" => :underscores,
      }.freeze, T::Hash[String, Symbol])

      DIVIDER_REGEX = /(#{DIVIDERS.keys.map { |v| Regexp.quote(v) }.join("|")})/

      MAJOR_MINOR_PATCH_REGEX = /^([^.,:]+)(?:.([^.,:]+)(?:.([^.,:]+))?)?/

      INVALID_CHARACTERS = /[^0-9a-zA-Z.,:\-_+ ]/

      class << self
        private

        sig { params(divider: String).void }
        def define_divider_methods(divider)
          define_divider_deletion_method(divider)
          define_divider_conversion_methods(divider)
        end

        sig { params(divider: String).void }
        def define_divider_deletion_method(divider)
          method_name = deletion_method_name(divider)
          define_method(method_name) do
            T.bind(self, Version)
            version { delete(divider) }
          end
        end

        sig { params(divider: String).returns(String) }
        def deletion_method_name(divider)
          "no_#{DIVIDERS[divider]}"
        end

        sig { params(left_divider: String).void }
        def define_divider_conversion_methods(left_divider)
          (DIVIDERS.keys - [left_divider]).each do |right_divider|
            define_divider_conversion_method(left_divider, right_divider)
          end
        end

        sig { params(left_divider: String, right_divider: String).void }
        def define_divider_conversion_method(left_divider, right_divider)
          method_name = conversion_method_name(left_divider, right_divider)
          define_method(method_name) do
            T.bind(self, Version)
            version { gsub(left_divider, right_divider) }
          end
        end

        sig { params(left_divider: String, right_divider: String).returns(String) }
        def conversion_method_name(left_divider, right_divider)
          "#{DIVIDERS[left_divider]}_to_#{DIVIDERS[right_divider]}"
        end
      end

      DIVIDERS.each_key do |divider|
        define_divider_methods(divider)
      end

      sig { returns(T.nilable(T.any(String, Symbol))) }
      attr_reader :raw_version

      sig { params(raw_version: T.nilable(T.any(String, Symbol))).void }
      def initialize(raw_version)
        @raw_version = raw_version
        super(raw_version.to_s)

        invalid = invalid_characters
        raise TypeError, "#{raw_version} contains invalid characters: #{invalid.uniq.join}!" if invalid.present?
      end

      sig { returns(T::Array[T.any(T::Array[String], String)]) }
      def invalid_characters
        return [] if raw_version.blank? || latest?

        raw_version.to_s.scan(INVALID_CHARACTERS)
      end

      sig { returns(T::Boolean) }
      def unstable?
        return false if latest?

        s = downcase.delete(".").gsub(/[^a-z\d]+/, "-")

        return true if s.match?(/(\d+|\b)(alpha|beta|preview|rc|dev|canary|snapshot)(\d+|\b)/i)
        return true if s.match?(/\A[a-z\d]+(-\d+)*-?(a|b|pre)(\d+|\b)/i)

        false
      end

      sig { returns(T::Boolean) }
      def latest?
        to_s == "latest"
      end

      # The major version.
      #
      # @api public
      sig { returns(T.self_type) }
      def major
        version { slice(MAJOR_MINOR_PATCH_REGEX, 1) }
      end

      # The minor version.
      #
      # @api public
      sig { returns(T.self_type) }
      def minor
        version { slice(MAJOR_MINOR_PATCH_REGEX, 2) }
      end

      # The patch version.
      #
      # @api public
      sig { returns(T.self_type) }
      def patch
        version { slice(MAJOR_MINOR_PATCH_REGEX, 3) }
      end

      # The major and minor version.
      #
      # @api public
      sig { returns(T.self_type) }
      def major_minor
        version { [major, minor].reject(&:empty?).join(".") }
      end

      # The major, minor and patch version.
      #
      # @api public
      sig { returns(T.self_type) }
      def major_minor_patch
        version { [major, minor, patch].reject(&:empty?).join(".") }
      end

      # The minor and patch version.
      #
      # @api public
      sig { returns(T.self_type) }
      def minor_patch
        version { [minor, patch].reject(&:empty?).join(".") }
      end

      # The comma separated values of the version as array.
      #
      # @api public
      sig { returns(T::Array[Version]) } # Only top-level T.self_type is supported https://sorbet.org/docs/self-type
      def csv
        split(",").map { self.class.new(it) }
      end

      # The version part before the first comma.
      #
      # @api public
      sig { returns(T.self_type) }
      def before_comma
        version { split(",", 2).first }
      end

      # The version part after the first comma.
      #
      # @api public
      sig { returns(T.self_type) }
      def after_comma
        version { split(",", 2).second }
      end

      # The version without any dividers.
      #
      # @see DIVIDER_REGEX
      # @api public
      sig { returns(T.self_type) }
      def no_dividers
        version { gsub(DIVIDER_REGEX, "") }
      end

      # The version with the given record separator removed from the end.
      #
      # @see String#chomp
      # @api public
      sig { params(separator: String).returns(T.self_type) }
      def chomp(separator = T.unsafe(nil))
        version { to_s.chomp(separator) }
      end

      private

      sig { params(_block: T.proc.returns(T.nilable(String))).returns(T.self_type) }
      def version(&_block)
        return self if empty? || latest?

        self.class.new(yield)
      end
    end
  end
end
