﻿using System;

namespace Oni
{
    internal struct Vector3 : IEquatable<Vector3>
    {
        public float X;
        public float Y;
        public float Z;

        public Vector3(float all)
        {
            X = all;
            Y = all;
            Z = all;
        }

        public Vector3(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public Vector3(float[] values, int index = 0)
        {
            int i = index * 3;

            X = values[i + 0];
            Y = values[i + 1];
            Z = values[i + 2];
        }

        public void CopyTo(float[] values, int index = 0)
        {
            values[index + 0] = X;
            values[index + 1] = Y;
            values[index + 2] = Z;
        }

        public Vector2 XZ => new Vector2(X, Z);

        public static Vector3 operator +(Vector3 v1, Vector3 v2)
        {
            v1.X += v2.X;
            v1.Y += v2.Y;
            v1.Z += v2.Z;

            return v1;
        }

        public static Vector3 operator -(Vector3 v1, Vector3 v2)
        {
            v1.X -= v2.X;
            v1.Y -= v2.Y;
            v1.Z -= v2.Z;

            return v1;
        }

        public static Vector3 operator -(Vector3 v)
        {
            v.X = -v.X;
            v.Y = -v.Y;
            v.Z = -v.Z;

            return v;
        }

        public static Vector3 operator *(Vector3 v, float s)
        {
            v.X *= s;
            v.Y *= s;
            v.Z *= s;

            return v;
        }

        public static Vector3 operator *(float s, Vector3 v)
        {
            v.X *= s;
            v.Y *= s;
            v.Z *= s;

            return v;
        }

        public static Vector3 operator *(Vector3 v1, Vector3 v2) => new Vector3
        {
            X = v1.X * v2.X,
            Y = v1.Y * v2.Y,
            Z = v1.Z * v2.Z,
        };

        public static Vector3 operator /(Vector3 v, float s) => v * (1.0f / s);

        public static Vector3 operator /(Vector3 v1, Vector3 v2) => new Vector3
        {
            X = v1.X /= v2.X,
            Y = v1.Y /= v2.Y,
            Z = v1.Z /= v2.Z
        };

        public static void Add(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
        {
            r.X = v1.X + v2.X;
            r.Y = v1.Y + v2.Y;
            r.Z = v1.Z + v2.Z;
        }

        public static void Substract(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
        {
            r.X = v1.X - v2.X;
            r.Y = v1.Y - v2.Y;
            r.Z = v1.Z - v2.Z;
        }

        public static void Multiply(ref Vector3 v, float f, out Vector3 r)
        {
            r.X = v.X * f;
            r.Y = v.Y * f;
            r.Z = v.Z * f;
        }

        public void Scale(float scale)
        {
            X *= scale;
            Y *= scale;
            Z *= scale;
        }

        public static Vector3 Clamp(Vector3 v, Vector3 min, Vector3 max)
        {
            Vector3 r;

            float x = v.X;
            x = (x > max.X) ? max.X : x;
            x = (x < min.X) ? min.X : x;

            float y = v.Y;
            y = (y > max.Y) ? max.Y : y;
            y = (y < min.Y) ? min.Y : y;

            float z = v.Z;
            z = (z > max.Z) ? max.Z : z;
            z = (z < min.Z) ? min.Z : z;

            r.X = x;
            r.Y = y;
            r.Z = z;

            return r;
        }

        public static Vector3 Cross(Vector3 v1, Vector3 v2)
        {
            return new Vector3(
                v1.Y * v2.Z - v1.Z * v2.Y,
                v1.Z * v2.X - v1.X * v2.Z,
                v1.X * v2.Y - v1.Y * v2.X);
        }

        public static void Cross(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
        {
            r = new Vector3(
                v1.Y * v2.Z - v1.Z * v2.Y,
                v1.Z * v2.X - v1.X * v2.Z,
                v1.X * v2.Y - v1.Y * v2.X);
        }

        public static float Dot(Vector3 v1, Vector3 v2)
        {
            return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z;
        }

        public static float Dot(ref Vector3 v1, ref Vector3 v2) => v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z;

        public float Dot(ref Vector3 v) => X * v.X + Y * v.Y + Z * v.Z;

        public static Vector3 Transform(Vector3 v, Quaternion q)
        {
            Quaternion vq = new Quaternion(v, 0.0f);
            q = q * vq * Quaternion.Conjugate(q);
            return new Vector3(q.X, q.Y, q.Z);
        }

        public static Vector3 Transform(Vector3 v, ref Matrix m)
        {
            return new Vector3(
                v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31 + m.M41,
                v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32 + m.M42,
                v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33 + m.M43);
        }

        public static void Transform(ref Vector3 v, ref Matrix m, out Vector3 r)
        {
            r.X = v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31 + m.M41;
            r.Y = v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32 + m.M42;
            r.Z = v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33 + m.M43;
        }

        public static Vector3 TransformNormal(Vector3 v, ref Matrix m)
        {
            return new Vector3(
                v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31,
                v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32,
                v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33);
        }

        public static void Transform(Vector3[] v, ref Matrix m, Vector3[] r)
        {
            for (int i = 0; i < v.Length; i++)
            {
                float x = v[i].X;
                float y = v[i].Y;
                float z = v[i].Z;

                r[i].X = x * m.M11 + y * m.M21 + z * m.M31 + m.M41;
                r[i].Y = x * m.M12 + y * m.M22 + z * m.M32 + m.M42;
                r[i].Z = x * m.M13 + y * m.M23 + z * m.M33 + m.M43;
            }
        }

        public static Vector3[] Transform(Vector3[] v, ref Matrix m)
        {
            var r = new Vector3[v.Length];
            Transform(v, ref m, r);
            return r;
        }

        public static void TransformNormal(Vector3[] v, ref Matrix m, Vector3[] r)
        {
            for (int i = 0; i < v.Length; i++)
            {
                float x = v[i].X;
                float y = v[i].Y;
                float z = v[i].Z;

                r[i].X = x * m.M11 + y * m.M21 + z * m.M31;
                r[i].Y = x * m.M12 + y * m.M22 + z * m.M32;
                r[i].Z = x * m.M13 + y * m.M23 + z * m.M33;
            }
        }

        public static Vector3[] TransformNormal(Vector3[] v, ref Matrix m)
        {
            var r = new Vector3[v.Length];
            TransformNormal(v, ref m, r);
            return r;
        }

        public static Vector3 Min(Vector3 v1, Vector3 v2)
        {
            if (v2.X < v1.X)
                v1.X = v2.X;

            if (v2.Y < v1.Y)
                v1.Y = v2.Y;

            if (v2.Z < v1.Z)
                v1.Z = v2.Z;

            return v1;
        }

        public static void Min(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
        {
            r.X = (v1.X < v2.X) ? v1.X : v2.X;
            r.Y = (v1.Y < v2.Y) ? v1.Y : v2.Y;
            r.Z = (v1.Z < v2.Z) ? v1.Z : v2.Z;
        }

        public static Vector3 Max(Vector3 v1, Vector3 v2)
        {
            if (v2.X > v1.X)
                v1.X = v2.X;

            if (v2.Y > v1.Y)
                v1.Y = v2.Y;

            if (v2.Z > v1.Z)
                v1.Z = v2.Z;

            return v1;
        }

        public static void Max(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
        {
            r.X = (v1.X > v2.X) ? v1.X : v2.X;
            r.Y = (v1.Y > v2.Y) ? v1.Y : v2.Y;
            r.Z = (v1.Z > v2.Z) ? v1.Z : v2.Z;
        }

        public static Vector3 Normalize(Vector3 v) => v * (1.0f / v.Length());

        public void Normalize()
        {
            float k = 1.0f / Length();

            X *= k;
            Y *= k;
            Z *= k;
        }

        public float LengthSquared() => X * X + Y * Y + Z * Z;

        public float Length() => FMath.Sqrt(LengthSquared());

        public static float Distance(Vector3 v1, Vector3 v2) => FMath.Sqrt((v2 - v1).LengthSquared());

        public static float DistanceSquared(Vector3 v1, Vector3 v2) => (v2 - v1).LengthSquared();

        public static Vector3 Lerp(Vector3 v1, Vector3 v2, float amount) => v1 + (v2 - v1) * amount;

        public static bool EqualsEps(Vector3 v1, Vector3 v2)
        {
            Vector3 d = v2 - v1;

            float dx = Math.Abs(d.X);
            float dy = Math.Abs(d.Y);
            float dz = Math.Abs(d.Z);

            return (dx < 0.0001f && dy < 0.0001f && dz < 0.0001f);
        }

        public static bool operator ==(Vector3 v1, Vector3 v2) => v1.X == v2.X && v1.Y == v2.Y && v1.Z == v2.Z;
        public static bool operator !=(Vector3 v1, Vector3 v2) => v1.X != v2.X || v1.Y != v2.Y || v1.Z != v2.Z;

        public bool Equals(Vector3 other) => X == other.X && Y == other.Y && Z == other.Z;
        public override bool Equals(object obj) => obj is Vector3 && Equals((Vector3)obj);
        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode();

        public override string ToString() => $"{{{X} {Y} {Z}}}";

        private static Vector3 zero = new Vector3();
        private static Vector3 one = new Vector3(1.0f);
        private static Vector3 up = new Vector3(0.0f, 1.0f, 0.0f);
        private static Vector3 down = new Vector3(0.0f, -1.0f, 0.0f);
        private static Vector3 right = new Vector3(1.0f, 0.0f, 0.0f);
        private static Vector3 left = new Vector3(-1.0f, 0.0f, 0.0f);
        private static Vector3 backward = new Vector3(0.0f, 0.0f, 1.0f);
        private static Vector3 forward = new Vector3(0.0f, 0.0f, -1.0f);

        public static Vector3 Zero => zero;
        public static Vector3 One => one;
        public static Vector3 Up => up;
        public static Vector3 Down => down;
        public static Vector3 Left => left;
        public static Vector3 Right => right;
        public static Vector3 Backward => backward;
        public static Vector3 Forward => forward;
        public static Vector3 UnitX => right;
        public static Vector3 UnitY => up;
        public static Vector3 UnitZ => backward;

        public float this[int i]
        {
            get
            {
                if (i == 1)
                    return Y;
                else if (i < 1)
                    return X;
                else
                    return Z;
            }
        }
    }
}
