using System;
using System.Collections.Generic;
using System.Text;

namespace Wayne.Lib
{
    /// <summary>
    /// Binary Encoder/Decoder with the custom 5-bit-per-byte format that
    /// is especially good for screen viewing, since the characters that 
    /// may be confused has been eliminated (I/1/l/O/0). The purpose is 
    /// to encode binary data into a string representation.
    /// </summary>
    public class BinaryConvert5Bit
    {
        /// <summary>
        /// Vadildates if a given set of characters are valid.
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public bool Valid5BitString(IEnumerable<char> s)
        {
            const string allowedChars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
            foreach (char character in s)
            {
                if (allowedChars.IndexOf(character) < 0)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Returns the string representation of the bytes 
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public string ToString(byte[] bytes)
        {
            byte[] fivebitBytes = EncodeFiveBitPerBytesArray(bytes);
            string encodedString = ConvertToString(fivebitBytes);
            return encodedString;
        }

        private string ConvertToString(byte[] bytes)
        {
            StringBuilder sb = new StringBuilder();
            foreach (var b in bytes)
            {
                sb.Append(ConvertByte(b));
            }
            return sb.ToString();
        }


        private char ConvertByte(byte b)
        {
            switch (b)
            {
                case 0: return '2';
                case 1: return '3';
                case 2: return '4';
                case 3: return '5';
                case 4: return '6';
                case 5: return '7';
                case 6: return '8';
                case 7: return '9';
                case 8: return 'A';
                case 9: return 'B';
                case 10: return 'C';
                case 11: return 'D';
                case 12: return 'E';
                case 13: return 'F';
                case 14: return 'G';
                case 15: return 'H';
                case 16: return 'J';
                case 17: return 'K';
                case 18: return 'L';
                case 19: return 'M';
                case 20: return 'N';
                case 21: return 'P';
                case 22: return 'Q';
                case 23: return 'R';
                case 24: return 'S';
                case 25: return 'T';
                case 26: return 'U';
                case 27: return 'V';
                case 28: return 'W';
                case 29: return 'X';
                case 30: return 'Y';
                case 31: return 'Z';

            }
            throw new ArgumentException(string.Format("Invalid byte {0} it contains more than 5 bits.", b));
        }

        private byte ConvertChar(char c)
        {
            switch (c)
            {
                case '2': return 0;
                case '3': return 1;
                case '4': return 2;
                case '5': return 3;
                case '6': return 4;
                case '7': return 5;
                case '8': return 6;
                case '9': return 7;
                case 'A': return 8;
                case 'B': return 9;
                case 'C': return 10;
                case 'D': return 11;
                case 'E': return 12;
                case 'F': return 13;
                case 'G': return 14;
                case 'H': return 15;
                case 'J': return 16;
                case 'K': return 17;
                case 'L': return 18;
                case 'M': return 19;
                case 'N': return 20;
                case 'P': return 21;
                case 'Q': return 22;
                case 'R': return 23;
                case 'S': return 24;
                case 'T': return 25;
                case 'U': return 26;
                case 'V': return 27;
                case 'W': return 28;
                case 'X': return 29;
                case 'Y': return 30;
                case 'Z': return 31;
            }
            throw new ArgumentException(string.Format("Invalid char {0}.", c));
        }

        private byte[] EncodeFiveBitPerBytesArray(byte[] bytes)
        {
            byte[] inputBytes = (byte[])bytes.Clone();

            //Calculate resulting number of bytes
            int inputBitCount = inputBytes.Length * 8;
            int resultingBytesCount = inputBitCount / 5;
            if (inputBitCount % 5 > 0)
                resultingBytesCount++;

            byte[] resultBytes = new byte[resultingBytesCount];
            for (int resultByteIndex = resultingBytesCount - 1; resultByteIndex >= 0; resultByteIndex--)
            {
                //                Console.WriteLine(ByteListToStringBinary(inputBytes));
                //Get the last five bits of the last byte in the byte array.
                byte fiveBits = (byte)(inputBytes[inputBytes.Length - 1] & 0x1f);
                resultBytes[resultByteIndex] = fiveBits;

                //Shift all the bits five places to the right
                for (int inputByteIndex = inputBytes.Length - 1; inputByteIndex >= 0; inputByteIndex--)
                {
                    inputBytes[inputByteIndex] = (byte)(inputBytes[inputByteIndex] >> 5);//Shift the remaining 3 bits to the right
                    if (inputByteIndex > 0)//If we are at the leftmost byte, there is nothing to take from left.
                        inputBytes[inputByteIndex] = (byte)(inputBytes[inputByteIndex] + ((inputBytes[inputByteIndex - 1] & 0x1f) << 3)); //Take last 5 bits of next byte and shift to the left.
                }
            }
            return resultBytes;
        }

        private string ByteListToStringBinary(byte[] inputBytes)
        {
            var bitStream = ConvertBytesToBitStream(inputBytes);
            return BitStreamToString(bitStream);
        }

        private string BitStreamToString(bool[] bitStream)
        {
            var chars = new char[bitStream.Length];
            for (int i = 0; i < bitStream.Length; i++)
            {
                chars[i] = bitStream[i] ? '1' : '0';
            }
            return new string(chars);
        }

        private bool[] ConvertBytesToBitStream(byte[] inputBytes)
        {
            List<bool> bitStream = new List<bool>();
            foreach (var inputByte in inputBytes)
            {
                for (int i = 7; i >= 0; i--)
                {
                    bitStream.Add((inputByte & (1 << i)) != 0);
                }
            }
            return bitStream.ToArray();
        }


        /// <summary>
        /// Returns the decoded bytes that was represented by the inputString.
        /// </summary>
        /// <param name="inputString"></param>
        /// <returns></returns>
        public byte[] ToBytes(string inputString)
        {
            byte[] inputBytes = Convert5BitStringToBytes(inputString);
            int numberOfResultingBytes = inputBytes.Length * 5 / 8;
            bool[] inputBitStream = new bool[inputBytes.Length * 5];
            for (int byteIndex = 0; byteIndex < inputBytes.Length; byteIndex++)
            {
                for (int bitIndex = 5; bitIndex >= 1; bitIndex--)
                    inputBitStream[byteIndex * 5 + 5 - bitIndex] = IsBitSet(inputBytes[byteIndex], bitIndex);
            }

            //Console.WriteLine("BitStream 1:    " + ByteListToStringBinary(inputBitStream));

            //Trim the bitstream to only contain the result bits.
            inputBitStream = TrimBitStream(inputBitStream, numberOfResultingBytes * 8);

            //Console.WriteLine("BitStream 2:    " + ByteListToStringBinary(inputBitStream));

            byte[] outputBytes = new byte[numberOfResultingBytes];
            for (int byteIndex = 0; byteIndex < inputBitStream.Length / 8; byteIndex++) //Iterate the bytes within the bit array.
            {
                for (int bitIndex = 0; bitIndex < 8; bitIndex++) //Iterate through the bits within each byte.
                {
                    bool b = inputBitStream[byteIndex * 8 + bitIndex];
                    if (b)
                    {
                        outputBytes[byteIndex] = (byte)(outputBytes[byteIndex] | (1 << (7 - bitIndex)));
                    }
                }
            }
            //Console.WriteLine("Output bytes:   " + ByteListToStringBinary(outputBytes.ToArray()));
            return outputBytes;
        }

        private byte[] Convert5BitStringToBytes(string inputString)
        {
            byte[] result = new byte[inputString.Length];
            for (int i = 0; i < inputString.Length; i++)
                result[i] = ConvertChar(inputString[i]);
            return result;
        }

        private bool[] TrimBitStream(bool[] inputBitStream, int countToKeep)
        {
            bool[] newBitStream = new bool[countToKeep];
            Array.Copy(inputBitStream, inputBitStream.Length - countToKeep, newBitStream, 0, countToKeep);
            return newBitStream;

        }

        private bool IsBitSet(byte b, int byteNumber)
        {
            return (b & (1 << (byteNumber - 1))) != 0;
        }
    }
}