/*
© 2017 Altavant Technologies Inc.
Web: http://www.altavant.com
*/

namespace Altavant.Fusion.Graphics
{
	using System;
	using Fusion.Utils;

	public enum MatrixOrder : byte
	{
		Append,
		Prepend
	}

	public class Matrix
	{
		private float _m11;
		private float _m12;
		private float _m21;
		private float _m22;
		private float _offsetX;
		private float _offsetY;
		private float[] _values;

		public Matrix()
			: this(1F, 0F, 0F, 1F, 0F, 0F)
		{

		}

		public Matrix(float m11, float m12, float m21, float m22, float offsetX, float offsetY)
		{
			_m11 = m11;
			_m12 = m12;
			_m21 = m21;
			_m22 = m22;
			_offsetX = offsetX;
			_offsetY = offsetY;
		}

		public Matrix(Android.Graphics.Matrix matrix)
		{
			float[] values = new float[9];

			matrix.GetValues(values);

			_m11 = values[Android.Graphics.Matrix.MscaleX];
			_m21 = values[Android.Graphics.Matrix.MskewX];
			_offsetX = values[Android.Graphics.Matrix.MtransX];
			_m12 = values[Android.Graphics.Matrix.MskewY];
			_m22 = values[Android.Graphics.Matrix.MscaleY];
			_offsetY = values[Android.Graphics.Matrix.MtransY];
		}

		public float M11
		{
			get
			{
				return _m11;
			}
			set
			{
				_m11 = value;
			}
		}

		public float M12
		{
			get
			{
				return _m12;
			}
			set
			{
				_m12 = value;
			}
		}

		public float M21
		{
			get
			{
				return _m21;
			}
			set
			{
				_m21 = value;
			}
		}

		public float M22
		{
			get
			{
				return _m22;
			}
			set
			{
				_m22 = value;
			}
		}

		public float OffsetX
		{
			get
			{
				return _offsetX;
			}
			set
			{
				_offsetX = value;
			}
		}

		public float OffsetY
		{
			get
			{
				return _offsetY;
			}
			set
			{
				_offsetY = value;
			}
		}

		public static Matrix Identity
		{
			get
			{
				return new Matrix(1F, 0F, 0F, 1F, 0F, 0F);
			}
		}

		public bool IsIdentity
		{
			get
			{
				if ((_m11 == 1F) && (_m12 == 0F) && (_m21 == 0F) && (_m22 == 1F) && (_offsetX == 0F) && (_offsetY == 0F))
					return true;

				return false;
			}
		}

		public void SetValues(float m11, float m12, float m21, float m22, float offsetX, float offsetY)
		{
			_m11 = m11;
			_m12 = m12;
			_m21 = m21;
			_m22 = m22;
			_offsetX = offsetX;
			_offsetY = offsetY;
		}

		public void Reset()
		{
			_m11 = 1F;
			_m12 = 0F;
			_m21 = 0F;
			_m22 = 1F;
			_offsetX = 0F;
			_offsetY = 0F;
		}

		public void Multiply(Matrix matrix)
		{
			Multiply(matrix, MatrixOrder.Prepend);
		}

		public void Multiply(Matrix matrix, MatrixOrder order)
		{
			if (matrix == null)
				throw new ArgumentNullException(nameof(matrix));

			float a11, a12, a21, a22, aX, aY;
			float b11, b12, b21, b22, bX, bY;

			if (order == MatrixOrder.Append)
			{
				a11 = _m11;
				a12 = _m12;
				a21 = _m21;
				a22 = _m22;
				aX = _offsetX;
				aY = _offsetY;

				b11 = matrix.M11;
				b12 = matrix.M12;
				b21 = matrix.M21;
				b22 = matrix.M22;
				bX = matrix.OffsetX;
				bY = matrix.OffsetY;
			}
			else
			{
				a11 = matrix.M11;
				a12 = matrix.M12;
				a21 = matrix.M21;
				a22 = matrix.M22;
				aX = matrix.OffsetX;
				aY = matrix.OffsetY;

				b11 = _m11;
				b12 = _m12;
				b21 = _m21;
				b22 = _m22;
				bX = _offsetX;
				bY = _offsetY;
			}

			_m11 = a11 * b11 + a12 * b21;
			_m12 = a11 * b12 + a12 * b22;
			_m21 = a21 * b11 + a22 * b21;
			_m22 = a21 * b12 + a22 * b22;
			_offsetX = aX * b11 + aY * b21 + bX;
			_offsetY = aX * b12 + aY * b22 + bY;
		}

		public void Translate(float offsetX, float offsetY)
		{
			Translate(offsetX, offsetY, MatrixOrder.Prepend);
		}

		public void Translate(float offsetX, float offsetY, MatrixOrder order)
		{
			Matrix matrix = Translation(offsetX, offsetY);

			Multiply(matrix, order);
		}

		public void Rotate(float angle)
		{
			Rotate(angle, MatrixOrder.Prepend);
		}

		public void Rotate(float angle, MatrixOrder order)
		{
			Matrix matrix = Rotation(angle);

			Multiply(matrix, order);
		}

		public void RotateAt(float angle, Point center)
		{
			RotateAt(angle, center.X, center.Y, MatrixOrder.Prepend);
		}

		public void RotateAt(float angle, float centerX, float centerY)
		{
			RotateAt(angle, centerX, centerY, MatrixOrder.Prepend);
		}

		public void RotateAt(float angle, Point center, MatrixOrder order)
		{
			if (center == null)
				throw new ArgumentNullException(nameof(center));

			RotateAt(angle, center.X, center.Y, order);
		}

		public void RotateAt(float angle, float centerX, float centerY, MatrixOrder order)
		{
			Matrix matrix = RotationAt(angle, centerX, centerY);

			Multiply(matrix, order);
		}

		public void Scale(float scaleX, float scaleY)
		{
			Scale(scaleX, scaleY, MatrixOrder.Prepend);
		}

		public void Scale(float scaleX, float scaleY, MatrixOrder order)
		{
			Matrix matrix = Scaling(scaleX, scaleY);

			Multiply(matrix, order);
		}

		public void ScaleAt(float scaleX, float scaleY, Point center)
		{
			ScaleAt(scaleX, scaleY, center.X, center.Y, MatrixOrder.Prepend);
		}

		public void ScaleAt(float scaleX, float scaleY, float centerX, float centerY)
		{
			ScaleAt(scaleX, scaleY, centerX, centerY, MatrixOrder.Prepend);
		}

		public void ScaleAt(float scaleX, float scaleY, Point center, MatrixOrder order)
		{
			if (center == null)
				throw new ArgumentNullException(nameof(center));

			ScaleAt(scaleX, scaleY, center.X, center.Y, order);
		}

		public void ScaleAt(float scaleX, float scaleY, float centerX, float centerY, MatrixOrder order)
		{
			Matrix matrix = ScalingAt(scaleX, scaleY, centerX, centerY);

			Multiply(matrix, order);
		}

		public void Shear(float shearX, float shearY)
		{
			Shear(shearX, shearY, MatrixOrder.Prepend);
		}

		public void Shear(float shearX, float shearY, MatrixOrder order)
		{
			Matrix matrix = Shearing(shearX, shearY);

			Multiply(matrix, order);
		}

		public void ShearAt(float shearX, float shearY, Point center)
		{
			ShearAt(shearX, shearY, center.X, center.Y, MatrixOrder.Prepend);
		}

		public void ShearAt(float shearX, float shearY, float centerX, float centerY)
		{
			ShearAt(shearX, shearY, centerX, centerY, MatrixOrder.Prepend);
		}

		public void ShearAt(float shearX, float shearY, Point center, MatrixOrder order)
		{
			if (center == null)
				throw new ArgumentNullException(nameof(center));

			ShearAt(shearX, shearY, center.X, center.Y, order);
		}

		public void ShearAt(float shearX, float shearY, float centerX, float centerY, MatrixOrder order)
		{
			Matrix matrix = ShearingAt(shearX, shearY, centerX, centerY);

			Multiply(matrix, order);
		}

		public void Skew(float skewX, float skewY)
		{
			Skew(skewX, skewY, MatrixOrder.Prepend);
		}

		public void Skew(float skewX, float skewY, MatrixOrder order)
		{
			Matrix matrix = Skewing(skewX, skewY);

			Multiply(matrix, order);
		}

		public void SkewAt(float skewX, float skewY, Point center)
		{
			SkewAt(skewX, skewY, center.X, center.Y, MatrixOrder.Prepend);
		}

		public void SkewAt(float skewX, float skewY, float centerX, float centerY)
		{
			SkewAt(skewX, skewY, centerX, centerY, MatrixOrder.Prepend);
		}

		public void SkewAt(float skewX, float skewY, Point center, MatrixOrder order)
		{
			if (center == null)
				throw new ArgumentNullException(nameof(center));

			SkewAt(skewX, skewY, center.X, center.Y, order);
		}

		public void SkewAt(float skewX, float skewY, float centerX, float centerY, MatrixOrder order)
		{
			Matrix matrix = SkewingAt(skewX, skewY, centerX, centerY);
			
			Multiply(matrix, order);
		}

		public Point TransformPoint(Point pt)
		{
			return TransformPoint(pt.X, pt.Y);
		}

		public Point TransformPoint(float x, float y)
		{
			float x1 = x * _m11 + y * _m21 + _offsetX;
			float y1 = x * _m12 + y * _m22 + _offsetY;

			return new Point(x1, y1);
		}

		public Matrix Clone()
		{
			return new Matrix(_m11, _m12, _m21, _m22, _offsetX, _offsetY);
		}

		public override int GetHashCode()
		{
			return _m11.GetHashCode() ^ _m12.GetHashCode() ^ _m21.GetHashCode() ^ _m22.GetHashCode() ^ _offsetX.GetHashCode() ^ _offsetY.GetHashCode();
		}

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

			if (obj == this)
				return true;

			if (obj is Matrix)
			{
				Matrix matrix = (Matrix)obj;

				if ((_m11 == matrix.M11) && (_m12 == matrix.M12) && (_m21 == matrix.M21) && (_m22 == matrix.M22) && (_offsetX == matrix.OffsetX) && (_offsetY == matrix.OffsetY))
					return true;
			}

			return false;
		}

		public override string ToString()
		{
			CharBuffer cb = new CharBuffer();

			cb.Add(Converter.ToString(_m11, 3));
			cb.Add(", ");
			cb.Add(Converter.ToString(_m12, 3));
			cb.Add(", ");
			cb.Add(Converter.ToString(_m21, 3));
			cb.Add(", ");
			cb.Add(Converter.ToString(_m22, 3));
			cb.Add(", ");
			cb.Add(Converter.ToString(_offsetX, 3));
			cb.Add(", ");
			cb.Add(Converter.ToString(_offsetY, 3));

			return cb.ToString();
		}

		public static Matrix Parse(string s)
		{
			if (string.IsNullOrEmpty(s))
				throw new ArgumentException(nameof(s));

			string[] vals = Utils.Split(s);

			float m11 = Convert.ToSingle(vals[0], Converter.NumberFormatInfo);
			float m12 = Convert.ToSingle(vals[1], Converter.NumberFormatInfo);
			float m21 = Convert.ToSingle(vals[2], Converter.NumberFormatInfo);
			float m22 = Convert.ToSingle(vals[3], Converter.NumberFormatInfo);
			float offsetX = Convert.ToSingle(vals[4], Converter.NumberFormatInfo);
			float offsetY = Convert.ToSingle(vals[5], Converter.NumberFormatInfo);

			return new Matrix(m11, m12, m21, m22, offsetX, offsetY);
		}

		public static Matrix Translation(Point offset)
		{
			return new Matrix(1F, 0F, 0F, 1F, offset.X, offset.Y);
		}

		public static Matrix Translation(float offsetX, float offsetY)
		{
			return new Matrix(1F, 0F, 0F, 1F, offsetX, offsetY);
		}

		public static Matrix Rotation(float angle)
		{
			double radianAngle = angle * Math.PI / 180F;
			
			float cosAngle = (float)Math.Cos(radianAngle);
			float sinAngle = (float)Math.Sin(radianAngle);
			
			return new Matrix(cosAngle, sinAngle, -sinAngle, cosAngle, 0F, 0F);
		}

		public static Matrix RotationAt(float angle, Point center)
		{
			return RotationAt(angle, center.X, center.Y);
		}

		public static Matrix RotationAt(float angle, float centerX, float centerY)
		{
			double radianAngle = angle * Math.PI / 180F;
			
			float cosAngle = (float)Math.Cos(radianAngle);
			float sinAngle = (float)Math.Sin(radianAngle);
			
			Matrix m1 = new Matrix(1F, 0F, 0F, 1F, -centerX, -centerY);
			Matrix m2 = new Matrix(cosAngle, sinAngle, -sinAngle, cosAngle, 0F, 0F);
			Matrix m3 = new Matrix(1F, 0F, 0F, 1F, centerX, centerY);
			
			m1.Multiply(m2, MatrixOrder.Append);
			m1.Multiply(m3, MatrixOrder.Append);
			
			return m1;
		}

		public static Matrix Scaling(float scale)
		{
			return Scaling(scale, scale);
		}

		public static Matrix Scaling(float scaleX, float scaleY)
		{
			return new Matrix(scaleX, 0F, 0F, scaleY, 0F, 0F);
		}

		public static Matrix ScalingAt(float scale, Point center)
		{
			return ScalingAt(scale, scale, center.X, center.Y);
		}

		public static Matrix ScalingAt(float scaleX, float scaleY, Point center)
		{
			return ScalingAt(scaleX, scaleY, center.X, center.Y);
		}

		public static Matrix ScalingAt(float scale, float centerX, float centerY)
		{
			return ScalingAt(scale, scale, centerX, centerY);
		}

		public static Matrix ScalingAt(float scaleX, float scaleY, float centerX, float centerY)
		{
			Matrix m1 = new Matrix(1F, 0F, 0F, 1F, -centerX, -centerY);
			Matrix m2 = new Matrix(scaleX, 0F, 0F, scaleY, 0F, 0F);
			Matrix m3 = new Matrix(1F, 0F, 0F, 1F, centerX, centerY);
			
			m1.Multiply(m2, MatrixOrder.Append);
			m1.Multiply(m3, MatrixOrder.Append);

			return m1;
		}

		public static Matrix Shearing(float shearX, float shearY)
		{
			return new Matrix(1F, shearY, shearX, 1F, 0F, 0F);
		}

		public static Matrix ShearingAt(float shearX, float shearY, Point center)
		{
			return ShearingAt(shearX, shearY, center.X, center.Y);
		}

		public static Matrix ShearingAt(float shearX, float shearY, float centerX, float centerY)
		{
			Matrix m1 = new Matrix(1F, 0F, 0F, 1F, -centerX, -centerY);
			Matrix m2 = new Matrix(1F, shearY, shearX, 1F, 0F, 0F);
			Matrix m3 = new Matrix(1F, 0F, 0F, 1F, centerX, centerY);
			
			m1.Multiply(m2, MatrixOrder.Append);
			m1.Multiply(m3, MatrixOrder.Append);
			
			return m1;
		}

		public static Matrix Skewing(float skewX, float skewY)
		{
			double angleAlpha = skewX * Math.PI / 180F;
			double angleBeta = skewY * Math.PI / 180F;
			
			float tanAlpha = (float)Math.Tan(angleAlpha);
			float tanBeta = (float)Math.Tan(angleBeta);
			
			return new Matrix(1F, tanBeta, tanAlpha, 1F, 0F, 0F);
		}

		public static Matrix SkewingAt(float skewX, float skewY, Point center)
		{
			return SkewingAt(skewX, skewY, center.X, center.Y);
		}

		public static Matrix SkewingAt(float skewX, float skewY, float centerX, float centerY)
		{
			double angleAlpha = skewX * Math.PI / 180F;
			double angleBeta = skewY * Math.PI / 180F;
			
			float tanAlpha = (float)Math.Tan(angleAlpha);
			float tanBeta = (float)Math.Tan(angleBeta);
			
			Matrix m1 = new Matrix(1F, 0F, 0F, 1F, -centerX, -centerY);
			Matrix m2 = new Matrix(1F, tanBeta, tanAlpha, 1F, 0F, 0F);
			Matrix m3 = new Matrix(1F, 0F, 0F, 1F, centerX, centerY);
			
			m1.Multiply(m2, MatrixOrder.Append);
			m1.Multiply(m3, MatrixOrder.Append);
			
			return m1;
		}

		public Android.Graphics.Matrix ToNative()
		{
			Android.Graphics.Matrix m = new Android.Graphics.Matrix();

			ToNative(m);

			return m;
		}

		public void ToNative(Android.Graphics.Matrix m)
		{
			if (_values == null)
				_values = new float[9];

			_values[Android.Graphics.Matrix.MscaleX] = _m11;
			_values[Android.Graphics.Matrix.MskewX] = _m21;
			_values[Android.Graphics.Matrix.MtransX] = _offsetX;
			_values[Android.Graphics.Matrix.MskewY] = _m12;
			_values[Android.Graphics.Matrix.MscaleY] = _m22;
			_values[Android.Graphics.Matrix.MtransY] = _offsetY;
			_values[Android.Graphics.Matrix.Mpersp2] = 1F;

			m.SetValues(_values);
		}
	}
}