/*
 * +================================================================================+
 *      __  ___  ______________  __  __  ______________   ____
 *     / / / / |/ /  _/_  __/\ \/ / / / / /_  __/  _/ /  / __/
 *    / /_/ /    // /  / /    \  / / /_/ / / / _/ // /___\ \
 *    \____/_/|_/___/ /_/     /_/  \____/ /_/ /___/____/___/
 *
 * +================================================================================+
 *
 *    "THE BEER-WARE LICENSE"
 *    -----------------------
 *
 *    As long as you retain this notice you can do whatever you want with this
 *    stuff. However it is expressly forbidden to sell or commercially distribute
 *    Unity Utils or parts of it apart from your games.
 *    If you meet any of the authors some day, and you think this stuff is
 *    worth it, you can buy them a beer in return.
 *
 *    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *    THE SOFTWARE.
 *
 * +================================================================================+
 *
 *    Authors:
 *    David Krummenacher (dave@timbookedtwo.com)
 *
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;

namespace Utils.Metrics {

	/// <summary>
	/// Software metrics behaviour. Add the provided prefab to the scene that is first loaded to
	/// ensure the metrics are taken correctly.
	/// </summary>
	public class Metrics : MonoBehaviour {

		/// <summary>
		/// The instance for the singleton implementation.
		/// </summary>
		private static Metrics instance;

		/// <summary>
		/// Gets the instance.
		/// </summary>
		/// <value>The instance.</value>
		public static Metrics Instance {
			get {
				if (instance == null) {
					instance = GameObject.FindObjectOfType<Metrics>();
					if (instance == null) instance = new GameObject("Metrics", typeof(Metrics)).GetComponent<Metrics>();
					DontDestroyOnLoad(instance.gameObject);
				}
				return instance;
			}
		}

		/// <summary>
		/// The output stream.
		/// </summary>
		private StreamWriter stream;

		/// <summary>
		/// The session start time.
		/// </summary>
		public DateTime SessionStartTime { get; private set; }
		/// <summary>
		/// The session end time.
		/// </summary>
		/// <value>The session end time.</value>
		public DateTime SessionEndTime { get; private set; }

		[Tooltip("")]
		/// <summary>
		/// Wheter to log frames per sescond or not.
		/// </summary>
		public bool LogFPS = true;
		[Tooltip("")]
		/// <summary>
		/// The value the FPS needs to be under to log a message.
		/// </summary>
		public int LogIfFPSUnder = 30;
		[Tooltip("")]
		/// <summary>
		/// The FPS intervall. How many times per second should the FPS be updated.
		/// </summary>
		public float FPSUpdateIntervall = 0.5f;
		/// <summary>
		/// The frames per second.
		/// </summary>
		/// <value>The frames per second.</value>
		public int FPS { get; private set; }
		/// <summary>
		/// The time until FPS update.
		/// </summary>
		private float timeUntilFPSUpdate;
		/// <summary>
		/// The last frame count used for FPS calculation.
		/// </summary>
		private int lastFrameCount;
		/// <summary>
		/// The last time used for FPS calculation.
		/// </summary>
		private float lastTime;

		void Awake() {
			// Singleton implementation
			if (instance == null) {
				instance = this;
				DontDestroyOnLoad(this);
			} else {
				if (this != instance) Destroy(this.gameObject);
				return;
			}

			// Init session once to prevent error when someone calls EndSession() before StartSession()
			SessionStartTime = DateTime.Now;

			// Open stream and append to file
			stream = new StreamWriter(Application.persistentDataPath + "/Metrics.csv", true);

			// Write startup data
			WriteData(new Data("Application started"));

			// Listen to application logs
			Application.logMessageReceived += HandleLogMessages;
		}

		void Start() {
			// FPS stuff
			timeUntilFPSUpdate = 0f;
			lastFrameCount = Time.frameCount;
			lastTime = Time.realtimeSinceStartup;
		}

		void Update() {
			// FPS stuff
			timeUntilFPSUpdate -= Time.deltaTime;

			if (timeUntilFPSUpdate <= 0f) {
				timeUntilFPSUpdate = FPSUpdateIntervall;

				// FPS calculation
				FPS = Mathf.RoundToInt((Time.frameCount - lastFrameCount) / (Time.realtimeSinceStartup - lastTime));
				lastFrameCount = Time.frameCount;
				lastTime = Time.realtimeSinceStartup;

				// Log FPS
				if (LogFPS) {
					// FPS will be 0 on startup
					if (Time.realtimeSinceStartup >= 1f + FPSUpdateIntervall) {
						if (FPS < LogIfFPSUnder) {
							WriteData(new Data("FPS Value is under " + LogIfFPSUnder.ToString(), "FPS: " + FPS));
						}
					}
				}
			}
		}

		void OnDestroy() {
			// Singleton implementation
			if (this != instance) return;

			// Write end data
			WriteData(new Data("Application quit"));
			stream.Close();
		}

		/// <summary>
		/// Handles the log messages.
		/// Only cares about exceptions and errors.
		/// </summary>
		/// <param name="condition">Condition.</param>
		/// <param name="stackTrace">Stack trace.</param>
		/// <param name="logType">Log type.</param>
		private void HandleLogMessages(string condition, string stackTrace, LogType logType) {
			if (logType == LogType.Exception) WriteData(new Data("Exception occured!", new string[] { condition, stackTrace }));
			if (logType == LogType.Error) WriteData(new Data("Error occured!", new string[] { condition, stackTrace }));
		}

		/// <summary>
		/// Write the specified data to the file.
		/// </summary>
		/// <param name="data">The data.</param>
		private void WriteData(Data data) {
			stream.Write(data.ToCSVRow());
		}

		/// <summary>
		/// Write the specified message and values to the metrics file.
		/// </summary>
		/// <param name="message">The message.</param>
		/// <param name="values">The values.</param>
		public void Write(string message, params string[] values) {
			WriteData(new Data(message, values));
		}

		/// <summary>
		/// Starts a session.
		/// </summary>
		public void StartSession() {
			SessionStartTime = DateTime.Now;
			WriteData(new Data("Session started"));
		}

		/// <summary>
		/// Ends the current session.
		/// </summary>
		public void EndSession() {
			SessionEndTime = DateTime.Now;
			TimeSpan timeSpan = SessionEndTime - SessionStartTime;
			WriteData(new Data("Session ended", new string[] { "Duration: " + timeSpan.ToString() }));
		}

		/// <summary>
		/// Metrics data container class.
		/// </summary>
		private class Data {

			/// <summary>
			/// The message time.
			/// </summary>
			public DateTime MessageTime;
			/// <summary>
			/// The message.
			/// </summary>
			public string Message;
			/// <summary>
			/// The additional values.
			/// </summary>
			public string[] Values;

			/// <summary>
			/// Initializes a new instance of the <see cref="Utils.Metrics.Metrics+Data"/> class.
			/// </summary>
			/// <param name="time">Time.</param>
			/// <param name="message">Message.</param>
			/// <param name="values">Values.</param>
			public Data(DateTime time, string message, params string[] values) {
				MessageTime = time;
				Message = message;
				Values = values;
			}

			/// <summary>
			/// Initializes a new instance of the <see cref="Utils.Metrics.Metrics+Data"/> class.
			/// </summary>
			/// <param name="message">Message.</param>
			/// <param name="values">Values.</param>
			public Data(string message, params string[] values) : this(DateTime.Now, message, values) { }

			/// <summary>
			/// Converts this data object to a CSV row string.
			/// </summary>
			/// <returns>The CSV row.</returns>
			public string ToCSVRow() {
				string output = ToCSVCell(
					MessageTime.Year.ToString() + "-" +
					MessageTime.Month.ToString() + "-" +
					MessageTime.Day.ToString()
					);
				
				output += "," + ToCSVCell(
					MessageTime.Hour.ToString() + ":" +
					MessageTime.Minute.ToString() + ":" +
					MessageTime.Second.ToString() + "." +
					MessageTime.Millisecond.ToString()
					);
				
				output += "," + ToCSVCell(Message);
				
				foreach(string value in Values) {
					output += "," + ToCSVCell(value);
				}
				
				return output + "\n";
			}

			/// <summary>
			/// Converts a string to a CSV cell.
			/// </summary>
			/// <returns>The CSV cell.</returns>
			/// <param name="data">Data.</param>
			private static string ToCSVCell(string data) {
				bool containsInvalidCharacters = (data.Contains(",") || data.Contains("\"") || data.Contains("\r") || data.Contains("\n"));
				
				if (containsInvalidCharacters) {
					StringBuilder stringBuilder = new StringBuilder();
					stringBuilder.Append("\"");
					foreach (char character in data) {
						if (character == '"') stringBuilder.Append("\"");
						else stringBuilder.Append(character);
					}
					stringBuilder.Append("\"");
					return stringBuilder.ToString();
				}
				
				return data;
			}
		}
	}
}