using System.Diagnostics; // Trace
using System.IO; // File
using System.Numerics; // BigInteger

namespace Packt.Shared
{
  public static class NumbersToWords
  {
    // Single-digit and small number names
    private static string[] smallNumbers = new string[]
      {
        "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
        "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
        "sixteen", "seventeen", "eighteen", "nineteen"
      };

    // Tens number names from twenty upwards
    private static string[] tens = new string[]
      {
        "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy",
        "eighty", "ninety"
      };

    // Scale number names for use during recombination
    private static string[] scaleNumbers = new string[]
      {
        "", "thousand", "million", "billion", "trillion",
        "quadrillion", "quintillion"
      };

    private static int groups = 7; // i.e. up to quintillion

    public static string ToWords(this int number)
    {
      return ToWords((BigInteger)number);
    }

    public static string ToWords(this long number)
    {
      return ToWords((BigInteger)number);
    }

    public static string ToWords(this BigInteger number)
    {
      /* 
        Convert A Number into Words
        by Richard Carr, published at http://www.blackwasp.co.uk/numbertowords.aspx
      */

      /*
        Zero Rule.
        If the value is 0 then the number in words is 'zero' and no other rules apply.
      */
      if (number == 0)
      {
        return "zero";
      }

      /*
        Three Digit Rule.
        The integer value is split into groups of three digits starting from the 
        right-hand side. Each set of three digits is then processed individually 
        as a number of hundreds, tens and units. Once converted to text, the 
        three-digit groups are recombined with the addition of the relevant scale 
        number (thousand, million, billion).
      */

      // Array to hold the specified number of three-digit groups
      int[] digitGroups = new int[groups];

      // Ensure a positive number to extract from
      var positive = BigInteger.Abs(number);

      // Extract the three-digit groups
      for (int i = 0; i < groups; i++)
      {
        digitGroups[i] = (int)(positive % 1000);
        positive /= 1000;
      }

      // write to a text file in the project folder
      using var textWriter = new TextWriterTraceListener(
        File.AppendText("log.txt"));

      Trace.Listeners.Add(textWriter);

      // text writer is buffered, so this option calls 
      // Flush() on all listeners after writing 
      Trace.AutoFlush = true;

      // log array of group numbers
      for (int x = 0; x < digitGroups.Length; x++)
      {
        Trace.WriteLine(string.Format(
          format: "digitGroups[{0}] = {1}",
          arg0: x,
          arg1: digitGroups[x]));
      }

      // Convert each three-digit group to words
      string[] groupTexts = new string[groups];

      for (int i = 0; i < groups; i++)
      {
        // call a local function (see below)
        groupTexts[i] = ThreeDigitGroupToWords(digitGroups[i]);
      }

      // log array of group texts
      for (int x = 0; x < groupTexts.Length; x++)
      {
        Trace.WriteLine(string.Format(
          format: "groupTexts[{0}] = {1}",
          arg0: x,
          arg1: groupTexts[x]));
      }

      /*
        Recombination Rules.
        When recombining the translated three-digit groups, each group except the 
        last is followed by a large number name and a comma, unless the group is 
        blank and therefore not included at all. One exception is when the final 
        group does not include any hundreds and there is more than one non-blank 
        group. In this case, the final comma is replaced with 'and'. eg. 
        'one billion, one million and twelve'.
      */

      // Recombine the three-digit groups
      string combined = groupTexts[0];
      bool appendAnd;

      // Determine whether an 'and' is needed
      appendAnd = (digitGroups[0] > 0) && (digitGroups[0] < 100);

      // Process the remaining groups in turn, smallest to largest
      for (int i = 1; i < groups; i++)
      {
        // Only add non-zero items
        if (digitGroups[i] != 0)
        {
          // Build the string to add as a prefix
          string prefix = groupTexts[i] + " " + scaleNumbers[i];

          if (combined.Length != 0)
          {
            prefix += appendAnd ? " and " : ", ";
          }

          // Opportunity to add 'and' is ended
          appendAnd = false;

          // Add the three-digit group to the combined string
          combined = prefix + combined;
        }
      }

      // Converts a three-digit group into English words
      string ThreeDigitGroupToWords(int threeDigits)
      {
        // Initialise the return text
        string groupText = "";

        // Determine the hundreds and the remainder
        int hundreds = threeDigits / 100;
        int tensUnits = threeDigits % 100;

        /* 
          Hundreds Rules.
          If the hundreds portion of a three-digit group is not zero, the number of 
          hundreds is added as a word. If the three-digit group is exactly divisible 
          by one hundred, the text 'hundred' is appended. If not, the text 
          "hundred and" is appended. eg. 'two hundred' or 'one hundred and twelve'
        */

        if (hundreds != 0)
        {
          groupText += smallNumbers[hundreds] + " hundred";

          if (tensUnits != 0)
          {
            groupText += " and ";
          }
        }

        // Determine the tens and units
        int tens = tensUnits / 10;
        int units = tensUnits % 10;

        /* Tens Rules.
           If the tens section of a three-digit group is two or higher, the appropriate 
           '-ty' word (twenty, thirty, etc.) is added to the text and followed by the 
           name of the third digit (unless the third digit is a zero, which is ignored). 
           If the tens and the units are both zero, no text is added. For any other value, 
           the name of the one or two-digit number is added as a special case.
        */

        if (tens >= 2)
        {
          groupText += NumbersToWords.tens[tens];
          if (units != 0)
          {
            groupText += " " + smallNumbers[units];
          }
        }
        else if (tensUnits != 0)
          groupText += smallNumbers[tensUnits];

        return groupText;
      }

      /* Negative Rule.
         Negative numbers are always preceded by the text 'negative'.
      */
      if (number < 0)
      {
        combined = "negative " + combined;
      }

      return combined;
    }
  }
}
