// Copyright 2014-2017 ClassicalSharp | Licensed under BSD-3
using System;
using ClassicalSharp.Entities;
using ClassicalSharp.GraphicsAPI;
using ClassicalSharp.Map;
using ClassicalSharp.Physics;
using OpenTK;

namespace ClassicalSharp.Model {
	
	/// <summary> Describes the type of skin that a humanoid model uses. </summary>
	public enum SkinType { Type64x32, Type64x64, Type64x64Slim, Invalid }
	
	/// <summary> Contains a set of quads and/or boxes that describe a 3D object as well as
	/// the bounding boxes that contain the entire set of quads and/or boxes. </summary>
	public abstract class IModel {
		protected Game game;
		protected const int quadVertices = 4;
		protected const int boxVertices = 6 * quadVertices;
		protected static RotateOrder Rotate = RotateOrder.ZYX;
		internal bool initalised;
		
		public const ushort UVMask   = 0x7FFF;
		public const ushort UVMaxBit = 0x8000;
		public const ushort UVMaxShift = 15;

		public IModel(Game game) { this.game = game; }
		
		public abstract void CreateParts();
		
		/// <summary> Whether the entity should be slightly bobbed up and down when rendering. </summary>
		/// <remarks> e.g. for players when their legs are at the peak of their swing,
		/// the whole model will be moved slightly down. </remarks>
		public bool Bobbing = true;

		/// <summary> Whether this model uses a skin texture. </summary>
		/// <remarks> If false, no attempt is made to download the skin of an entity which has this model. </remarks>
		public bool UsesSkin = true;
		
		/// <summary> Whether humanoid animations should be calculated, instead of normal animations. </summary>
		public bool CalcHumanAnims;
		
		/// <summary> Whether the model uses humanoid skin texture, instead of mob skin texture. </summary>
		public bool UsesHumanSkin;
		
		/// <summary> Amount player score increased by when they kill an entity with this model. </summary>
		public byte SurivalScore = 5;
		
		/// <summary> Whether this model pushes other models when collided with. </summary>
		public bool Pushes = true;
		
		
		/// <summary> Gravity applied to this entity. </summary>
		public float Gravity = 0.08f;
		
		/// <summary> Drag applied to the entity. </summary>
		public Vector3 Drag = new Vector3(0.91f, 0.98f, 0.91f);
		
		/// <summary> Friction applied to the entity when is on the ground. </summary>
		public Vector3 GroundFriction = new Vector3(0.6f, 1.0f, 0.6f);
		
		
		/// <summary> Vertical offset from the model's feet/base that the name texture should be drawn at. </summary>
		public abstract float NameYOffset { get; }
		
		/// <summary> Vertical offset from the model's feet/base that the model's eye is located. </summary>
		public abstract float GetEyeY(Entity entity);
		
		/// <summary> The maximum scale the entity can have (for collisions and rendering). </summary>
		public float MaxScale = 2.0f;
		
		/// <summary> Scaling factor applied, multiplied by the entity's current model scale. </summary>
		public float ShadowScale = 1.0f;
		
		/// <summary> Scaling factor applied, multiplied by the entity's current model scale. </summary>
		public float NameScale = 1.0f;
		
		
		/// <summary> The size of the bounding box that is used when
		/// performing collision detection for this model. </summary>
		public abstract Vector3 CollisionSize { get; }
		
		/// <summary> Bounding box that contains this model,
		/// assuming that the model is not rotated at all.</summary>
		public abstract AABB PickingBounds { get; }
		
		protected static SkinType skinType;
		protected static float cosHead, sinHead;
		protected static float uScale, vScale;
		protected static PackedCol[] cols = new PackedCol[Side.Sides];
		
		/// <summary> Returns whether the model should be rendered based on the given entity's position. </summary>
		public static bool ShouldRender(Entity p, FrustumCulling culling) {
			Vector3 pos = p.Position;
			AABB bb = p.PickingBounds;
			
			float maxLen = Math.Max(bb.Width, Math.Max(bb.Height, bb.Length));
			pos.Y += bb.Height / 2; // centre Y coordinate
			return culling.SphereInFrustum(pos.X, pos.Y, pos.Z, maxLen);
		}
		
		/// <summary> Returns the closest distance of the given entity to the camera. </summary>
		public static float RenderDistance(Entity p, Vector3 camPos) {
			Vector3 pos = p.Position;
			AABB bb = p.modelAABB;
			pos.Y += bb.Height / 2; // centre Y coordinate
			
			float dx = MinDist(camPos.X - pos.X, bb.Width / 2);
			float dy = MinDist(camPos.Y - pos.Y, bb.Height / 2);
			float dz = MinDist(camPos.Z - pos.Z, bb.Length / 2);
			return dx * dx + dy * dy + dz * dz;
		}
		
		static float MinDist(float dist, float extent) {
			// Compare min coord, centre coord, and max coord
			float dMin = Math.Abs(dist - extent), dMax = Math.Abs(dist + extent);
			return Math.Min(Math.Abs(dist), Math.Min(dMin, dMax));
		}
		
		/// <summary> Sets up the state for, then renders an entity model,
		/// based on the given entity's position and orientation. </summary>
		public void Render(Entity p) {
			Vector3 pos = p.Position;
			if (Bobbing) pos.Y += p.anim.bobbingModel;
			SetupState(p);
			game.Graphics.SetBatchFormat(VertexFormat.P3fT2fC4b);
			
			Matrix4 m = TransformMatrix(p, pos);
			p.transform = m;
			Matrix4.Mult(out m, ref p.transform, ref game.Graphics.View);
			
			game.Graphics.LoadMatrix(ref m);
			DrawModel(p);
			game.Graphics.LoadMatrix(ref game.Graphics.View);
		}
		
		public void SetupState(Entity p) {
			index = 0;
			PackedCol col = p.Colour();
			
			cols[0] = col;
			if (!p.NoShade) {
				cols[1] = PackedCol.Scale(col, PackedCol.ShadeYBottom);
				cols[2] = PackedCol.Scale(col, PackedCol.ShadeZ);
				cols[4] = PackedCol.Scale(col, PackedCol.ShadeX);
			} else {
				cols[1] = col; cols[2] = col; cols[4] = col;
			}
			cols[3] = cols[2]; cols[5] = cols[4];
			
			float yawDelta = p.HeadY - p.RotY;
			cosHead = (float)Math.Cos(yawDelta * Utils.Deg2Rad);
			sinHead = (float)Math.Sin(yawDelta * Utils.Deg2Rad);
		}
		
		/// <summary> Performs the actual rendering of an entity model. </summary>
		public abstract void DrawModel(Entity p);
		
		/// <summary> Sends the updated vertex data to the GPU. </summary>
		protected void UpdateVB() {
			ModelCache cache = game.ModelCache;
			game.Graphics.UpdateDynamicVb_IndexedTris(
				cache.vb, cache.vertices, index);
			index = 0;
		}
		
		/// <summary> Recalculates properties such as name Y offset, collision size. </summary>
		/// <remarks> This is not used by majority of models. (BlockModel is the exception). </remarks>
		public virtual void RecalcProperties(Entity p) { }
		
		
		protected internal virtual Matrix4 TransformMatrix(Entity p, Vector3 pos) {
			return p.TransformMatrix(p.ModelScale, pos);
		}
		
		public ModelVertex[] vertices;
		public int index, texIndex;
		protected byte armX = 6, armY = 12; // these translate arm model part back to (0, 0) origin
		
		public void ApplyTexture(Entity entity) {
			int tex = UsesHumanSkin ? entity.TextureId : entity.MobTextureId;
			if (tex != 0) {
				skinType = entity.SkinType;
			} else {
				tex = game.ModelCache.Textures[texIndex].TexID;
				skinType = game.ModelCache.Textures[texIndex].SkinType;
			}
			
			game.Graphics.BindTexture(tex);
			bool _64x64 = skinType != SkinType.Type64x32;
			uScale = entity.uScale * 0.015625f;
			vScale = entity.vScale * (_64x64 ? 0.015625f : 0.03125f);
		}
		
		
		protected BoxDesc MakeBoxBounds(int x1, int y1, int z1, int x2, int y2, int z2) {
			return ModelBuilder.MakeBoxBounds(x1, y1, z1, x2, y2, z2);
		}
		
		protected BoxDesc MakeRotatedBoxBounds(int x1, int y1, int z1, int x2, int y2, int z2) {
			return ModelBuilder.MakeRotatedBoxBounds(x1, y1, z1, x2, y2, z2);
		}
		
		protected ModelPart BuildBox(BoxDesc desc) {
			return ModelBuilder.BuildBox(this, desc);
		}
		
		protected ModelPart BuildRotatedBox(BoxDesc desc) {
			return ModelBuilder.BuildRotatedBox(this, desc);
		}
		
		protected void DrawPart(ModelPart part) {
			VertexP3fT2fC4b vertex = default(VertexP3fT2fC4b);
			VertexP3fT2fC4b[] finVertices = game.ModelCache.vertices;
			
			for (int i = 0; i < part.Count; i++) {
				ModelVertex v = vertices[part.Offset + i];
				vertex.X = v.X; vertex.Y = v.Y; vertex.Z = v.Z;
				vertex.Col = cols[i >> 2];
				
				vertex.U = (v.U & UVMask) * uScale - (v.U >> UVMaxShift) * 0.01f * uScale;
				vertex.V = (v.V & UVMask) * vScale - (v.V >> UVMaxShift) * 0.01f * vScale;
				finVertices[index++] = vertex;
			}
		}
		
		protected void DrawRotate(float angleX, float angleY, float angleZ, ModelPart part, bool head) {
			float cosX = (float)Math.Cos(-angleX), sinX = (float)Math.Sin(-angleX);
			float cosY = (float)Math.Cos(-angleY), sinY = (float)Math.Sin(-angleY);
			float cosZ = (float)Math.Cos(-angleZ), sinZ = (float)Math.Sin(-angleZ);
			float x = part.RotX, y = part.RotY, z = part.RotZ;
			VertexP3fT2fC4b vertex = default(VertexP3fT2fC4b);
			VertexP3fT2fC4b[] finVertices = game.ModelCache.vertices;
			
			for (int i = 0; i < part.Count; i++) {
				ModelVertex v = vertices[part.Offset + i];
				v.X -= x; v.Y -= y; v.Z -= z;
				float t = 0;
				
				// Rotate locally
				if (Rotate == RotateOrder.ZYX) {
					t = cosZ * v.X + sinZ * v.Y; v.Y = -sinZ * v.X + cosZ * v.Y; v.X = t; // Inlined RotZ
					t = cosY * v.X - sinY * v.Z; v.Z  = sinY * v.X + cosY * v.Z; v.X = t; // Inlined RotY
					t = cosX * v.Y + sinX * v.Z; v.Z = -sinX * v.Y + cosX * v.Z; v.Y = t; // Inlined RotX
				} else if (Rotate == RotateOrder.XZY) {
					t = cosX * v.Y + sinX * v.Z; v.Z = -sinX * v.Y + cosX * v.Z; v.Y = t; // Inlined RotX
					t = cosZ * v.X + sinZ * v.Y; v.Y = -sinZ * v.X + cosZ * v.Y; v.X = t; // Inlined RotZ
					t = cosY * v.X - sinY * v.Z; v.Z =  sinY * v.X + cosY * v.Z; v.X = t; // Inlined RotY
				} else if (Rotate == RotateOrder.YZX) {
					t = cosY * v.X - sinY * v.Z; v.Z =  sinY * v.X + cosY * v.Z; v.X = t; // Inlined RotY
					t = cosZ * v.X + sinZ * v.Y; v.Y = -sinZ * v.X + cosZ * v.Y; v.X = t; // Inlined RotZ
					t = cosX * v.Y + sinX * v.Z; v.Z = -sinX * v.Y + cosX * v.Z; v.Y = t; // Inlined RotX
				}
				
				// Rotate globally
				if (head) {
					t = cosHead * v.X - sinHead * v.Z; v.Z = sinHead * v.X + cosHead * v.Z; v.X = t; // Inlined RotY
				}
				vertex.X = v.X + x; vertex.Y = v.Y + y; vertex.Z = v.Z + z;
				vertex.Col = cols[i >> 2];
				
				vertex.U = (v.U & UVMask) * uScale - (v.U >> UVMaxShift) * 0.01f * uScale;
				vertex.V = (v.V & UVMask) * vScale - (v.V >> UVMaxShift) * 0.01f * vScale;
				finVertices[index++] = vertex;
			}
		}
		
		protected enum RotateOrder { ZYX, XZY, YZX }
		
		public void RenderArm(Entity p) {
			Vector3 pos = p.Position;
			if (Bobbing) pos.Y += p.anim.bobbingModel;
			SetupState(p);
			
			game.Graphics.SetBatchFormat(VertexFormat.P3fT2fC4b);
			ApplyTexture(p);	
			Matrix4 translate;
			
			if (game.ClassicArmModel) {
				// TODO: Position's not quite right.
				// Matrix4.Translate(out m, -armX / 16f + 0.2f, -armY / 16f - 0.20f, 0);
				// is better, but that breaks the dig animation
				Matrix4.Translate(out translate, -armX / 16f,         -armY / 16f - 0.10f, 0);
			} else {
				Matrix4.Translate(out translate, -armX / 16f + 0.10f, -armY / 16f - 0.26f, 0);
			}

			Matrix4 m = p.TransformMatrix(p.ModelScale, pos);
			Matrix4.Mult(out m, ref m, ref game.Graphics.View);
			Matrix4.Mult(out m, ref translate, ref m);
			
			game.Graphics.LoadMatrix(ref m);
			Rotate = RotateOrder.YZX;
			DrawArm(p);
			Rotate = RotateOrder.ZYX;
			game.Graphics.LoadMatrix(ref game.Graphics.View);
		}
		
		protected void DrawArmPart(ModelPart part) {
			part.RotX = armX / 16.0f; part.RotY = (armY + armY / 2) / 16.0f;
			if (game.ClassicArmModel) {
				DrawRotate(0, -90 * Utils.Deg2Rad, 120 * Utils.Deg2Rad, part, false);
			} else {
				DrawRotate(-20 * Utils.Deg2Rad, -70 * Utils.Deg2Rad, 135 * Utils.Deg2Rad, part, false);
			}
		}
		
		public virtual void DrawArm(Entity p) { }	
	}
}