// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.Globalization;
using System.Runtime.InteropServices;
using UnityEngine;

namespace HoloToolkit.Unity
{
    /// <summary>
    /// 3D integer class - operates similarly to Unity's Vector3D
    /// </summary>
    [System.Serializable]
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct Int3 : IEquatable<Int3>, IFormattable
    {
        public static readonly Int3 zero = new Int3(0, 0, 0);
        public static readonly Int3 one = new Int3(1, 1, 1);

        public static readonly Int3 forward = new Int3(0, 0, 1);
        public static readonly Int3 back = new Int3(0, 0, -1);
        public static readonly Int3 up = new Int3(0, 1, 0);
        public static readonly Int3 down = new Int3(0, -1, 0);
        public static readonly Int3 left = new Int3(-1, 0, 0);
        public static readonly Int3 right = new Int3(1, 0, 0);

        public int x;
        public int y;
        public int z;

        public Int3(int x, int y, int z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }

        public Int3(int x, int y)
        {
            this.x = x;
            this.y = y;
            this.z = 0;
        }

        public override int GetHashCode()
        {
            var hashCode = x.GetHashCode();
            hashCode = (hashCode * 397) ^ y.GetHashCode();
            hashCode = (hashCode * 397) ^ z.GetHashCode();
            return hashCode;
        }

        public int this[int index]
        {
            get
            {
                if (index == 0) return x;
                if (index == 1) return y;
                if (index == 2) return z;
                throw new ArgumentOutOfRangeException("Invalid Int3 index!");
            }
            set
            {
                if (index == 0) x = value;
                if (index == 1) y = value;
                if (index == 2) z = value;
                throw new ArgumentOutOfRangeException("Invalid Int3 index!");
            }
        }

        #region conversion
        public static explicit operator Vector3(Int3 v)
        {
            return new Vector3(v.x, v.y, v.z);
        }

        public static explicit operator Vector2(Int3 v)
        {
            return new Vector2(v.x, v.y);
        }
        #endregion

        #region math operations
        public float magnitude
        {
            get { return Magnitude(this); }
        }

        public int sqrMagnitude
        {
            get { return SqrMagnitude(this); }
        }

        public int dotOne
        {
            get { return DotOne(this); }
        }

        public float Magnitude(Int3 v)
        {
            return Mathf.Sqrt(SqrMagnitude(v));
        }

        public int SqrMagnitude(Int3 v)
        {
            return v.x * v.x + v.y * v.y + v.z * v.z;
        }

        public static Int3 Scale(Int3 l, Int3 r)
        {
            return new Int3(l.x * r.x, l.y * r.y, l.z * r.z);
        }

        public static int Dot(Int3 l, Int3 r)
        {
            return l.x * r.x + l.y * r.y + l.z * r.z;
        }

        public static int DotOne(Int3 v)
        {
            return v.x * v.y * v.z;
        }

        public static Int3 Cross(Int3 l, Int3 r)
        {
            return new Int3(l.y * r.z - l.z * r.y,
                            l.z * r.x - l.x * r.z,
                            l.x * r.y - l.y * r.x);
        }

        public static Int3 Min(Int3 l, Int3 r)
        {
            return new Int3(Mathf.Min(l.x, r.x), Mathf.Min(l.y, r.y), Mathf.Min(l.z, r.z));
        }

        public static Int3 Max(Int3 l, Int3 r)
        {
            return new Int3(Mathf.Max(l.x, r.x), Mathf.Max(l.y, r.y), Mathf.Max(l.z, r.z));
        }

        public static Int3 Clamp(Int3 v, Int3 min, Int3 max)
        {
            return new Int3(Mathf.Clamp(v.x, min.x, max.x),
                            Mathf.Clamp(v.y, min.y, max.y),
                            Mathf.Clamp(v.z, min.z, max.z));
        }

        public static Int3 ClosestPowerOfTwo(Int3 v)
        {
            return new Int3(Mathf.ClosestPowerOfTwo(v.x), Mathf.ClosestPowerOfTwo(v.y), Mathf.ClosestPowerOfTwo(v.z));
        }

        public static int CubicToLinearIndex(Int3 v, Int3 size)
        {
            return (v.x) +
                   (v.y * size.x) +
                   (v.z * size.x * size.y);
        }

        public static Int3 LinearToCubicIndex(int v, Int3 size)
        {
            return new Int3(v % size.x,
                           (v / size.x) % size.y,
                           (v / (size.x * size.y)) % size.z);
        }
        #endregion math operations

        #region math operators
        public static Int3 operator +(Int3 l, Int3 r)
        {
            return new Int3(l.x + r.x, l.y + r.y, l.z + r.z);
        }

        public static Int3 operator -(Int3 l, Int3 r)
        {
            return new Int3(l.x - r.x, l.y - r.y, l.z - r.z);
        }

        public static Int3 operator -(Int3 v)
        {
            return new Int3(-v.x, -v.y, -v.z);
        }

        public static Int3 operator *(Int3 v, int d)
        {
            return new Int3(v.x * d, v.y * d, v.z * d);
        }

        public static Int3 operator *(int d, Int3 v)
        {
            return new Int3(v.x * d, v.y * d, v.z * d);
        }

        public static Int3 operator /(Int3 v, int d)
        {
            return new Int3(v.x / d, v.y / d, v.z / d);
        }
        #endregion math operators

        #region comparison
        public bool Equals(Int3 other)
        {
            return x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z);
        }

        public static bool operator ==(Int3 l, Int3 r)
        {
            return l.Equals(r);
        }

        public static bool operator !=(Int3 l, Int3 r)
        {
            return !l.Equals(r);
        }

        public override bool Equals(object value)
        {
            return (value is Int3) ? Equals((Int3)value) : false;
        }
        #endregion

        #region IFormattable
        public override string ToString()
        {
            return ToString(CultureInfo.CurrentCulture);
        }

        public string ToString(IFormatProvider formatProvider)
        {
            return string.Format(formatProvider, "X:{0} Y:{1} Z:{2}", x, y, z);
        }

        public string ToString(string format, IFormatProvider formatProvider)
        {
            if (format == null)
            {
                return ToString(formatProvider);
            }

            return string.Format(formatProvider,
                                 "X:{0} Y:{1} Z:{2}",
                                 x.ToString(format, formatProvider),
                                 y.ToString(format, formatProvider),
                                 z.ToString(format, formatProvider));
        }
        #endregion
    }
}