Hill

using System;
using System.Linq;
using Algorithms.Numeric;

namespace Algorithms.Encoders;

/// <summary>
///     Lester S. Hill's polygraphic substitution cipher,
///     without representing letters using mod26, using
///     corresponding "(char)value" instead.
/// </summary>
public class HillEncoder : IEncoder<double[,]>
{
    private readonly GaussJordanElimination linearEquationSolver;

    public HillEncoder() => linearEquationSolver = new GaussJordanElimination(); // TODO: add DI

    public string Encode(string text, double[,] key)
    {
        var preparedText = FillGaps(text);
        var chunked = ChunkTextToArray(preparedText);
        var splitted = SplitToCharArray(chunked);

        var ciphered = new double[chunked.Length][];

        for (var i = 0; i < chunked.Length; i++)
        {
            var vector = new double[3];
            Array.Copy(splitted, i * 3, vector, 0, 3);
            var product = MatrixCipher(vector, key);
            ciphered[i] = product;
        }

        var merged = MergeArrayList(ciphered);

        return BuildStringFromArray(merged);
    }

    public string Decode(string text, double[,] key)
    {
        var chunked = ChunkTextToArray(text);
        var split = SplitToCharArray(chunked);

        var deciphered = new double[chunked.Length][];

        for (var i = 0; i < chunked.Length; i++)
        {
            var vector = new double[3];
            Array.Copy(split, i * 3, vector, 0, 3);
            var product = MatrixDeCipher(vector, key);
            deciphered[i] = product;
        }

        var merged = MergeArrayList(deciphered);
        var str = BuildStringFromArray(merged);

        return UnFillGaps(str);
    }

    /// <summary>
    ///     Converts elements from the array to their corresponding Unicode characters.
    /// </summary>
    /// <param name="arr">array of vectors.</param>
    /// <returns>Message.</returns>
    private static string BuildStringFromArray(double[] arr) => new(arr.Select(c => (char)c).ToArray());

    /// <summary>
    ///     Multiplies the key for the given scalar.
    /// </summary>
    /// <param name="vector">list of splitted words as numbers.</param>
    /// <param name="key">Cipher selected key.</param>
    /// <returns>Ciphered vector.</returns>
    private static double[] MatrixCipher(double[] vector, double[,] key)
    {
        var multiplied = new double[vector.Length];

        for (var i = 0; i < key.GetLength(1); i++)
        {
            for (var j = 0; j < key.GetLength(0); j++)
            {
                multiplied[i] += key[i, j] * vector[j];
            }
        }

        return multiplied;
    }

    /// <summary>
    ///     Given a list of vectors, returns a single array of elements.
    /// </summary>
    /// <param name="list">List of ciphered arrays.</param>
    /// <returns>unidimensional list.</returns>
    private static double[] MergeArrayList(double[][] list)
    {
        var merged = new double[list.Length * 3];

        for (var i = 0; i < list.Length; i++)
        {
            Array.Copy(list[i], 0, merged, i * 3, list[0].Length);
        }

        return merged;
    }

    /// <summary>
    ///     Splits the input text message as chunks of words.
    /// </summary>
    /// <param name="chunked">chunked words list.</param>
    /// <returns>spliiter char array.</returns>
    private static char[] SplitToCharArray(string[] chunked)
    {
        var splitted = new char[chunked.Length * 3];

        for (var i = 0; i < chunked.Length; i++)
        {
            for (var j = 0; j < 3; j++)
            {
                splitted[i * 3 + j] = chunked[i].ToCharArray()[j];
            }
        }

        return splitted;
    }

    /// <summary>
    ///     Chunks the input text message.
    /// </summary>
    /// <param name="text">text message.</param>
    /// <returns>array of words.</returns>
    private static string[] ChunkTextToArray(string text)
    {
        // To split the message into chunks
        var div = text.Length / 3;
        var chunks = new string[div];

        for (var i = 0; i < div; i++)
        {
            chunks.SetValue(text.Substring(i * 3, 3), i);
        }

        return chunks;
    }

    /// <summary>
    ///     Fills a text message with spaces at the end
    ///     to enable a simple split by 3-length-word.
    /// </summary>
    /// <param name="text">Text Message.</param>
    /// <returns>Modified text Message.</returns>
    private static string FillGaps(string text)
    {
        var remainder = text.Length % 3;
        return remainder == 0 ? text : text + new string(' ', 3 - remainder);
    }

    /// <summary>
    ///     Removes the extra spaces included on the cipher phase.
    /// </summary>
    /// <param name="text">Text message.</param>
    /// <returns>Deciphered Message.</returns>
    private static string UnFillGaps(string text) => text.TrimEnd();

    /// <summary>
    ///     Finds the inverse of the given matrix using a linear equation solver.
    /// </summary>
    /// <param name="vector">Splitted words vector.</param>
    /// <param name="key">Key used for the cipher.</param>
    /// <returns>TODO.</returns>
    private double[] MatrixDeCipher(double[] vector, double[,] key)
    {
        // To augment the original key with the given vector.
        var augM = new double[3, 4];

        for (var i = 0; i < key.GetLength(0); i++)
        {
            for (var j = 0; j < key.GetLength(1); j++)
            {
                augM[i, j] = key[i, j];
            }
        }

        for (var k = 0; k < vector.Length; k++)
        {
            augM[k, 3] = vector[k];
        }

        _ = linearEquationSolver.Solve(augM);

        return new[] { augM[0, 3], augM[1, 3], augM[2, 3] };
    }
}