using System.Collections.Generic;
using System.Linq;

namespace Algorithms.Encoders
{
    /// <summary>
    ///     Class for Soundex encoding strings.
    /// </summary>
    public class SoundexEncoder
    {
        /// <summary>
        ///     Encodes a string using the Soundex Algorithm.
        /// </summary>
        /// <param name="text">The string to encode.</param>
        /// <returns>The Soundex encoded string (one uppercase character and three digits).</returns>
        public string Encode(string text)
        {
            text = text.ToLowerInvariant();
            var chars = OmitHAndW(text);
            IEnumerable<int> numbers = ProduceNumberCoding(chars);
            numbers = CollapseDoubles(numbers);
            numbers = OmitVowels(numbers);
            numbers = CollapseLeadingDigit(numbers, text[0]);
            numbers = numbers.Take(3);
            numbers = PadTo3Numbers(numbers);
            var final = numbers.ToArray();
            return $"{text.ToUpperInvariant()[0]}{final[0]}{final[1]}{final[2]}";
        }

        private IEnumerable<int> CollapseLeadingDigit(IEnumerable<int> numbers, char c)
        {
            using var enumerator = numbers.GetEnumerator();
            enumerator.MoveNext();
            if (enumerator.Current == MapToNumber(c))
            {
                enumerator.MoveNext();
            }

            do
            {
                yield return enumerator.Current;
            }
            while (enumerator.MoveNext());
        }

        private IEnumerable<int> PadTo3Numbers(IEnumerable<int> numbers)
        {
            using var enumerator = numbers.GetEnumerator();
            for (var i = 0; i < 3; i++)
            {
                yield return enumerator.MoveNext()
                    ? enumerator.Current
                    : 0;
            }
        }

        private IEnumerable<int> OmitVowels(IEnumerable<int> numbers) => numbers.Where(i => i != 0);

        private IEnumerable<char> OmitHAndW(string text) => text.Where(c => c != 'h' && c != 'w');

        private IEnumerable<int> CollapseDoubles(IEnumerable<int> numbers)
        {
            var previous = int.MinValue;
            foreach (var i in numbers)
            {
                if (previous != i)
                {
                    yield return i;
                    previous = i;
                }
            }
        }

        private IEnumerable<int> ProduceNumberCoding(IEnumerable<char> text) => text.Select(MapToNumber);

        private int MapToNumber(char ch)
        {
            var mapping = new Dictionary<char, int>
            {
                ['a'] = 0,
                ['e'] = 0,
                ['i'] = 0,
                ['o'] = 0,
                ['u'] = 0,
                ['y'] = 0,
                ['h'] = 8,
                ['w'] = 8,
                ['b'] = 1,
                ['f'] = 1,
                ['p'] = 1,
                ['v'] = 1,
                ['c'] = 2,
                ['g'] = 2,
                ['j'] = 2,
                ['k'] = 2,
                ['q'] = 2,
                ['s'] = 2,
                ['x'] = 2,
                ['z'] = 2,
                ['d'] = 3,
                ['t'] = 3,
                ['l'] = 4,
                ['m'] = 5,
                ['n'] = 5,
                ['r'] = 6,
            };

            return mapping[ch];
        }
    }
}
