#!/usr/bin/env python
"""
Adapted from:
    - http://www.djangosnippets.org/snippets/764/
    - http://www.satchmoproject.com/trac/browser/satchmo/trunk/satchmo/apps/satchmo_utils/views.py
    - http://tinyurl.com/shoppify-credit-cards
"""

import re

# Well known card regular expressions.
CARDS = {
    "Visa": re.compile(r"^4\d{12}(\d{3})?$"),
    "Mastercard": re.compile(r"^(5[1-5]\d{4}|677189)\d{10}$"),
    "Dinersclub": re.compile(r"^3(0[0-5]|[68]\d)\d{11}$"),
    "Amex": re.compile(r"^3[47]\d{13}$"),
    "Discover": re.compile(r"^(6011|65\d{2})\d{12}$"),
}

# Well known test numbers
TEST_NUMBERS = [
    "378282246310005",
    "371449635398431",
    "378734493671000",
    "30569309025904",
    "38520000023237",
    "6011111111111117",
    "6011000990139424",
    "555555555554444",
    "5105105105105100",
    "4111111111111111",
    "4012888888881881",
    "4222222222222",
]


def verify_credit_card(number):
    """Returns the card type for given card number or None if invalid."""
    return CreditCard(number).verify()


class CreditCard:
    def __init__(self, number):
        self.number = number

    def is_number(self):
        """True if there is at least one digit in number."""
        self.number = re.sub(r"[^\d]", "", self.number)
        return self.number.isdigit()

    def is_mod10(self):
        """Returns True if number is valid according to mod10."""
        double = 0
        total = 0
        for i in range(len(self.number) - 1, -1, -1):
            for c in str((double + 1) * int(self.number[i])):
                total = total + int(c)
            double = (double + 1) % 2
        return (total % 10) == 0

    def is_test(self):
        """Returns True if number is a test card number."""
        # Note: test numbers cannot be used in the PP Pro sandbox.
        # Instead, use the credit card number associated with a
        # sandbox account (Test Accounts -> View Details).
        return self.number in TEST_NUMBERS

    def get_type(self):
        """Return the type if it matches one of the cards."""
        for card, pattern in CARDS.items():
            if pattern.match(self.number):
                return card
        return None

    def verify(self):
        """Returns the card type if valid else None."""
        if self.is_number() and not self.is_test() and self.is_mod10():
            return self.get_type()
        return None
