using Fractions;
using System.Globalization;
using System.Numerics;

namespace k8s.Models
{
    /// <summary>
    ///     port https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go to c#
    ///     Quantity is a fixed-point representation of a number.
    ///     It provides convenient marshaling/unmarshaling in JSON and YAML,
    ///     in addition to String() and Int64() accessors.
    ///     The serialization format is:
    ///     quantity        ::= signedNumber suffix
    ///     (Note that suffix may be empty, from the "" case in decimalSI.)
    ///     digit           ::= 0 | 1 | ... | 9
    ///     digits          ::= digit | digitdigits
    ///     number          ::= digits | digits.digits | digits. | .digits
    ///     sign            ::= "+" | "-"
    ///     signedNumber    ::= number | signnumber
    ///     suffix          ::= binarySI | decimalExponent | decimalSI
    ///     binarySI        ::= Ki | Mi | Gi | Ti | Pi | Ei
    ///     (International System of units; See: http:///physics.nist.gov/cuu/Units/binary.html)
    ///     decimalSI       ::= m | "" | k | M | G | T | P | E
    ///     (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)
    ///     decimalExponent ::= "e" signedNumber | "E" signedNumber
    ///     No matter which of the three exponent forms is used, no quantity may represent
    ///     a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal
    ///     places. Numbers larger or more precise will be capped or rounded up.
    ///     (E.g.: 0.1m will rounded up to 1m.)
    ///     This may be extended in the future if we require larger or smaller quantities.
    ///     When a Quantity is parsed from a string, it will remember the type of suffix
    ///     it had, and will use the same type again when it is serialized.
    ///     Before serializing, Quantity will be put in "canonical form".
    ///     This means that Exponent/suffix will be adjusted up or down (with a
    ///     corresponding increase or decrease in Mantissa) such that:
    ///     a. No precision is lost
    ///     b. No fractional digits will be emitted
    ///     c. The exponent (or suffix) is as large as possible.
    ///     The sign will be omitted unless the number is negative.
    ///     Examples:
    ///     1.5 will be serialized as "1500m"
    ///     1.5Gi will be serialized as "1536Mi"
    ///     NOTE: We reserve the right to amend this canonical format, perhaps to
    ///     allow 1.5 to be canonical.
    ///     TODO: Remove above disclaimer after all bikeshedding about format is over,
    ///     or after March 2015.
    ///     Note that the quantity will NEVER be internally represented by a
    ///     floating point number. That is the whole point of this exercise.
    ///     Non-canonical values will still parse as long as they are well formed,
    ///     but will be re-emitted in their canonical form. (So always use canonical
    ///     form, or don't diff.)
    ///     This format is intended to make it difficult to use these numbers without
    ///     writing some sort of special handling code in the hopes that that will
    ///     cause implementors to also use a fixed point implementation.
    /// </summary>
    [JsonConverter(typeof(ResourceQuantityJsonConverter))]
    public class ResourceQuantity
    {
        public enum SuffixFormat
        {
            /// <summary>
            /// e.g., 12e6
            /// </summary>
            DecimalExponent,

            /// <summary>
            /// e.g., 12Mi (12 * 2^20)
            /// </summary>
            BinarySI,

            /// <summary>
            /// e.g., 12M  (12 * 10^6)
            /// </summary>
            DecimalSI,
        }

        public static readonly decimal MaxAllowed = (decimal)BigInteger.Pow(2, 63) - 1;

        private static readonly char[] SuffixChars = "eEinumkKMGTP".ToCharArray();
        private Fraction _unitlessValue;

        public ResourceQuantity(decimal n, int exp, SuffixFormat format)
        {
            _unitlessValue = Fraction.FromDecimal(n) * Fraction.Pow(10, exp);
            Format = format;
        }

        public SuffixFormat Format { get; private set; }

        public string CanonicalizeString()
        {
            return CanonicalizeString(Format);
        }

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

        //
        // CanonicalizeString = go version CanonicalizeBytes
        // CanonicalizeBytes returns the canonical form of q and its suffix (see comment on Quantity).
        //
        // Note about BinarySI:
        // * If q.Format is set to BinarySI and q.Amount represents a non-zero value between
        //   -1 and +1, it will be emitted as if q.Format were DecimalSI.
        // * Otherwise, if q.Format is set to BinarySI, fractional parts of q.Amount will be
        //   rounded up. (1.1i becomes 2i.)
        public string CanonicalizeString(SuffixFormat suffixFormat)
        {
            if (suffixFormat == SuffixFormat.BinarySI)
            {
                if (_unitlessValue > -1024 && _unitlessValue < 1024)
                {
                    return Suffixer.AppendMaxSuffix(_unitlessValue, SuffixFormat.DecimalSI);
                }

                if (HasMantissa(_unitlessValue))
                {
                    return Suffixer.AppendMaxSuffix(_unitlessValue, SuffixFormat.DecimalSI);
                }
            }

            return Suffixer.AppendMaxSuffix(_unitlessValue, suffixFormat);
        }

        public ResourceQuantity(string v)
        {
            if (v == null)
            {
                // No value has been defined, initialize to 0.
                _unitlessValue = new Fraction(0);
                Format = SuffixFormat.BinarySI;
                return;
            }

            var value = v.Trim();

            var si = value.IndexOfAny(SuffixChars);
            if (si == -1)
            {
                si = value.Length;
            }

            var literal = Fraction.FromString(value.Substring(0, si), CultureInfo.InvariantCulture);
            var suffixer = new Suffixer(value.Substring(si));

            _unitlessValue = literal.Multiply(Fraction.Pow(suffixer.Base, suffixer.Exponent));
            Format = suffixer.Format;

            if (Format == SuffixFormat.BinarySI && _unitlessValue > Fraction.FromDecimal(MaxAllowed))
            {
                _unitlessValue = Fraction.FromDecimal(MaxAllowed);
            }
        }

        public static implicit operator ResourceQuantity(string v)
        {
            return new ResourceQuantity(v);
        }

        private static bool HasMantissa(Fraction value)
        {
            if (value.IsZero)
            {
                return false;
            }

            return BigInteger.Remainder(value.Numerator, value.Denominator) > 0;
        }

        public static implicit operator decimal(ResourceQuantity v)
        {
            return v.ToDecimal();
        }

        public static implicit operator ResourceQuantity(decimal v)
        {
            return new ResourceQuantity(v, 0, SuffixFormat.DecimalExponent);
        }

        public bool Equals(ResourceQuantity other)
        {
            if (ReferenceEquals(null, other))
            {
                return false;
            }

            if (ReferenceEquals(this, other))
            {
                return true;
            }

            return _unitlessValue.Equals(other._unitlessValue);
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as ResourceQuantity);
        }

        public override int GetHashCode()
        {
            return _unitlessValue.GetHashCode();
        }

        public static bool operator ==(ResourceQuantity left, ResourceQuantity right)
        {
            if (left is null)
            {
                return right is null;
            }

            return left.Equals(right);
        }

        public static bool operator !=(ResourceQuantity left, ResourceQuantity right)
        {
            return !(left == right);
        }

        private sealed class Suffixer
        {
            private static readonly IReadOnlyDictionary<string, (int, int)> BinSuffixes =
                new Dictionary<string, (int, int)>
                {
                    // Don't emit an error when trying to produce
                    // a suffix for 2^0.
                    { "", (2, 0) },
                    { "Ki", (2, 10) },
                    { "Mi", (2, 20) },
                    { "Gi", (2, 30) },
                    { "Ti", (2, 40) },
                    { "Pi", (2, 50) },
                    { "Ei", (2, 60) },
                };

            private static readonly IReadOnlyDictionary<string, (int, int)> DecSuffixes =
                new Dictionary<string, (int, int)>
                {
                    { "n", (10, -9) },
                    { "u", (10, -6) },
                    { "m", (10, -3) },
                    { "", (10, 0) },
                    { "k", (10, 3) },
                    { "M", (10, 6) },
                    { "G", (10, 9) },
                    { "T", (10, 12) },
                    { "P", (10, 15) },
                    { "E", (10, 18) },
                };

            public Suffixer(string suffix)
            {
                // looked up
                {
                    if (DecSuffixes.TryGetValue(suffix, out var be))
                    {
                        (Base, Exponent) = be;
                        Format = SuffixFormat.DecimalSI;

                        return;
                    }
                }

                {
                    if (BinSuffixes.TryGetValue(suffix, out var be))
                    {
                        (Base, Exponent) = be;
                        Format = SuffixFormat.BinarySI;

                        return;
                    }
                }

                if (char.ToLower(suffix[0]) == 'e')
                {
                    Base = 10;
                    Exponent = int.Parse(suffix.Substring(1));
                    Format = SuffixFormat.DecimalExponent;
                    return;
                }

                throw new ArgumentException("unable to parse quantity's suffix");
            }

            public SuffixFormat Format { get; }

            public int Base { get; }
            public int Exponent { get; }

            public static string AppendMaxSuffix(Fraction value, SuffixFormat format)
            {
                if (value.IsZero)
                {
                    return "0";
                }

                switch (format)
                {
                    case SuffixFormat.DecimalExponent:
                        {
                            var minE = -9;
                            var lastv = Roundup(value * Fraction.Pow(10, -minE));

                            for (var exp = minE; ; exp += 3)
                            {
                                var v = value * Fraction.Pow(10, -exp);
                                if (HasMantissa(v))
                                {
                                    break;
                                }

                                minE = exp;
                                lastv = v;
                            }

                            if (minE == 0)
                            {
                                return $"{(decimal)lastv}";
                            }

                            return $"{(decimal)lastv}e{minE}";
                        }

                    case SuffixFormat.BinarySI:
                        return AppendMaxSuffix(value, BinSuffixes);
                    case SuffixFormat.DecimalSI:
                        return AppendMaxSuffix(value, DecSuffixes);
                    default:
                        throw new ArgumentOutOfRangeException(nameof(format), format, null);
                }
            }

            private static string AppendMaxSuffix(Fraction value, IReadOnlyDictionary<string, (int, int)> suffixes)
            {
                var min = suffixes.First();
                var suffix = min.Key;
                var lastv = Roundup(value * Fraction.Pow(min.Value.Item1, -min.Value.Item2));

                foreach (var kv in suffixes.Skip(1))
                {
                    var v = value * Fraction.Pow(kv.Value.Item1, -kv.Value.Item2);
                    if (HasMantissa(v))
                    {
                        break;
                    }

                    suffix = kv.Key;
                    lastv = v;
                }

                return $"{(decimal)lastv}{suffix}";
            }

            private static Fraction Roundup(Fraction lastv)
            {
                var round = BigInteger.DivRem(lastv.Numerator, lastv.Denominator, out var remainder);
                if (!remainder.IsZero)
                {
                    lastv = round + 1;
                }

                return lastv;
            }
        }

        public int ToInt32()
        {
            return _unitlessValue.ToInt32();
        }

        public long ToInt64()
        {
            return _unitlessValue.ToInt64();
        }

        public uint ToUInt32()
        {
            return _unitlessValue.ToUInt32();
        }

        public ulong ToUInt64()
        {
            return _unitlessValue.ToUInt64();
        }

        public BigInteger ToBigInteger()
        {
            return _unitlessValue.ToBigInteger();
        }

        public decimal ToDecimal()
        {
            return _unitlessValue.ToDecimal();
        }

        public double ToDouble()
        {
            return _unitlessValue.ToDouble();
        }
    }
}
