﻿using UnityEngine;
using System.Collections.Generic;
using System.IO.Compression;
using System.IO;


public class SppChildInfo
{
	public int vertexCount = 0;
	public int triangleCount = 0;
	public int meshCount = 0;
	// drawCalls
}

public static class SppBinaryReader
{
	public static GameObject curGameObject = null;
	public static GameObject rootGameObject = null;
	public static int totalVerts = 0;
	public static int totalTris = 0;
	public static int totalMeshes = 0;
	public static Dictionary<string, SppChildInfo> statistics = new Dictionary<string, SppChildInfo> ();
	private static byte[] buffer;
	// byte array containing data in .3d format
	private static System.Text.UTF8Encoding enc;
	private static char[] magic;
	public static int fileVersion;
	private static System.IO.BinaryReader breader;
	private static System.IO.MemoryStream memoryStream;
	public const int maxFileVersion = 2;
	private static Quaternion addRotation;
	private static GameObject sceneRoot;
	private static int offset = 0;

	public static float worldScale = 0.01f;

	public static byte[] GetBuffer ()
	{
		return buffer;
	}

	public static void SetBuffer (byte[] newBuffer)
	{
		buffer = new byte[0];

		buffer = newBuffer;
	}

	public static void ClearBuffer ()
	{
		buffer = new byte[0];
		System.GC.Collect ();
	}

	// Open a .3d file
	public static bool Load3D (string fileName)
	{
#if !UNITY_WEBPLAYER
		int delta = System.Environment.TickCount;

		if (System.IO.File.Exists (fileName)) {
			try {
				buffer = System.IO.File.ReadAllBytes (fileName);
				delta = System.Environment.TickCount - delta;
				Log (delta + " ms to read file " + fileName);

				return ValidateBuffer ();
			} catch (System.Exception e) {
				Log (e.Message);
				return false;
			}
		} else {
			return false;
		}
#else
		return false;
#endif
	}

	// Open a .gz files and uncompress the content
	public static bool LoadGZ (string fileName)
	{
#if !UNITY_WEBPLAYER
		int delta = System.Environment.TickCount;
		if (System.IO.File.Exists (fileName)) {
			try {
				byte[] gzBuffer = System.IO.File.ReadAllBytes (fileName);
				if (gzBuffer [0] == 0x1f && gzBuffer [1] == 0x8b) {
					buffer = Decompress (gzBuffer);

					delta = System.Environment.TickCount - delta;
					Log (delta + " ms to read file " + fileName);

					return ValidateBuffer ();
				} else {
					return false;
				}	
			} catch (System.Exception e) {
				Log (e.Message);
				return false;
			}
		} else {
			return false;
		}
#else
		return false;
#endif
	}

	public static bool Save3D (string fileName)
	{
#if !UNITY_WEBPLAYER
		if (buffer.Length > 0) {
			try {
				System.IO.File.WriteAllBytes (fileName, buffer);
				return true;
			} catch (System.Exception e) {
				Log (e.Message);
				return false;
			}
		} else
			return false;
#else
		return false;
#endif
	}


	public static bool SaveGZ (string fileName)
	{
#if !UNITY_WEBPLAYER
		if (buffer.Length > 0) {
			try {
				byte[] gzBuffer = Compress (buffer);
				System.IO.File.WriteAllBytes (fileName, gzBuffer);
				return true;
			} catch (System.Exception e) {
				Log (e.Message);
				return false;
			}
		} else
			return false;
#else
		return false;
#endif
	}

	public static bool SetBufferFromGZ (byte[] gzBuffer)
	{
		try {
			if (gzBuffer [0] == 0x1f && gzBuffer [1] == 0x8b) {
				buffer = Decompress (gzBuffer);
				return ValidateBuffer ();
			} else {
				return false;
			}	
		} catch (System.Exception e) {
			Log (e.Message);
			return false;
		}
	}

	public static bool BufferToScene (Quaternion additionalRotation)
	{
		try {
            SppMaterial.Init();
		} catch (System.Exception e) {
			Debug.LogError (e);
		}	
		offset = 0;
		addRotation = additionalRotation;
		totalVerts = 0;
		totalTris = 0;
		totalMeshes = 0;
		statistics.Clear (); 
		if (rootGameObject != null)
			GameObject.DestroyImmediate (rootGameObject);

		curGameObject = null;
		rootGameObject = null;

		enc = new System.Text.UTF8Encoding ();
		memoryStream = new System.IO.MemoryStream (buffer);
		breader = new System.IO.BinaryReader (memoryStream);
		// MAGIC BYTES
		magic = breader.ReadChars (3); 
		if (TestMagicBytes () == false) {
			Debug.LogError ("This is not a valid binary spp format");
			return false;
		}	

		offset += 3;
		fileVersion = breader.ReadInt32 ();
		offset += 4;
		while (breader.PeekChar () != 0) {
			try {
				char tag = breader.ReadChar ();
				offset++;
				if (tag == 't') {
					ReadTransform (breader);
				} else if (tag == 'm') {
					ReadMesh (breader);
				} else if (tag == 'r') {
					CloneMeshes (breader);
				} else {
					Log ("Unexpected tag (BufferToScene): " + tag);
					break;
				}
			} catch {
				break;
			}
		}
        breader.Dispose ();
		memoryStream.Dispose();
		return true;
	}

	public static void BufferToSceneBegin (Quaternion additionalRotation/*, GameObject root*/)
	{
        SppMaterial.Init();
		addRotation = additionalRotation;
		totalVerts = 0;
		totalTris = 0;
		totalMeshes = 0;
		statistics.Clear (); 

		if (rootGameObject != null)
			GameObject.DestroyImmediate (rootGameObject);

		curGameObject = null;
		rootGameObject = null;

		enc = new System.Text.UTF8Encoding ();
		memoryStream = new System.IO.MemoryStream (buffer);
		breader = new System.IO.BinaryReader (memoryStream);

		offset = 0;

		// MAGIC BYTES
		magic = breader.ReadChars (3); 
		if (TestMagicBytes () == false) {
			Debug.LogError ("This is not a valid binary spp format");
			return;
		}	

		offset += 3;
		fileVersion = breader.ReadInt32 ();
		offset += 4;
	}

	public static void BufferToSceneEnd ()
	{
        breader.Dispose ();
		memoryStream.Dispose ();
        addRotation = Quaternion.identity;
	}

	public static bool BufferToSceneIterate (out char currentTag)
	{
        SppMaterial.Init();
		currentTag = '.';
		try {
			if (breader.PeekChar () != 0) {
				try {
					char tag = breader.ReadChar ();
					offset++;
					currentTag = tag;
					if (tag == 't') {
						ReadTransform (breader);
					} else if (tag == 'm') {
						ReadMesh (breader);
					} else if (tag == 'r') {
						CloneMeshes (breader);
					} else {
						Log ("Unexpected tag (BufferToSceneIterate): " + tag);
						return false; //break;
					}
				} catch {
					return false;
				}
				return true;
			} else {
				return false;
			}
		} catch (System.Exception e) {
			Debug.Log (e.Message);
			return false;
		}
	}

	// validate buffer - via version and magic bytes
	private static bool ValidateBuffer ()
	{
		try {
			if (buffer [0] == 'S' && buffer [1] == 'P' && buffer [2] == 'P' && buffer [3] >= 0 && buffer [3] <= maxFileVersion && buffer [4] == 0 && buffer [5] == 0 && buffer [6] == 0)
				return true;
			else
				return false;
		} catch {
			return false;
		}	
	}

	private static void ReadTransform (System.IO.BinaryReader breader)
	{
		string p = "";
		string c = "";
		byte[] sBytes;

		int l = breader.ReadInt32 ();
		offset += 4;
		if (l > 0) {
			sBytes = breader.ReadBytes (l);
			p = enc.GetString (sBytes);
			offset += l;
		}	
		l = breader.ReadInt32 ();
		offset += 4;
		if (l > 0) {
			sBytes = breader.ReadBytes (l);
			c = enc.GetString (sBytes);
			offset += l;
		}	
		Vector3 pos = new Vector3 (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
		offset += (3 * 4);
		Vector4 rot = new Vector4 (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
		offset += (4 * 4);
		Vector3 scale = new Vector3 (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
		offset += (3 * 4);
		curGameObject = new GameObject ();
		curGameObject.name = c;

		curGameObject.transform.localScale = scale;
		if (p.Length > 0) {
			try {
				// check whether we already have an statistic entry
				curGameObject.transform.parent = GameObject.Find (p).transform;
			} catch {
				curGameObject.transform.parent = null;
			}
			curGameObject.transform.localPosition = pos;
			curGameObject.transform.localRotation = ConvertRotation (rot.x, rot.y, rot.z);
		} else {
			curGameObject.transform.position = pos;
			curGameObject.transform.rotation = addRotation * ConvertRotation (rot.x, rot.y, rot.z);
		}
		if (rootGameObject == null)
			rootGameObject = curGameObject;
	}

#pragma warning disable 219
	private static void CloneMeshes (System.IO.BinaryReader breader)
	{
		string p = "";
		string c = "";
		byte[] sBytes;

		int l = breader.ReadInt32 ();
		offset += 4;
		if (l > 0) {
			sBytes = breader.ReadBytes (l);
			p = enc.GetString (sBytes);
			offset += l;
		}	
		l = breader.ReadInt32 ();
		offset += 4;
		if (l > 0) {
			sBytes = breader.ReadBytes (l);
			c = enc.GetString (sBytes);
			offset += l;
		}	
		GameObject reference = GameObject.Find ("/" + p + "/" + c);
		if (reference != null) {
			string pKeyOrg = "";
			string pKey = "";
			Transform t = reference.transform;
			foreach (Transform child in t) {
				GameObject childObject = child.gameObject;
				GameObject referenceChild = new GameObject ();
				referenceChild.name = childObject.name;
				referenceChild.transform.parent = curGameObject.transform;
				referenceChild.transform.localRotation = Quaternion.identity;
				referenceChild.transform.localPosition = Vector3.zero;

				// clone meshes
				// mesh stuff

				MeshFilter mf = childObject.GetComponent<MeshFilter> () as MeshFilter;
				if (mf) {
					MeshFilter newMF = referenceChild.AddComponent<MeshFilter> () as MeshFilter;
					if (Application.isPlaying) {
						newMF.mesh = mf.mesh;
					} else {
						newMF.sharedMesh = mf.sharedMesh;
					}	
				}
				MeshRenderer mr = childObject.GetComponent<MeshRenderer> () as MeshRenderer;
				if (mr) {
					// BEGIN clone Material
					MeshRenderer newMR = referenceChild.AddComponent<MeshRenderer> () as MeshRenderer;
                    newMR.sharedMaterial = mr.sharedMaterial;
				}
				MeshCollider mc = childObject.GetComponent<MeshCollider> () as MeshCollider;
				if (mc) {
					MeshCollider newMC = referenceChild.AddComponent<MeshCollider> () as MeshCollider;
				}				
			}
			// statistics
			pKey = curGameObject.name;
			pKeyOrg = reference.name; 

			SppChildInfo cloneInfo = statistics [pKeyOrg];
			statistics.Add (pKey, cloneInfo);
			totalMeshes += cloneInfo.meshCount;
			totalVerts += cloneInfo.vertexCount;
			totalTris += cloneInfo.triangleCount;

		} else {
			Log ("Reference not found!");
		}	
	}

	private static void ReadMesh (System.IO.BinaryReader breader)
	{
		// read header
		int vertCount = breader.ReadInt32 ();
		offset += 4;
		int triCount = breader.ReadInt32 ();
		offset += 4;
		int format = breader.ReadInt32 ();
		offset += 4;
		string shaderName = "";

		int l;
		byte[] sBytes;
		totalMeshes++;
		totalVerts += vertCount;
		totalTris += triCount;


		string pKey = curGameObject.transform.parent.gameObject.name;
		SppChildInfo ci;
		if (statistics.ContainsKey (pKey)) {
			ci = statistics [pKey];
			ci.meshCount = ci.meshCount + 1;
			ci.triangleCount = ci.triangleCount + triCount;
			ci.vertexCount = ci.vertexCount + vertCount;
		} else {
			ci = new SppChildInfo ();
			ci.meshCount = ci.meshCount + 1;
			ci.triangleCount = ci.triangleCount + triCount;
			ci.vertexCount = ci.vertexCount + vertCount;
			statistics.Add (pKey, ci);
		}

		// sanity check
		if (vertCount < 0 || vertCount > 65535) {
			Log ("Invalid vertex count in the mesh data! " + vertCount);
		}
		if (triCount < 0 || triCount > 65535) {
			Log ("Invalid triangle count in the mesh data! " + triCount);
		}
		if (format < 1 || (format & 1) == 0 || format > 15) {
			Log ("Invalid vertex format in the mesh data!");
		}

		Mesh mesh = new Mesh ();
        mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;

        int i;

		// positions
		Vector3[] verts = new Vector3[vertCount];
		float swp;
		for (i = 0; i < vertCount; ++i) {
			verts [i] = new Vector3 (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
			offset += (3 * 4);
			if (fileVersion == 0) {
				verts [i].x = -verts [i].x;
				swp = verts [i].y;
				verts [i].y = verts [i].z;
				verts [i].z = swp;
			}	
		}
		mesh.vertices = verts;

		if ((format & 2) == 2) { // have normals 
			Vector3[] normals = new Vector3[vertCount];
			for (i = 0; i < vertCount; ++i) {
				normals [i] = new Vector3 (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
				offset += (3 * 4);
			}
			mesh.normals = normals;
		}

		if ((format & 4) == 4) { // have tangents
			Vector4[] tangents = new Vector4[vertCount];
			for (i = 0; i < vertCount; ++i) {
				tangents [i] = new Vector4 (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
				offset += (4 * 4);
			}
			mesh.tangents = tangents;
		}

		if ((format & 8) == 8) { // have UVs
			Vector2[] uvs = new Vector2[vertCount];
			for (i = 0; i < vertCount; ++i) {
				uvs [i] = new Vector2 (breader.ReadSingle (), breader.ReadSingle ());
				offset += (2 * 4);
			}
			mesh.uv = uvs;
		}

		// triangle indices
		int[] tris = new int[triCount * 3];
		for (i = 0; i < triCount; ++i) {
			tris [i * 3 + 0] = breader.ReadInt32 ();
			tris [i * 3 + 1] = breader.ReadInt32 ();
			tris [i * 3 + 2] = breader.ReadInt32 ();
			offset += (3 * 4);
		}
		mesh.triangles = tris;
		Color c1, c2;
		float shiny = 1f;

		if (fileVersion >= 2) {
			l = breader.ReadInt32 ();
			offset += 4;
			if (l > 0) {
				sBytes = breader.ReadBytes (l);
				shaderName = enc.GetString (sBytes);
				offset += l;
			} else {
				shaderName = "";
			}	
			c1 = new Color (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
			offset += (4 * 4);
			c2 = new Color (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
			offset += (4 * 4);
			shiny = breader.ReadSingle ();
		} else {	
			c1 = new Color (breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle (), breader.ReadSingle ());
			offset += (4 * 4);
			c2 = new Color (0.75f, 0.75f, 0.75f, 1f); //c1;
		}	
		MeshFilter mf = curGameObject.AddComponent<MeshFilter> ();
        if ((format & 2) != 2)
        { // no normals 
            //mesh.RecalculateNormals();
            NormalSolver.FastNormals(mesh);
        }    
		mf.mesh = mesh;
		MeshRenderer mr = curGameObject.AddComponent<MeshRenderer> ();
        string materialKey;
        if (c1.a < 1)
        {
            materialKey = "T" + ColorUtility.ToHtmlStringRGBA(c1);
        }
        else
        {
            materialKey = "O" + ColorUtility.ToHtmlStringRGB(c1);
        }

        mr.sharedMaterial = SppMaterial.GetMaterial(materialKey, c1);

        curGameObject.transform.localRotation = Quaternion.identity;
		curGameObject.transform.localPosition = Vector3.zero;

		// work around
		MeshCollider mc = curGameObject.AddComponent<MeshCollider> ();
		mc.sharedMesh = mesh;
	}

	// keep current parent/child names

	public static Quaternion ConvertRotation (float x, float y, float z)
	{
		Quaternion qx = Quaternion.AngleAxis (x, Vector3.right);
		Quaternion qy = Quaternion.AngleAxis (y, Vector3.up);
		Quaternion qz = Quaternion.AngleAxis (z, Vector3.forward);
		// XZY		
		return (qx * qz * qy); // this is the order
	}

	private static void Log (string msg)
	{
#if UNITY
		Debug.Log(msg);
#else
		// NOP
#endif
	}

	public static byte[] Decompress (byte[] gzip)
	{
		byte[] baRetVal = null;
		using (System.IO.MemoryStream ByteStream = new System.IO.MemoryStream (gzip)) {

			// Create a GZIP stream with decompression mode.
			// ... Then create a buffer and write into while reading from the GZIP stream.
			using (System.IO.Compression.GZipStream stream = new System.IO.Compression.GZipStream (ByteStream, System.IO.Compression.CompressionMode.Decompress)) {
				const int size = 4096;
				byte[] buffer = new byte[size];
				using (System.IO.MemoryStream memory = new System.IO.MemoryStream ()) {
					int count = 0;
					count = stream.Read (buffer, 0, size);
					while (count > 0) {
						memory.Write (buffer, 0, count);
						memory.Flush ();
						count = stream.Read (buffer, 0, size);
					}

					baRetVal = memory.ToArray ();
                    memory.Dispose ();
                }
                stream.Dispose ();
            } // End Using System.IO.Compression.GZipStream stream 
            ByteStream.Dispose ();
        } // End Using System.IO.MemoryStream ByteStream

        return baRetVal;
	}
	// End Sub Decompress

	public static byte[] Compress (byte[] raw)
	{
		using (MemoryStream memory = new MemoryStream ()) {
			using (GZipStream gzip = new GZipStream (memory, CompressionMode.Compress, true)) {
				gzip.Write (raw, 0, raw.Length);
			}
			return memory.ToArray ();
		}
	}

	private static bool TestMagicBytes() {
		if (magic.Length == 3) {
			if (magic[0] == 'S' && magic[1] == 'P' && magic[2] == 'P') {
				return true;
			}	
		} 
		return false;
	}	
}
