#define PROTOTYPE
#if UNITY_EDITOR || UNITY_STANDALONE
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using ProBuilder2.Common;
using ProBuilder2.MeshOperations;

namespace ProBuilder2.Examples
{

	[RequireComponent(typeof(AudioSource))]
	public class IcoBumpin : MonoBehaviour
	{
		pb_Object ico;			// A reference to the icosphere pb_Object component
		Mesh icoMesh;			// A reference to the icosphere mesh (cached because we access the vertex array every frame)
		Transform icoTransform;	// A reference to the icosphere transform component.  Cached because I can't remember if GameObject.transform is still a performance drain :|
		AudioSource audioSource;// Cached reference to the audiosource.

		/**
		 * Holds a pb_Face, the normal of that face, and the index of every vertex that touches it (sharedIndices).
		 */
		struct FaceRef
		{
			public pb_Face face;
			public Vector3 nrm;		// face normal
			public int[] indices;	// all vertex indices (including shared connected vertices)

			public FaceRef(pb_Face f, Vector3 n, int[] i)
			{
				face = f;
				nrm = n;
				indices = i;
			}
		}

		// All faces that have been extruded
		FaceRef[] outsides;

		// Keep a copy of the original vertex array to calculate the distance from origin.
		Vector3[] original_vertices, displaced_vertices;

		// The radius of the mesh icosphere on instantiation.
		[Range(1f, 10f)]
		public float icoRadius = 2f;

		// The number of subdivisions to give the icosphere.
		[Range(0, 3)]
		public int icoSubdivisions = 2;

		// How far along the normal should each face be extruded when at idle (no audio input).
		[Range(0f, 1f)]
		public float startingExtrusion = .1f;

		// The material to apply to the icosphere.
		public Material material;

		// The max distance a frequency range will extrude a face.
		[Range(1f, 50f)]
		public float extrusion = 30f;

		// An FFT returns a spectrum including frequencies that are out of human hearing range -
		// this restricts the number of bins used from the spectrum to the lower @fftBounds.
		[Range(8, 128)]
		public int fftBounds = 32;

		// How high the icosphere transform will bounce (sample volume determines height).
		[Range(0f, 10f)]
		public float verticalBounce = 4f;

		// Optionally weights the frequency amplitude when calculating extrude distance.
		public AnimationCurve frequencyCurve;

		// A reference to the line renderer that will be used to render the raw waveform.
		public LineRenderer waveform;

		// The y size of the waveform.
		public float waveformHeight	= 2f;

		// How far from the icosphere should the waveform be.
		public float waveformRadius	= 20f;

		// If @rotateWaveformRing is true, this is the speed it will travel.
		public float waveformSpeed = .1f;

		// If true, the waveform ring will randomly orbit the icosphere.
		public bool rotateWaveformRing = false;

		// If true, the waveform will bounce up and down with the icosphere.
		public bool bounceWaveform = false;

		public GameObject missingClipWarning;

		// Icosphere's starting position.
		Vector3 icoPosition = Vector3.zero;
		float faces_length;

		const float TWOPI = 6.283185f;		// 2 * PI
		const int WAVEFORM_SAMPLES = 1024;	// How many samples make up the waveform ring.
		const int FFT_SAMPLES = 4096;		// How many samples are used in the FFT.  More means higher resolution.

		// Keep copy of the last frame's sample data to average with the current when calculating
		// deformation amounts.  Smoothes the visual effect.
		float[] fft = new float[FFT_SAMPLES],
				fft_history = new float[FFT_SAMPLES],
				data = new float[WAVEFORM_SAMPLES],
				data_history = new float[WAVEFORM_SAMPLES];

		// Root mean square of raw data (volume, but not in dB).
		float rms = 0f, rms_history = 0f;

		/**
		 * Creates the icosphere, and loads all the cache information.
		 */
		void Start()
		{
			audioSource = GetComponent<AudioSource>();

			if( audioSource.clip == null )
				missingClipWarning.SetActive(true);

			// Create a new icosphere.
			ico = pb_ShapeGenerator.IcosahedronGenerator(icoRadius, icoSubdivisions);

			// Shell is all the faces on the new icosphere.
			pb_Face[] shell = ico.faces;

			// Materials are set per-face on pb_Object meshes.  pb_Objects will automatically
			// condense the mesh to the smallest set of subMeshes possible based on materials.
#if !PROTOTYPE
			foreach(pb_Face f in shell)
				f.material = material;
#else
			ico.gameObject.GetComponent<MeshRenderer>().sharedMaterial = material;
#endif

			pb_Face[] connectingFaces;

			// Extrude all faces on the icosphere by a small amount.  The third boolean parameter
			// specifies that extrusion should treat each face as an individual, not try to group
			// all faces together.
			ico.Extrude(shell, startingExtrusion, false, out connectingFaces);

			// ToMesh builds the mesh positions, submesh, and triangle arrays.  Call after adding
			// or deleting vertices, or changing face properties.
			ico.ToMesh();

			// Refresh builds the normals, tangents, and UVs.
			ico.Refresh();

			outsides = new FaceRef[shell.Length];
			Dictionary<int, int> lookup = ico.sharedIndices.ToDictionary();

			// Populate the outsides[] cache.  This is a reference to the tops of each extruded column, including
			// copies of the sharedIndices.
			for(int i = 0; i < shell.Length; ++i)
				outsides[i] = new FaceRef( 	shell[i],
											pb_Math.Normal(ico, shell[i]),
											ico.sharedIndices.AllIndicesWithValues(lookup, shell[i].distinctIndices).ToArray()
											);

			// Store copy of positions array un-modified
			original_vertices = new Vector3[ico.vertices.Length];
			System.Array.Copy(ico.vertices, original_vertices, ico.vertices.Length);

			// displaced_vertices should mirror icosphere mesh vertices.
			displaced_vertices = ico.vertices;

			icoMesh = ico.msh;
			icoTransform = ico.transform;

			faces_length = (float)outsides.Length;

			// Build the waveform ring.
			icoPosition = icoTransform.position;
#if UNITY_4_5 || UNITY_4_6 || UNITY_4_7 || UNITY_5_0 || UNITY_5_1 || UNITY_5_2 || UNITY_5_3 || UNITY_5_4
			waveform.SetVertexCount(WAVEFORM_SAMPLES);
#else
			waveform.numPositions = WAVEFORM_SAMPLES;
#endif

			if( bounceWaveform )
				waveform.transform.parent = icoTransform;

			audioSource.Play();
		}

		void Update()
		{
			// fetch the fft spectrum
			audioSource.GetSpectrumData(fft, 0, FFTWindow.BlackmanHarris);

			// get raw data for waveform
			audioSource.GetOutputData(data, 0);

			// calculate root mean square (volume)
			rms = RMS(data);

			/**
			 * For each face, translate the vertices some distance depending on the frequency range assigned.
			 * Not using the TranslateVertices() pb_Object extension method because as a convenience, that method
			 * gathers the sharedIndices per-face on every call, which while not tremondously expensive in most
			 * contexts, is far too slow for use when dealing with audio, and especially so when the mesh is
			 * somewhat large.
			 */
			for(int i = 0; i < outsides.Length; i++)
			{
				float normalizedIndex = (i/faces_length);

				int n = (int)(normalizedIndex*fftBounds);

				Vector3 displacement = outsides[i].nrm * ( ((fft[n]+fft_history[n]) * .5f) * (frequencyCurve.Evaluate(normalizedIndex) * .5f + .5f)) * extrusion;

				foreach(int t in outsides[i].indices)
				{
					displaced_vertices[t] = original_vertices[t] + displacement;
				}
			}

			Vector3 vec = Vector3.zero;

			// Waveform ring
			for(int i = 0; i < WAVEFORM_SAMPLES; i++)
			{
				int n = i < WAVEFORM_SAMPLES-1 ? i : 0;
				vec.x = Mathf.Cos((float)n/WAVEFORM_SAMPLES * TWOPI) * (waveformRadius + (((data[n] + data_history[n]) * .5f) * waveformHeight));
				vec.z = Mathf.Sin((float)n/WAVEFORM_SAMPLES * TWOPI) * (waveformRadius + (((data[n] + data_history[n]) * .5f) * waveformHeight));

				vec.y = 0f;

				waveform.SetPosition(i, vec);
			}

			// Ring rotation
			if( rotateWaveformRing )
			{
				Vector3 rot = waveform.transform.localRotation.eulerAngles;

				rot.x = Mathf.PerlinNoise(Time.time * waveformSpeed, 0f) * 360f;
				rot.y = Mathf.PerlinNoise(0f, Time.time * waveformSpeed) * 360f;

				waveform.transform.localRotation = Quaternion.Euler(rot);
			}

			icoPosition.y = -verticalBounce + ((rms + rms_history) * verticalBounce);
			icoTransform.position = icoPosition;

			// Keep copy of last FFT samples so we can average with the current.  Smoothes the movement.
			System.Array.Copy(fft, fft_history, FFT_SAMPLES);
			System.Array.Copy(data, data_history, WAVEFORM_SAMPLES);
			rms_history = rms;

			icoMesh.vertices = displaced_vertices;
		}

		/**
		 * Root mean square is a good approximation of perceived loudness.
		 */
		float RMS(float[] arr)
		{
			float 	v = 0f,
					len = (float)arr.Length;

			for(int i = 0; i < len; i++)
				v += Mathf.Abs(arr[i]);

			return Mathf.Sqrt(v / (float)len);
		}
	}
}
#endif
