// Created with MonoDevelop
//
//    Copyright (C) 2019 Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.IO;


namespace Hiperscan.SGS.Common
{
    public class Crc32 : System.Security.Cryptography.HashAlgorithm
    {
        private readonly UInt32[] table = new UInt32[256];
        private UInt32 current_value;

        private const UInt32 GENERATOR_POLYNOMIAL = 0x04C11DB7;
        private const UInt32 GENERATOR_SEED       = 0xFFFFFFFF;
        private const UInt32 GENERATOR_XOR_OUT    = 0xFFFFFFFF;

        private const int GENERATOR_HASH_SIZE = 32;

        public Crc32()
        {
            this.CreateTable();
            this.current_value = Crc32.ReverseBits(GENERATOR_SEED, GENERATOR_HASH_SIZE);
        }

        public override int HashSize => GENERATOR_HASH_SIZE;

        public UInt32[] GetTable()
        {
            lock (this)
                return (UInt32[])this.table.Clone();
        }

        public override void Initialize()
        {
            lock (this)
                this.current_value = Crc32.ReverseBits(GENERATOR_SEED, GENERATOR_HASH_SIZE);
        }

        public new byte[] ComputeHash(byte[] buffer)
        {
            lock (this)
                return base.ComputeHash(buffer);
        }

        public new byte[] ComputeHash(Stream stream)
        {
            lock (this)
                return base.ComputeHash(stream);
        }

        protected override void HashCore(byte[] array, int ibStart, int cbSize)
        {
            this.current_value = this.ComputeCrc(this.current_value, array, ibStart, cbSize);
        }

        protected override byte[] HashFinal()
        {
            return Crc32.ToBigEndianBytes(this.current_value ^ GENERATOR_XOR_OUT);
        }

        private UInt32 ComputeCrc(UInt32 init, byte[] data, int offset, int length)
        {
            UInt32 crc = init;

            for (int ix = offset; ix < offset + length; ix++)
            {
                crc = (this.table[(crc ^ data[ix]) & 0xFF] ^ (crc >> 8));
            }

            return crc;
        }

        private void CreateTable()
        {
            for (int ix = 0; ix < this.table.Length; ix++)
            {
                this.table[ix] = this.CreateTableEntry(ix);
            }
        }

        private UInt32 CreateTableEntry(int index)
        {
            UInt32 r = (UInt32)index;

            r = Crc32.ReverseBits(r, GENERATOR_HASH_SIZE);

            UInt32 last_bit = (1u << (GENERATOR_HASH_SIZE - 1));

            for (int ix = 0; ix < 8; ++ix)
            {
                if ((r & last_bit) != 0)
                    r = ((r << 1) ^ GENERATOR_POLYNOMIAL);
                else
                    r <<= 1;
            }

            r = Crc32.ReverseBits(r, GENERATOR_HASH_SIZE);

            return r;
        }

        private static UInt32 ReverseBits(UInt32 val, int len)
        {
            UInt32 new_val = 0;

            for (int ix = len - 1; ix >= 0; --ix)
            {
                new_val |= (val & 1) << ix;
                val >>= 1;
            }

            return new_val;
        }

        private static byte[] ToBigEndianBytes(UInt32 val)
        {
            var result = BitConverter.GetBytes(val);

            if (BitConverter.IsLittleEndian)
                Array.Reverse(result);

            return result;
        }

        private static UInt32 FromBigToUInt32(byte[] buffer, int start)
        {
            return (UInt32)(buffer[start] << 24 | buffer[start + 1] << 16 | buffer[start + 2] << 8 | buffer[start + 3]);
        }

        private static UInt32 ToUInt32(byte[] hash)
        {
            return Crc32.FromBigToUInt32(hash, 0);
        }

        public static UInt32 FromBigEndian(byte[] hash)
        {
            UInt32 result = 0;
            for (int ix = 0; ix < sizeof(UInt32); ++ix)
            {
                result |= ((UInt32)hash[ix]) << (32 - 8 * (ix + 1));
            }

            return result;
        }
    }
}