''' <summary>
''' Credit/Debit Card Number verfication class
''' </summary>
''' <remarks>Remember as soon as you assign a card number this class will check it's type and validity. If you make any other changes then you will have to assign the card number again.</remarks>
Public Class MoneyCard

    Dim g_card_number As String
    Dim g_cards As DataTable
    Dim g_valid As Boolean
    Dim g_clean, g_type As String

    ''' <summary>
    ''' Initializes a new instance of the <see cref="MoneyCard" /> class.
    ''' </summary>
    Public Sub New()
        g_cards = New DataTable
        g_cards.Columns.Add("CardType", GetType(String))
        g_cards.Columns.Add("Prefix", GetType(String))
        g_cards.Columns.Add("Length", GetType(Int32))

        Me.LoadXml(Mitul.My.Resources.MoneyCard)
    End Sub

    ''' <summary>
    ''' Gets or sets the card number.
    ''' </summary>
    ''' <value>The card number.</value>
    Public Property CardNumber() As String
        Get
            Return g_card_number
        End Get
        Set(ByVal value As String)
            Me.g_card_number = value

            'Get a clean card number. With only digits
            Dim l_card_number As New System.Text.StringBuilder(19)
            Dim x As Integer
            Dim drw() As DataRow
            Dim r As Integer

            For x = 0 To Me.CardNumber.Length - 1
                If Char.IsDigit(Me.g_card_number.Chars(x)) Then l_card_number.Append(Me.g_card_number.Chars(x))
            Next
            Me.g_clean = l_card_number.ToString

            'Let's find the type of the card
            Me.g_type = "Unkwon"
            drw = Me.g_cards.Select("Length = " & Me.g_clean.Length & " AND Prefix LIKE '" & Me.g_clean.Chars(0).ToString & "%'", "Prefix")
            For r = 0 To drw.Length - 1
                If Me.g_clean.StartsWith(drw(r)("Prefix").ToString) Then
                    Me.g_type = drw(r)("CardType").ToString
                    Exit For
                End If
            Next

            Me.g_valid = Me.g_type <> "Unkwon" AndAlso Me.IsCreditCardValid(Me.g_clean)
        End Set
    End Property

    ''' <summary>
    ''' Gets the clean card number.
    ''' </summary>
    ''' <value>The clean card number.</value>
    Public ReadOnly Property CleanCardNumber() As String
        Get
            Return Me.g_clean
        End Get
    End Property

    ''' <summary>
    ''' Returns <c>true</c> if it is a valid card number. 
    ''' </summary>
    ''' <value>Is it valid ot not.</value>
    Public ReadOnly Property IsValid() As String
        Get
            Return Me.g_valid
        End Get
    End Property

    ''' <summary>
    ''' Gets the type of the card.
    ''' </summary>
    ''' <value>The type of the card.</value>
    Public ReadOnly Property CardType() As String
        Get
            Return Me.g_type
        End Get
    End Property

    ''' <summary>
    ''' Alls the card types that this class can identify.
    ''' </summary>
    ''' <returns>An array of sring.</returns>
    Public Function AllCardTypes() As String()
        Dim r As Integer
        Dim lst As New List(Of String)

        For r = 0 To Me.g_cards.Rows.Count - 1
            If Not lst.Contains(Me.g_cards.Rows(r)("CardType").ToString) Then
                lst.Add(Me.g_cards.Rows(r)("CardType").ToString)
            End If
        Next

        Return lst.ToArray
    End Function

    ''' <summary>
    ''' Adds a type of the card to the list of cards that this class can handle.
    ''' </summary>
    ''' <param name="p_type">The card type. ex. MasterCard</param>
    ''' <param name="p_prefix">The prefixes. ex. 55, 3401-3406 - This say card number start with either 55 or from 3401 to 3406.</param>
    ''' <param name="p_len">The lengths of number. ex. 13, 16-18 - This says card number can be either 13, 16, 17, or 18 digits long.</param>
    Public Sub AddCardType(ByVal p_type As String, ByVal p_prefix As String, ByVal p_len As String)
        Dim drw As DataRow
        Dim lMinLen, lMaxLen, l As Int32
        Dim lFirst, lLast, p As Integer
        Dim Lens(), Prefixes() As String
        Dim x, y As Integer

        Lens = p_len.Replace(" ", "").Split(",".ToCharArray, StringSplitOptions.RemoveEmptyEntries)
        Prefixes = p_prefix.Replace(" ", "").Split(",".ToCharArray, StringSplitOptions.RemoveEmptyEntries)

        For x = 0 To Lens.Length - 1
            BreakMinMax(Lens(x), lMinLen, lMaxLen)
            For l = lMinLen To lMaxLen
                For y = 0 To Prefixes.Length - 1
                    BreakMinMax(Prefixes(y), lFirst, lLast)
                    For p = lFirst To lLast
                        drw = Me.g_cards.NewRow
                        drw("CardType") = p_type
                        drw("Prefix") = p.ToString
                        drw("Length") = l
                        Me.g_cards.Rows.Add(drw)
                    Next
                Next
            Next
        Next
    End Sub

    ''' <summary>
    ''' Removes all cards types from the list.
    ''' </summary>
    Public Sub RemoveAllCards()
        Me.g_cards.Rows.Clear()
    End Sub

    ''' <summary>
    ''' Removes the card specified by the prefix and length.
    ''' </summary>
    ''' <param name="p_prefix">The prefixex of the card number. ex. 55, 34-36 - will remove card with prefix 55, 34, 35, and 36.</param>
    ''' <param name="p_len">The length of the card number. ex. 13, 16-18 - will remove card with lenght of 13, 16, 17, and 18.</param>
    Public Sub RemoveCard(ByVal p_prefix As String, ByVal p_len As String)
        Dim drw() As DataRow
        Dim l_select As String = ""
        Dim Lens(), Prefixes() As String
        Dim x, y, r As Integer
        Dim lFirst, lLast, p As Integer
        Dim lMinLen, lMaxLen, l As Int32

        Lens = Nothing
        Prefixes = Nothing
        If Not (String.IsNullOrEmpty(p_prefix) OrElse p_prefix.Trim = 0) Then
            Prefixes = p_prefix.Replace(" ", "").Split(",".ToCharArray, StringSplitOptions.RemoveEmptyEntries)
        End If

        If Not (String.IsNullOrEmpty(p_len) OrElse p_len.Trim = 0) Then
            Lens = p_len.Replace(" ", "").Split(",".ToCharArray, StringSplitOptions.RemoveEmptyEntries)
        End If

        If Lens Is Nothing OrElse Lens.Length = 0 Then
            If Prefixes Is Nothing OrElse Prefixes.Length = 0 Then
                'Both are empty. Nothing to delete.
            Else
                For y = 0 To Prefixes.Length - 1
                    Me.RemoveCard(Prefixes(y))
                Next
            End If
        Else
            For x = 0 To Lens.Length - 1
                If Prefixes Is Nothing OrElse Prefixes.Length = 0 Then
                    Me.RemoveCardByLength(Lens(x))
                Else
                    BreakMinMax(Lens(x), lMinLen, lMaxLen)
                    For l = lMinLen To lMaxLen
                        For y = 0 To Prefixes.Length - 1
                            BreakMinMax(Prefixes(y), lFirst, lLast)
                            For p = lFirst To lLast
                                drw = Me.g_cards.Select("Length = " & l.ToString & " AND Prefix = '" & p.ToString & "'")
                                For r = drw.Length - 1 To 0 Step -1
                                    drw(r).Delete()
                                Next
                            Next
                        Next
                    Next
                End If
            Next
            Me.g_cards.AcceptChanges()
        End If

    End Sub

    ''' <summary>
    ''' Removes the card specified by the prefix.
    ''' </summary>
    ''' <param name="p_prefix">The prefixex of the card number. ex. 55, 34-36 - will remove card with prefix 55, 34, 35, and 36.</param>
    Public Sub RemoveCard(ByVal p_prefix As String)
        Dim lFirst, lLast, p As Integer
        Dim drw() As DataRow
        Dim r As Integer

        BreakMinMax(p_prefix, lFirst, lLast)
        For p = lFirst To lLast
            drw = Me.g_cards.Select("Prefix = '" & p.ToString & "'")
            For r = drw.Length - 1 To 0 Step -1
                drw(r).Delete()
            Next
        Next
        Me.g_cards.AcceptChanges()
    End Sub

    ''' <summary>
    ''' Removes the card specified by the length.
    ''' </summary>
    ''' <param name="p_len">The length of the card number. ex. 13, 16-18 - will remove card with lenght of 13, 16, 17, and 18.</param>
    Public Sub RemoveCardByLength(ByVal p_len As String)
        Dim lMinLen, lMaxLen, l As Int32
        Dim drw() As DataRow
        Dim r As Integer

        BreakMinMax(p_len, lMinLen, lMaxLen)
        For l = lMinLen To lMaxLen
            drw = Me.g_cards.Select("Length = " & l.ToString)
            For r = drw.Length - 1 To 0 Step -1
                drw(r).Delete()
            Next
        Next
        Me.g_cards.AcceptChanges()
    End Sub

    ''' <summary>
    ''' Loads the valid cards from XML. The XML should have Cards/Card xpath. Card element should have Name, Prefix, and Length elements.
    ''' </summary>
    ''' <param name="p_xml">The Xml string.</param>
    Public Sub LoadXml(ByVal p_xml As String)
        Dim l_xml As New System.Xml.XmlDocument()
        Dim cards As System.Xml.XmlNodeList
        Dim card As System.Xml.XmlNode

        l_xml.LoadXml(Mitul.My.Resources.MoneyCard)
        Try
            cards = l_xml.SelectNodes("Cards/Card")
            For Each card In cards
                Me.AddCardType(card.Item("Name").InnerText, card.Item("Prefix").InnerText, card.Item("Length").InnerText)
            Next
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary> 
    ''' Validates a credit card number using the standard Luhn/mod10 
    ''' validation algorithm. 
    ''' </summary> 
    ''' <param name="cardNumber">Card number, with or without 
    ''' punctuation</param> 
    ''' <returns>True if card number appears valid, false if not 
    ''' </returns> 
    Private Function IsCreditCardValid(ByVal cardNumber As String) As Boolean
        Dim x As Integer
        Dim sum, total As Integer
        Dim bOdd As Boolean = True

        If cardNumber.Length < 13 OrElse cardNumber.Length > 19 Then
            Return False
        End If

        total = 0
        For x = cardNumber.Length - 1 To 0 Step -1
            If bOdd Then
                sum = Integer.Parse(cardNumber.Substring(x, 1))
            Else
                sum = Integer.Parse(cardNumber.Substring(x, 1)) * 2
            End If
            bOdd = Not bOdd
            If sum > 9 Then sum -= 9
            total += sum
        Next

        Return (total Mod 10 = 0)
    End Function

    Private Sub BreakMinMax(ByVal s As String, ByRef lMin As Int32, ByRef lMax As Int32)
        s = s.Replace(" ", "").Trim
        If s.IndexOf("-") = -1 Then
            lMin = Int32.Parse(s)
            lMax = lMin
        Else
            lMin = Int32.Parse(s.Substring(0, s.IndexOf("-")))
            lMax = Int32.Parse(s.Substring(s.IndexOf("-") + 1))
        End If
    End Sub

End Class
