''' <summary>
''' Contains methods that provide support for converting a number to a series of words. This class cannot be inherited.
''' </summary>
Public NotInheritable Class NumberConverter

    '//delegates
    ''' <summary>
    ''' Represents the method used when a number being converted exceeds the maximum handled digits.
    ''' </summary>
    ''' <param name="index">The index of the section that requires the text. The <paramref name="index">index</paramref> * 3 = the digit.</param>
    Public Delegate Function QueryLargeNumberTextHandler(index As Integer) As String

    '//constants
    ''' <summary>
    ''' A <see cref="System.Char">System.Char</see> that represents the separation of converted text.
    ''' </summary>
    Public Const Separator As Char = " "c

    ''' <summary>
    ''' A <see cref="System.Integer">System.Integer</see> that represents the maximum digits that can be handled internally.
    ''' </summary>
    Public Const DefaultMaximumDigits As Integer = 63

    '//fields
    Private Shared ReadOnly Morphemes() As String = {"Eleven", "Twelve", "Thirteen", "Fourteen", _
                                                     "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}

    Private Shared ReadOnly LargeNumbersText() As String = {"Hundred", "Thousand", "Million", "Billion", "Trillion", _
                                                            "Quadrillion", "Quintillion", "Sextillion", "Septillion", _
                                                            "Octillion", "Nonillion", "Decillion", "Undecillion", _
                                                            "Duodecillion", "Tredecillion", "Quattuordecillion", _
                                                            "Quindecillion", "Sexdecillion", "Septendecillion", _
                                                            "Octodecillion", "Novemdecillion", "Vigintillion"}

    Private Shared ReadOnly TenNumbersText() As String = {"Ten", "Twenty", "Thirty", "Forty", "Fifty", _
                                                          "Sixty", "Seventy", "Eighty", "Ninety"}

    Private Shared ReadOnly OneNumbersText() As String = {"Zero", "One", "Two", "Three", "Four", _
                                                          "Five", "Six", "Seven", "Eight", "Nine"}

    '//constructors
    Private Sub New()
    End Sub

    '//methods
    Private Shared Sub AppendLargeNumberText(builder As System.Text.StringBuilder, index As Integer, handler As QueryLargeNumberTextHandler)
        If index > (NumberConverter.LargeNumbersText.Length - 1) Then
            builder.Append(handler.Invoke(index))
        Else
            builder.Append(NumberConverter.LargeNumbersText(index))
        End If
    End Sub

    Private Shared Function OnQueryLargeNumberText(index As Integer) As String
        Throw New OverflowException("Default implementation can handle up to 63 digits, otherwise, you must specify a handler.")
    End Function

    Private Shared Function ProcessDigit(digit As Char) As String
        Dim value = CInt(digit.ToString())
        If value = 0 Then
            Return String.Empty
        End If

        '//index of array correlates to value
        Return NumberConverter.OneNumbersText(value)
    End Function

    Private Shared Function ProcessSection(section As String, index As Integer, trim As Boolean, handler As QueryLargeNumberTextHandler) As String
        Dim builder = New System.Text.StringBuilder()

        Select Case section.Length

            Case 3
                If section.Equals("000") Then
                    Return String.Empty
                End If

                '//dealing with a 3 digit number
                Dim digit = CInt(section(0).ToString())
                If digit > 0 Then
                    builder.Append(NumberConverter.ProcessDigit(section(0)))
                    builder.Append(NumberConverter.Separator)

                    '//always append hundred
                    builder.Append(NumberConverter.LargeNumbersText(0))
                    builder.Append(NumberConverter.Separator)
                End If

                '//recursive call to process the last two digits
                builder.Append(NumberConverter.ProcessSection(section.Substring(1, 2), 0, False, handler))
                'builder.Append(NumberConverter.Separator)
                If index > 0 Then

                    '//append the large number text
                    NumberConverter.AppendLargeNumberText(builder, index, handler)
                    builder.Append(NumberConverter.Separator)
                End If

            Case 2

                '//dealing with a 2 digit number
                Dim morphemeDigit = CInt(section(0).ToString())
                Dim morphemeIdentifier = CInt(section(1).ToString())

                If morphemeDigit = 0 Then

                    '//ignore this digit and do a recursive call on the next digit
                    builder.Append(NumberConverter.ProcessDigit(section(1)))
                    builder.Append(NumberConverter.Separator)
                ElseIf morphemeDigit = 1 Then
                    If morphemeIdentifier = 0 Then

                        '//the number is ten
                        builder.Append(NumberConverter.TenNumbersText(0))
                        builder.Append(NumberConverter.Separator)
                    Else

                        '//the number is after ten and before twenty
                        builder.Append(NumberConverter.Morphemes(morphemeIdentifier - 1))
                        builder.Append(NumberConverter.Separator)
                    End If
                Else

                    '//twenty, thirty, etc...
                    builder.Append(NumberConverter.TenNumbersText(morphemeDigit - 1))
                    builder.Append(NumberConverter.Separator)

                    If morphemeIdentifier <> 0 Then
                        builder.Append(NumberConverter.ProcessDigit(section(1)))
                        builder.Append(NumberConverter.Separator)
                    End If
                End If

                If index > 0 Then

                    '//append the large number text
                    NumberConverter.AppendLargeNumberText(builder, index, handler)
                    builder.Append(NumberConverter.Separator)
                End If
            Case 1

                '//dealing with a 1 digit number
                builder.Append(NumberConverter.ProcessDigit(section(0)))
                builder.Append(NumberConverter.Separator)

                '//append the large number text
                If index > 0 Then
                    NumberConverter.AppendLargeNumberText(builder, index, handler)
                End If

                builder.Append(NumberConverter.Separator)
        End Select

        If trim Then
            Return builder.ToString().Trim()
        End If

        Return builder.ToString()
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.Byte">System.Byte</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.Byte">System.Byte</see> to convert</param>.
    Public Shared Function Convert(value As Byte) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.SByte">System.SByte</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.SByte">System.SByte</see> to convert</param>.
    Public Shared Function Convert(value As SByte) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.Short">System.Short</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.Short">System.Short</see> to convert</param>.
    Public Shared Function Convert(value As Short) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.UShort">System.UShort</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.UShort">System.UShort</see> to convert</param>.
    Public Shared Function Convert(value As UShort) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.Integer">System.Integer</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.Integer">System.Integer</see> to convert</param>.
    Public Shared Function Convert(value As Integer) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.UInteger">System.UInteger</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.UInteger">System.UInteger</see> to convert</param>.
    Public Shared Function Convert(value As UInteger) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.Long">System.Long</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.Long">System.Long</see> to convert</param>.
    Public Shared Function Convert(value As Long) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.ULong">System.ULong</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.ULong">System.ULong</see> to convert</param>.
    Public Shared Function Convert(value As ULong) As String
        Return NumberConverter.Convert(value.ToString(), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.Double">System.Double</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.Double">System.Double</see> to convert</param>.
    Public Shared Function Convert(value As Double) As String
        Return NumberConverter.Convert(Math.Floor(value).ToString("0"), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.Single">System.Single</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.Single">System.Single</see> to convert</param>.
    Public Shared Function Convert(value As Single) As String
        Return NumberConverter.Convert(Math.Floor(value).ToString("0"), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.Decimal">System.Decimal</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.Decimal">System.Decimal</see> to convert</param>.
    Public Shared Function Convert(value As Decimal) As String
        Return NumberConverter.Convert(Math.Floor(value).ToString("0"), Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.String">System.String</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.String">System.String</see> to convert.</param>
    ''' <remarks>Typically, this overload is used for numbers that cannot be represented by built in .NET types. If the value 
    ''' cannot be validated a run-time, a Try-Catch block should be used when calling this method.</remarks>
    ''' <exception cref="ArgumentException">Thrown if the <see cref="System.String">System.String</see> does not represent a number.</exception>
    ''' <exception cref="OverflowException">Thrown if the <see cref="System.String">System.String</see> exceeds the maximum allowable digits.</exception>
    Public Shared Function Convert(value As String) As String
        Return NumberConverter.Convert(value, Nothing)
    End Function

    ''' <summary>
    ''' Converts the specified <see cref="System.String">System.String</see> to a word representation.
    ''' </summary>
    ''' <param name="value">The <see cref="System.String">System.String</see> to convert</param>
    ''' <param name="handler">The <see cref="QueryLargeNumberTextHandler">
    ''' QueryLargeNumberTextHandler</see> used for cases where
    ''' the length of the string exceeds the default implementation limit.</param>
    ''' <remarks>Typically, this overload is used for numbers that cannot be represented by built in .NET types. If the value 
    ''' cannot be validated a run-time, a Try-Catch block should be used when calling this method.</remarks>
    ''' <exception cref="ArgumentException">Thrown if the <see cref="System.String">System.String</see> does not represent a number.</exception>
    ''' <exception cref="OverflowException">Thrown if nothing was passed for the <paramref name="handler">handler</paramref>.</exception>
    Public Shared Function Convert(value As String, handler As QueryLargeNumberTextHandler) As String
        If Not value.All(Function(c) Char.IsDigit(c)) Then
            Throw New ArgumentException("Value was not recognized as a valid number.")
        End If
        If value.All(Function(c) Char.Equals("0"c, c)) Then
            Return NumberConverter.OneNumbersText(0)
        End If
        If handler Is Nothing Then
            handler = AddressOf NumberConverter.OnQueryLargeNumberText
        End If

        Dim sections(CInt(Math.Ceiling(value.Length / 3)) - 1) As String
        Dim i = 0
        Dim j = value.Length - 1
        Dim builder = New System.Text.StringBuilder()

        '//break number in sections of 3 or less digits
        Do Until j < 0
            If sections(i) Is Nothing Then
                sections(i) = String.Empty
            End If

            sections(i) = sections(i).Insert(0, value(j))

            If sections(i).Length = 3 Then
                i += 1
            End If

            j -= 1
        Loop

        '//process the sections
        For i = sections.Length - 1 To 0 Step -1
            builder.Append(NumberConverter.ProcessSection(sections(i), i, True, handler))
            If Not sections(i).Equals("000") Then
                builder.Append(NumberConverter.Separator)
            End If
        Next

        Return builder.ToString().Trim()
    End Function

End Class