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

namespace Algorithms.Encoders
{
    /// <summary>
    ///     Encodes using Feistel cipher.
    ///     https://en.wikipedia.org/wiki/Feistel_cipher
    ///     In cryptography, a Feistel cipher (also known as Lubyâ€“Rackoff block cipher)
    ///     is a symmetric structure used in the construction of block ciphers,
    ///     named after the German-born physicist and cryptographer Horst Feistel
    ///     who did pioneering research while working for IBM (USA)
    ///     A large proportion of block ciphers use the scheme, including the US DES,
    ///     the Soviet/Russian GOST and the more recent Blowfish and Twofish ciphers.
    /// </summary>
    public class FeistelCipher : IEncoder<uint>
    {
        // number of rounds to transform data block, each round a new "round" key is generated.
        private const int Rounds = 32;

        /// <summary>
        ///     Encodes text using specified key,
        ///     where n - text length.
        /// </summary>
        /// <param name="text">Text to be encoded.</param>
        /// <param name="key">Key that will be used to encode the text.</param>
        /// <exception cref="ArgumentException">Error: key should be more than 0x00001111 for better encoding, key=0 will throw DivideByZero exception.</exception>
        /// <returns>Encoded text.</returns>
        public string Encode(string text, uint key)
        {
            List<ulong> blocksListPlain = SplitTextToBlocks(text);
            StringBuilder encodedText = new();

            foreach (ulong block in blocksListPlain)
            {
                uint temp = 0;

                // decompose a block to two subblocks 0x0123456789ABCDEF => 0x01234567 & 0x89ABCDEF
                uint rightSubblock = (uint)(block & 0x00000000FFFFFFFF);
                uint leftSubblock = (uint)(block >> 32);

                uint roundKey;

                // Feistel "network" itself
                for (int round = 0; round < Rounds; round++)
                {
                    roundKey = GetRoundKey(key, round);
                    temp = rightSubblock ^ BlockModification(leftSubblock, roundKey);
                    rightSubblock = leftSubblock;
                    leftSubblock = temp;
                }

                // compile text string formating the block value to text (hex based), length of the output = 16 byte always
                ulong encodedBlock = leftSubblock;
                encodedBlock = (encodedBlock << 32) | rightSubblock;
                encodedText.Append(string.Format("{0:X16}", encodedBlock));
            }

            return encodedText.ToString();
        }

        /// <summary>
        ///     Decodes text that was encoded using specified key.
        /// </summary>
        /// <param name="text">Text to be decoded.</param>
        /// <param name="key">Key that was used to encode the text.</param>
        /// <exception cref="ArgumentException">Error: key should be more than 0x00001111 for better encoding, key=0 will throw DivideByZero exception.</exception>
        /// <exception cref="ArgumentException">Error: The length of text should be divisible by 16 as it the block lenght is 16 bytes.</exception>
        /// <returns>Decoded text.</returns>
        public string Decode(string text, uint key)
        {
            // The plain text will be padded to fill the size of block (16 bytes)
            if (text.Length % 16 != 0)
            {
                throw new ArgumentException($"The length of {nameof(key)} should be divisible by 16");
            }

            List<ulong> blocksListEncoded = GetBlocksFromEncodedText(text);
            StringBuilder decodedTextHex = new();

            foreach (ulong block in blocksListEncoded)
            {
                uint temp = 0;

                // decompose a block to two subblocks 0x0123456789ABCDEF => 0x01234567 & 0x89ABCDEF
                uint rightSubblock = (uint)(block & 0x00000000FFFFFFFF);
                uint leftSubblock = (uint)(block >> 32);

                // Feistel "network" - decoding, the order of rounds and operations on the blocks is reverted
                uint roundKey;
                for (int round = Rounds - 1; round >= 0; round--)
                {
                    roundKey = GetRoundKey(key, round);
                    temp = leftSubblock ^ BlockModification(rightSubblock, roundKey);
                    leftSubblock = rightSubblock;
                    rightSubblock = temp;
                }

                // compose decoded block
                ulong decodedBlock = leftSubblock;
                decodedBlock = (decodedBlock << 32) | rightSubblock;

                for(int i = 0; i < 8; i++)
                {
                    ulong a = (decodedBlock & 0xFF00000000000000) >> 56;

                    // it's a trick, the code works with non zero characters, if your text has ASCII code 0x00 it will be skipped.
                    if (a != 0)
                    {
                        decodedTextHex.Append((char)a);
                    }

                    decodedBlock = decodedBlock << 8;
                }
            }

            return decodedTextHex.ToString();
        }

        // Using the size of block = 8 bytes this function splts the text and returns set of 8 bytes (ulong) blocks
        // the last block is extended up to 8 bytes if the tail of the text is smaller than 8 bytes
        private static List<ulong> SplitTextToBlocks(string text)
        {
            List<ulong> blocksListPlain = new();
            byte[] textArray = Encoding.ASCII.GetBytes(text);
            int offset = 8;
            for(int i = 0; i < text.Length; i += 8)
            {
                // text not always has len%16 == 0, that's why the offset should be adjusted for the last part of the text
                if (i > text.Length - 8)
                {
                    offset = text.Length - i;
                }

                string block = Convert.ToHexString(textArray, i, offset);
                blocksListPlain.Add(Convert.ToUInt64(block, 16));
            }

            return blocksListPlain;
        }

        // convert the encoded text to the set of ulong values (blocks for decoding)
        private static List<ulong> GetBlocksFromEncodedText(string text)
        {
            List<ulong> blocksListPlain = new();
            for(int i = 0; i < text.Length; i += 16)
            {
                ulong block = Convert.ToUInt64(text.Substring(i, 16), 16);
                blocksListPlain.Add(block);
            }

            return blocksListPlain;
        }

        // here might be any deterministic math formula
        private static uint BlockModification(uint block, uint key)
        {
            for (int i = 0; i < 32; i++)
            {
                // 0x55555555 for the better distribution 0 an 1 in the block
                block = ((block ^ 0x55555555) * block) % key;
                block = block ^ key;
            }

            return block;
        }

        // There are many ways to generate a round key, any deterministic math formula does work
        private static uint GetRoundKey(uint key, int round)
        {
            // "round + 2" - to avoid a situation when pow(key,1) ^ key  = key ^ key = 0
            uint a = (uint)Math.Pow((double)key, round + 2);
            return a ^ key;
        }
    }
}
