#if DEBUG
#define RANGE_CHECK
#endif

using System;
using System.Runtime.CompilerServices;

namespace SideScrollerGame.Sim.Math;

public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquatable<FixPoint16Long>
{
    public FixPoint16Long(FixPoint16Long other)
    {
        m_Value = other.m_Value;
    }

    public FixPoint16Long(int value)
    {
        m_Value = (long)value << c_Shift;
    }

    public FixPoint16Long(long value)
    {
#if RANGE_CHECK
        if (value < c_IntegerMin || value > c_IntegerMax)
        {
            throw new ArithmeticException($"Long to FixPoint argument out of range: {value}");
        }
#endif
        m_Value = value << c_Shift;
    }

    public FixPoint16Long(double value)
    {
#if RANGE_CHECK
        if (value < c_IntegerMin || value > c_IntegerMax)
        {
            throw new ArithmeticException($"Double to FixPoint argument out of range: {value}");
        }
#endif
        if (value < 0.0)
        {
            m_Value = (int)((value * c_Multiplier) - 0.5);
        }
        else
        {
            m_Value = (int)((value * c_Multiplier) + 0.5);
        }
    }

    public FixPoint16Long(float value)
    {
#if RANGE_CHECK
        if (value < c_IntegerMin || value > c_IntegerMax)
        {
            throw new ArithmeticException($"Single to FixPoint argument out of range: {value}");
        }
#endif
        if (value < 0.0f)
        {
            m_Value = (int)((value * c_MultiplierFloat) - 0.5f);
        }
        else
        {
            m_Value = (int)((value * c_MultiplierFloat) + 0.5f);
        }
    }

    public long ToLongFloor()
    {
        return m_Value >> c_Shift;
    }

    public long ToLongCeil()
    {
        return (m_Value + c_FractionMask) >> c_Shift;
    }

    public long ToLongRound()
    {
        if (m_Value < 0)
        {
            return -((-m_Value + c_Half) >> c_Shift);
        }

        return (m_Value + c_Half) >> c_Shift;
    }

    public long ToLong()
    {
        if (m_Value < 0)
        {
            return -(-m_Value >> c_Shift);
        }

        return m_Value >> c_Shift;
    }

    public double ToDouble()
    {
        return c_Divisor * m_Value;
    }

    public float ToFloat()
    {
        return c_DivisorFloat * m_Value;
    }

    public override string ToString()
    {
        return $"{ToDouble()}[0x{m_Value:x16}]";
    }

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

    public int CompareTo(object? obj)
    {
        if (obj is not FixPoint16Long other)
            return -1;
        
        return m_Value.CompareTo(other.m_Value);
    }

    public int CompareTo(FixPoint16Long other)
    {
        return m_Value.CompareTo(other.m_Value);
    }

    public override bool Equals(object? obj)
    {
        if (obj == null)
        {
            return false;
        }

        return ((FixPoint16Long)obj).m_Value == m_Value;
    }

    public bool Equals(FixPoint16Long other)
    {
        return other.m_Value == m_Value;
    }

    public bool IsZero()
    {
        return m_Value == 0L;
    }

    public static bool operator ==(FixPoint16Long a, FixPoint16Long b)
    {
        return a.m_Value == b.m_Value;
    }

    public static bool operator !=(FixPoint16Long a, FixPoint16Long b)
    {
        return a.m_Value != b.m_Value;
    }

    public static bool operator <(FixPoint16Long a, FixPoint16Long b)
    {
        return a.m_Value < b.m_Value;
    }

    public static bool operator >(FixPoint16Long a, FixPoint16Long b)
    {
        return a.m_Value > b.m_Value;
    }

    public static bool operator <=(FixPoint16Long a, FixPoint16Long b)
    {
        return a.m_Value <= b.m_Value;
    }

    public static bool operator >=(FixPoint16Long a, FixPoint16Long b)
    {
        return a.m_Value >= b.m_Value;
    }

    public static FixPoint16Long operator <<(FixPoint16Long a, int shift)
    {
        return new() { m_Value = a.m_Value << shift };
    }

    public static FixPoint16Long operator >> (FixPoint16Long a, int shift)
    {
        return new() { m_Value = a.m_Value >> shift };
    }

    public static FixPoint16Long operator +(FixPoint16Long a, FixPoint16Long b)
    {
        return new() { m_Value = a.m_Value + b.m_Value };
    }

    public static FixPoint16Long operator -(FixPoint16Long a, FixPoint16Long b)
    {
        return new() { m_Value = a.m_Value - b.m_Value };
    }

    public static FixPoint16Long operator *(FixPoint16Long a, FixPoint16Long b)
    {
        Int128 bigA = a.m_Value;
        Int128 bigB = b.m_Value;
        var result = ((bigA * bigB) + c_Half) >> c_Shift;
#if RANGE_CHECK
        if (result < long.MinValue || result > long.MaxValue)
        {
            throw new ArithmeticException($"Multiplication result out of range: {result}");
        }
#endif
        return new() { m_Value = (long)result };
    }

    public static FixPoint16Long operator /(FixPoint16Long a, FixPoint16Long b)
    {
#if RANGE_CHECK
        if (b.m_Value == 0)
        {
            throw new ArithmeticException("Divison by zero");
        }
#endif
        Int128 result;

        if (((ulong)a.m_Value & 0x8000000000000000UL) == ((ulong)b.m_Value & 0x8000000000000000UL))
        {
            result = (((Int128)a.m_Value << c_Shift) + (b.m_Value / 2)) / b.m_Value;
        }
        else
        {
            result = (((Int128)a.m_Value << c_Shift) - (b.m_Value / 2)) / b.m_Value;
        }
#if RANGE_CHECK
        if (result < long.MinValue || result > long.MaxValue)
        {
            throw new ArithmeticException($"Division result out of range: {result}");
        }
#endif
        return new() { m_Value = (long)result };
    }

    public static FixPoint16Long operator -(FixPoint16Long a)
    {
        return new() { m_Value = -a.m_Value };
    }

    public static FixPoint16Long operator *(FixPoint16Long a, int value)
    {
        return new() { m_Value = a.m_Value * value };
    }

    public static FixPoint16Long operator /(FixPoint16Long a, int value)
    {
#if RANGE_CHECK
        if (value == 0)
        {
            throw new ArithmeticException("Divison by zero");
        }
#endif
        if (((a.m_Value >> 32) & 0x80000000) == (value & 0x80000000))
        {
            return new() { m_Value = (long)(((Int128)a.m_Value + (value / 2)) / value) };
        }

        return new() { m_Value = (long)(((Int128)a.m_Value - (value / 2)) / value) };
    }

    public static implicit operator FixPoint16Long(int value)
    {
        return new(value);
    }

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

    public static implicit operator FixPoint16Long(FixPoint16 value)
    {
        return new() { m_Value = value.m_Value };
    }

    public static explicit operator FixPoint16Long(double value)
    {
        return new(value);
    }

    public static explicit operator FixPoint16Long(float value)
    {
        return new(value);
    }

    public static explicit operator int(FixPoint16Long value)
    {
        return (int)value.ToLong();
    }

    public static explicit operator long(FixPoint16Long value)
    {
        return value.ToLong();
    }

    public static explicit operator double(FixPoint16Long value)
    {
        return value.ToDouble();
    }

    public static explicit operator float(FixPoint16Long value)
    {
        return value.ToFloat();
    }

    public static FixPoint16Long Floor(FixPoint16Long value)
    {
        return value.ToLongFloor();
    }

    public static FixPoint16Long Ceil(FixPoint16Long value)
    {
        return value.ToLongCeil();
    }

    public static FixPoint16Long Round(FixPoint16Long value)
    {
        return value.ToLongRound();
    }

    public static int Sign(FixPoint16Long value)
    {
        return System.Math.Sign(value.m_Value);
    }

    public static FixPoint16Long Abs(FixPoint16Long value)
    {
        return new() { m_Value = System.Math.Abs(value.m_Value) };
    }

    public static FixPoint16Long Min(FixPoint16Long value1, FixPoint16Long value2)
    {
        return new() { m_Value = System.Math.Min(value1.m_Value, value2.m_Value) };
    }

    public static FixPoint16Long Max(FixPoint16Long value1, FixPoint16Long value2)
    {
        return new() { m_Value = System.Math.Max(value1.m_Value, value2.m_Value) };
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static FixPoint16Long Length(FixPoint16Long a, FixPoint16Long b)
    {
        var aSquared = a.m_Value * a.m_Value;
        var bSquared = b.m_Value * b.m_Value;
        var value = aSquared + bSquared;
#if RANGE_CHECK
        if (value < 0)
        {
            throw new ArithmeticException($"Length squared out of range: {value}");
        }
#endif

        return new() { m_Value = IntMath.Sqrt(value) };
    }

    private const int c_Shift = 16;
    private const long c_IntegerMin = -140737488355327L;
    private const long c_IntegerMax = 140737488355327L;
    private const long c_Half = 32786L;
    private const long c_FractionMask = 0x000000000000ffffL;
    private const double c_Multiplier = 65536.0;
    private const double c_Divisor = 1.0 / 65536.0;
    private const float c_MultiplierFloat = 65536.0f;
    private const float c_DivisorFloat = (float)c_Divisor;

    public long m_Value;

    public static readonly FixPoint16Long Zero = new() { m_Value = 0 };
    public static readonly FixPoint16Long MinValue = new() { m_Value = long.MinValue };
    public static readonly FixPoint16Long MaxValue = new() { m_Value = long.MaxValue };
    public static readonly FixPoint16Long Epsilon = new() { m_Value = 1 };
    public static readonly FixPoint16Long MinusEpsilon = new() { m_Value = -1 };
    public static readonly FixPoint16Long One = new(1);
    public static readonly FixPoint16Long MinusOne = new(-1);
    public static readonly FixPoint16Long Half = new() { m_Value = One.m_Value / 2 };
    public static readonly FixPoint16Long MinusHalf = -Half;
}
