lib/credit_card_validations/detector.rb
# == CreditCardValidations Detector
#
# class provides credit card number validations
module CreditCardValidations
  class Detector

    include Mmi

    class_attribute :brands
    self.brands = {}

    attr_reader :number

    def initialize(number)
      @number = number.to_s.tr('- ', '')
    end

    # credit card number validation
    def valid?(*brands)
      !!valid_number?(*brands)
    end

    #brand name
    def brand(*keys)
      valid_number?(*keys)
    end

    def valid_number?(*keys)
      selected_brands = keys.blank? ? self.brands : resolve_keys(*keys)
      if selected_brands.any?
        matched_brands = []
        selected_brands.each do |key, brand|
          match_data = matches_brand?(brand)
          matched_brands << {brand: key, matched_prefix_length: match_data.to_s.length} if match_data
        end

        if matched_brands.present?
          return matched_brands.sort{|a, b| a[:matched_prefix_length] <=> b[:matched_prefix_length]}.last[:brand]
        end
      end
      nil
    end

    #check if luhn valid
    def valid_luhn?
      @valid_luhn ||= Luhn.valid?(number)
    end

    def brand_name
      self.class.brand_name(brand)
    end

    protected

    def resolve_keys(*keys)
      brand_keys = keys.map do |el|
        if el.is_a? String
          #try to find key by name
          el = (self.class.brand_key(el) || el).to_sym
        end
        el.downcase
      end
      self.brands.slice(*brand_keys)
    end

    def matches_brand?(brand)
      rules = brand.fetch(:rules)
      options = brand.fetch(:options, {})

      rules.each do |rule|
        if (options[:skip_luhn] || valid_luhn?) &&
            rule[:length].include?(number.length) &&
            match_data = number.match(rule[:regexp])
          return match_data
        end
      end
      false
    end

    class << self

      def has_luhn_check_rule?(key)
        !brands[key].fetch(:options, {}).fetch(:skip_luhn, false)
      end

      #
      # add brand
      #
      #   CreditCardValidations.add_brand(:en_route, {length: 15, prefixes: ['2014', '2149']}, {skip_luhn: true}) #skip luhn
      #
      def add_brand(key, rules, options = {})

        brands[key] = {rules: [], options: options || {}}

        Array.wrap(rules).each do |rule|
          add_rule(key, rule[:length], rule[:prefixes])
        end

        define_brand_method(key)

      end

      def brand_name(brand_key)
        brand = brands[brand_key]
        if brand
          brand.fetch(:options, {})[:brand_name] || brand_key.to_s.titleize
        else
          nil
        end
      end

      def brand_key(brand_name)
        brands.detect do |_, brand|
          brand[:options][:brand_name] == brand_name
        end.try(:first)
      end

      # CreditCardValidations.delete_brand(:en_route)
      def delete_brand(key)
        key = key.to_sym
        undef_brand_method(key)
        brands.reject! { |k, _| k == key }
      end

      #create rule for detecting brand
      def add_rule(key, length, prefixes)
        unless brands.has_key?(key)
          raise Error.new("brand #{key} is undefined, please use #add_brand method")
        end
        length, prefixes = Array(length), Array(prefixes)
        brands[key][:rules] << {length: length, regexp: compile_regexp(prefixes), prefixes: prefixes}
      end

      protected

      # create methods like visa?,  maestro? etc
      def define_brand_method(key)
        define_method "#{key}?".to_sym do
          valid?(key)
        end unless method_defined? "#{key}?".to_sym
      end

      def undef_brand_method(key)
        undef_method "#{key}?".to_sym if method_defined? "#{key}?".to_sym
      end

      #create regexp by array of prefixes
      def compile_regexp(prefixes)
        Regexp.new("^((#{prefixes.join(")|(")}))")
      end

    end
  end
end
