using System;

using GostCryptography.Properties;

namespace GostCryptography.Asn1.Ber
{
    [Serializable]
    public class BigInteger
    {
        private const int AddressBits = 3;
        private const int BitIndexMask = 7;
        private const int BitsPerUnit = 8;
        internal const int MaxBigIntLen = 100000;
        private const int UnitMask = -1;

        [NonSerialized]
        private static readonly int[] BitsPerDigit =
        {
            0, 0, 0x400, 0x658, 0x800, 0x94a, 0xa58, 0xb3b, 0xc00, 0xcaf, 0xd4a, 0xdd7, 0xe58, 0xece, 0xf3b, 0xfa1,
            0x1000, 0x105a, 0x10af, 0x10fe, 0x114a, 0x1192, 0x11d7, 0x1219, 0x1258, 0x1294, 0x12ce, 0x1306, 0x133b, 0x136f, 0x13a1, 0x13d2,
            0x1400, 0x142e, 0x145a, 0x1485, 0x14af
        };

        [NonSerialized]
        private static readonly int[] ByteRadix =
        {
            0, 0, 0x80, 0, 0, 0, 0, 0, 0x40, 0, 100, 0, 0, 0, 0, 0, 0x10
        };

        [NonSerialized]
        private static readonly int[] DigitsPerByte =
        {
            0, 0, 7, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 1
        };

        private static readonly byte[] Zero = new byte[0];

        [NonSerialized]
        private int _sign;

        [NonSerialized]
        private byte[] _value;

        public BigInteger()
        {
            _value = Zero;
            _sign = 0;
        }

        public BigInteger(long value)
            : this(value.ToString())
        {
        }

        public BigInteger(string value)
        {
            Init(value, 10);
        }

        public BigInteger(byte[] value, int sign)
        {
            _value = value;
            _sign = sign;
        }

        public BigInteger(string value, int radix)
        {
            Init(value, radix);
        }

        private static int BitsLeftOf(int x)
        {
            if (x != 0)
            {
                return (UnitMask << (BitsPerUnit - x));
            }

            return UnitMask;
        }

        private static void DestructiveMulAdd(byte[] x, int y, byte z)
        {
            var num = (byte)(y & 0xff);
            var num2 = z;
            var length = x.Length;
            var num5 = 0;

            for (var i = length - 1; i >= 0; i--)
            {
                var num4 = (num * x[i]) + num5;
                x[i] = (byte)num4;
                num5 = num4 >> BitsPerUnit;
            }

            var num7 = x[length - 1] + num2;
            x[length - 1] = (byte)num7;
            num5 = num7 >> BitsPerUnit;

            for (var j = length - 2; j >= 0; j--)
            {
                num7 = x[j] + num5;
                x[j] = (byte)num7;
                num5 = num7 >> BitsPerUnit;
            }
        }

        private static void DivideByInt(ref BigInteger divident, int divisor, ref BigInteger quotient, ref int reminder)
        {
            var index = 0;
            var num3 = 4;
            var num4 = 0;
            var num5 = 0;

            if (divisor == 0)
            {
                return;
            }

            reminder = 0;

            if (divident._sign == 0)
            {
                quotient._sign = 0;
                quotient._value = Zero;
                return;
            }

            quotient._value = new byte[divident._value.Length];

            var num2 = quotient._value.Length - 1;
            quotient._sign = ((quotient._sign * divisor) > 0) ? 1 : -1;

            var num6 = divident._value.Length * 2;

            while (num4 < num6)
            {
                num5 = num5 << 4;
                num4++;
                num5 |= (divident._value[index] >> num3) & 15;

                if (num3 == 0)
                {
                    num3 = 4;
                    index++;
                }
                else
                {
                    num3 = 0;
                }

                ShiftLeft(quotient, 4);

                if (num5 >= divisor)
                {
                    quotient._value[num2] = (byte)(quotient._value[num2] | ((byte)((num5 / divisor) & 15)));
                    num5 = num5 % divisor;
                }

                reminder = num5;
            }

            quotient._value = RemoveLeadingZeroBytes(quotient._value);
        }

        public bool Equals(long value)
        {
            return Equals(new BigInteger(value));
        }

        public override bool Equals(object value)
        {
            var integer = value as BigInteger;

            if (integer == null)
            {
                return false;
            }

            if (_value.Length != integer._value.Length)
            {
                return false;
            }

            for (var i = 0; i < _value.Length; i++)
            {
                if (_value[i] != integer._value[i])
                {
                    return false;
                }
            }

            return true;
        }

        private static void FastCopy(ref BigInteger src, ref BigInteger dst)
        {
            dst._value = new byte[src._value.Length];
            Array.Copy(src._value, 0, dst._value, 0, src._value.Length);
            dst._sign = src._sign;
        }

        private BigInteger GetCopy()
        {
            var integer = new BigInteger();

            if (_value.Length > 0)
            {
                integer._value = new byte[_value.Length];
                Array.Copy(_value, 0, integer._value, 0, _value.Length);
            }
            else
            {
                integer._value = Zero;
            }

            integer._sign = _sign;

            return integer;
        }

        private BigInteger GetCopyAndInverse()
        {
            var integer = new BigInteger();

            if (_value.Length > 0)
            {
                integer._value = new byte[_value.Length];

                if (_sign < 0)
                {
                    integer._value = GetData();
                    integer._sign = 1;
                    return integer;
                }

                Array.Copy(_value, 0, integer._value, 0, _value.Length);
                integer._sign = _sign;

                return integer;
            }

            integer._value = Zero;

            return integer;
        }

        public byte[] GetData()
        {
            int num2;
            var dataLen = GetDataLen();
            var index = _value.Length - 1;
            var num4 = dataLen - 1;

            if (_sign == 0)
            {
                return Zero;
            }

            var buffer = new byte[dataLen];

            if (_sign >= 0)
            {
                num2 = _value.Length - 1;

                while (((num2 >= 0) && (num4 >= 0)) && (index >= 0))
                {
                    buffer[num4] = _value[index];
                    num2--;
                    num4--;
                    index--;
                }

                if ((dataLen - _value.Length) > 0)
                {
                    buffer[num4] = 0;
                }

                return buffer;
            }

            num2 = _value.Length - 1;

            while (((num2 >= 0) && (num4 >= 0)) && (index >= 0))
            {
                unchecked
                {
                    buffer[num4] = (byte)-_value[index];
                }

                if (_value[index] != 0)
                {
                    num2--;
                    num4--;
                    index--;
                    break;
                }

                num2--;
                num4--;
                index--;
            }

            while (((num2 >= 0) && (num4 >= 0)) && (index >= 0))
            {
                unchecked
                {
                    buffer[num4] = (byte)~_value[index];
                }

                num2--;
                num4--;
                index--;
            }

            if ((dataLen - _value.Length) > 0)
            {
                buffer[num4] = 0xff;
            }

            return buffer;
        }

        private int GetDataLen()
        {
            if (_sign == 0)
            {
                return 1;
            }

            if ((_sign > 0) && ((_value[0] & 0x80) != 0))
            {
                return (_value.Length + 1);
            }

            if (_sign < 0)
            {
                var num = _value[0];

                if ((_value.Length == 1) || ((_value.Length > 1) && (_value[1] == 0)))
                {
                    num = (byte)~(num - 1);
                }
                else
                {
                    unchecked
                    {
                        num = (byte)~num;
                    }
                }

                if ((num & 0x80) == 0)
                {
                    return (_value.Length + 1);
                }
            }

            return _value.Length;
        }

        public override int GetHashCode()
        {
            if (_value == null)
            {
                return base.GetHashCode();
            }

            if (_value.Length == 0)
            {
                return base.GetHashCode();
            }

            var num = 0;
            var num2 = (_value.Length > 20) ? 20 : _value.Length;

            for (var i = 0; i < num2; i++)
            {
                num ^= _value[i];
            }

            return num;
        }

        public void Init(string val, int radix)
        {
            var str = "";

            if (val[0] == '-')
            {
                val = val.Substring(1);
                str = "-";
            }

            if (val.StartsWith("0x"))
            {
                radix = 0x10;
                val = val.Substring(2);
            }
            else if (val.StartsWith("0b"))
            {
                radix = 2;
                val = val.Substring(2);
            }
            else if (val.StartsWith("0o"))
            {
                radix = 8;
                val = val.Substring(2);
            }

            val = str + val;
            var startIndex = 0;
            var length = val.Length;

            if (((radix != 2) && (radix != 0x10)) && ((radix != 10) && (radix != 8)))
            {
                throw new FormatException(Resources.Asn1InvalidFormatForBigIntegerValue);
            }

            if (val.Length == 0)
            {
                throw new FormatException(Resources.Asn1ZeroLengthBigInteger);
            }

            _sign = 1;

            var index = val.IndexOf('-');

            if (index != -1)
            {
                if (index != 0)
                {
                    throw new FormatException(Resources.Asn1IllegalEmbeddedMinusSign);
                }

                if (val.Length == 1)
                {
                    throw new FormatException(Resources.Asn1ZeroLengthBigInteger);
                }

                _sign = -1;

                startIndex = 1;
            }

            while ((startIndex < length) && (val[startIndex] == '0'))
            {
                startIndex++;
            }

            if (startIndex == length)
            {
                _sign = 0;
                _value = Zero;
            }
            else
            {
                var num2 = length - startIndex;
                var num5 = Asn1Util.UrShift(num2 * BitsPerDigit[radix], 10) + 1;
                var num1 = (num5 + 0x1f) / 0x20;

                _value = new byte[num2];

                var num6 = num2 % DigitsPerByte[radix];

                if (num6 == 0)
                {
                    num6 = DigitsPerByte[radix];
                }

                var str2 = val.Substring(startIndex, num6);
                startIndex += num6;

                _value[_value.Length - 1] = Convert.ToByte(str2, radix);

                if (_value[_value.Length - 1] < 0)
                {
                    throw new FormatException(Resources.Asn1IllegalDigit);
                }

                var y = ByteRadix[radix];
                byte z;

                while (startIndex < val.Length)
                {
                    str2 = val.Substring(startIndex, DigitsPerByte[radix]);
                    startIndex += DigitsPerByte[radix];
                    z = Convert.ToByte(str2, radix);

                    if (z < 0)
                    {
                        throw new FormatException(Resources.Asn1IllegalDigit);
                    }

                    DestructiveMulAdd(_value, y, z);
                }

                _value = TrustedStripLeadingZeroInts(_value);
            }
        }

        private static string IntToStr(long value, int radix)
        {
            var chArray = new char[0x22];
            var num = 0;
            var str = "";

            if ((radix >= 2) && (radix <= 0x10))
            {
                while (num < 0x22)
                {
                    chArray[num++] = (char)((ushort)(value % radix));

                    if ((value /= radix) == 0L)
                    {
                        break;
                    }
                }

                while (num != 0)
                {
                    var ch = chArray[--num];

                    if (ch < '\n')
                    {
                        str = str + ((char)(ch + '0'));
                    }
                    else
                    {
                        str = str + ((char)((ch - '\n') + 0x41));
                    }
                }
            }

            return str;
        }

        public bool IsNegative()
        {
            return (_sign < 0);
        }

        private char NibbleToHexChar(int b)
        {
            if ((b >= 0) && (b <= 9))
            {
                return (char)(b + 0x30);
            }

            if ((b >= 10) && (b <= 15))
            {
                return (char)((b - 10) + 0x61);
            }

            return '?';
        }

        public static implicit operator BigInteger(long value)
        {
            return new BigInteger(value);
        }

        private static byte[] RemoveLeadingZeroBytes(byte[] data)
        {
            if (data.Length == 0)
            {
                return data;
            }

            var index = 0;

            while ((index < data.Length) && (data[index] == 0))
            {
                index++;
            }

            var destinationArray = new byte[data.Length - index];
            Array.Copy(data, index, destinationArray, 0, data.Length - index);

            return destinationArray;
        }

        public void SetData(byte[] ivalue)
        {
            if (ivalue.Length > MaxBigIntLen)
            {
                throw ExceptionUtility.CryptographicException(Resources.Asn1TooBigIntegerValue, ivalue.Length);
            }

            if ((ivalue.Length > 0) && ((ivalue[0] & 0x80) != 0))
            {
                var index = 0;
                var num = 0;

                _sign = -1;

                while ((num < ivalue.Length) && (ivalue[index] == 0xff))
                {
                    num++;
                    index++;
                }

                var num2 = num;

                while ((num2 < ivalue.Length) && (ivalue[index] == 0))
                {
                    num2++;
                    index++;
                }

                var num3 = (num2 == ivalue.Length) ? 1 : 0;
                _value = new byte[(ivalue.Length - num) + num3];
                index = num;

                var num4 = num;

                while (num < ivalue.Length)
                {
                    unchecked
                    {
                        _value[(num - num4) + num3] = (byte)~ivalue[index];
                    }

                    num++;
                    index++;
                }

                for (num = _value.Length - 1; (_value[num] = (byte)(_value[num] + 1)) == 0; num--)
                {
                }

                _value = RemoveLeadingZeroBytes(_value);
            }
            else
            {
                _value = RemoveLeadingZeroBytes(ivalue);
                _sign = (ivalue.Length == 0) ? 0 : 1;
            }
        }

        private static int ShiftLeft(BigInteger data, uint shift)
        {
            var value = data._value;
            var length = value.Length;
            var index = (int)(shift >> AddressBits);
            var num3 = ((int)shift) & BitIndexMask;
            var num4 = 8 - num3;
            var num5 = 0;
            var num7 = length;

            if (length != 0)
            {
                length = length << AddressBits;
                var num6 = (int)((((length - shift) + 8L) - 1L) >> AddressBits);

                while (num5 < (num6 - 1))
                {
                    value[num5++] = (byte)((value[index] << num3) | ((num4 == 8) ? 0 : (value[index + 1] >> num4)));
                    index++;
                }

                length &= BitIndexMask;
                value[num5] = (num7 == num6) ? ((byte)((value[index] & BitsLeftOf(length)) << num3)) : ((byte)((value[index] << num3) | ((num4 == 8) ? 0 : ((value[index + 1] & BitsLeftOf(length)) >> num4))));

                if (num6 < num7)
                {
                    for (var i = num6; i < (num7 - num6); i++)
                    {
                        value[i] = 0;
                    }
                }
            }

            return 0;
        }

        public override string ToString()
        {
            return ToString(10);
        }

        public string ToString(int radix)
        {
            if ((radix == 2) || (radix == 0x10))
            {
                int num;
                int num2;

                if (radix == 2)
                {
                    num2 = 8;
                    num = 1;
                }
                else
                {
                    num2 = 2;
                    num = 4;
                }

                var num3 = num2 * GetDataLen();
                var chArray = new char[num3];
                var index = num3 - 1;

                for (var i = _value.Length - 1; i >= 0; i--)
                {
                    byte num6;
                    int num8;

                    if (_sign < 0)
                    {
                        unchecked
                        {
                            num6 = (byte)~_value[i];
                        }

                        if ((_sign < 0) && ((num6 = (byte)(num6 + 1)) != 0))
                        {
                            _sign = 0;
                        }
                    }
                    else
                    {
                        num6 = _value[i];
                    }

                    var num7 = num8 = 0;

                    while (num7 < num2)
                    {
                        var b = (num6 >> num8) & ((1 << num) - 1);
                        chArray[index] = NibbleToHexChar(b);
                        num7++;
                        index--;
                        num8 += num;
                    }
                }

                while (index >= 0)
                {
                    chArray[index--] = '0';
                }

                return new string(chArray);
            }

            var reminder = 0;
            var str = "";
            var quotient = new BigInteger();

            var copy = (radix == 10) ? GetCopy() : GetCopyAndInverse();

            do
            {
                DivideByInt(ref copy, ByteRadix[radix], ref quotient, ref reminder);
                var str2 = IntToStr(reminder, radix);
                var length = str2.Length;

                str = str2 + str;

                if ((quotient._value.Length != 0) || (radix != 10))
                {
                    int num12;

                    for (num12 = length; num12 < DigitsPerByte[radix]; num12++)
                    {
                        str = '0' + str;
                    }

                    FastCopy(ref quotient, ref copy);

                    if (((quotient._value.Length == 0) && (_sign > 0)) && ((radix != 10) && ((reminder & 0x80) != 0)))
                    {
                        str = '0' + str;
                    }
                }
                else if ((_sign < 0) && (radix == 10))
                {
                    str = '-' + str;
                }

            }
            while ((quotient._value != null) && (quotient._value.Length != 0));

            return str;
        }

        private static byte[] TrustedStripLeadingZeroInts(byte[] val)
        {
            var index = 0;

            while ((index < val.Length) && (val[index] == 0))
            {
                index++;
            }

            if (index <= 0)
            {
                return val;
            }

            var buffer = new byte[val.Length - index];

            for (var i = 0; i < (val.Length - index); i++)
            {
                buffer[i] = val[index + i];
            }

            return buffer;
        }
    }
}
