/****************************************************************************
 *
 * Copyright (c) 2018 CRI Middleware Co., Ltd.
 *
 ****************************************************************************/

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

namespace CriWare {

[Serializable]
public sealed class CriProfilerEditor : EditorWindow
{
	private static CriProfiler profiler = CriProfiler.GetSingleton();

	#region Unity Event Callbacks

	[MenuItem ("Window/CRIWARE/CRI Profiler")]
	static void InitWindow() {
		CriProfilerEditor window = (CriProfilerEditor)EditorWindow.GetWindow(typeof(CriProfilerEditor));
#if UNITY_5_2 || UNITY_5_3 || UNITY_5_4_OR_NEWER
		window.titleContent = new GUIContent("CRI Profiler");
#else
		window.title = "CRI Profiler";
#endif
		window.Show();
	}

	private struct PlaymodeState {
		public readonly bool isPlaying;
		public readonly bool isPaused;
		public readonly bool isPlayingOrWillChangePlaymode;
		public PlaymodeState(bool isPlaying, bool isPaused, bool isPlayingOrWillChangePlaymode) {
			this.isPlaying = isPlaying;
			this.isPaused = isPaused;
			this.isPlayingOrWillChangePlaymode = isPlayingOrWillChangePlaymode;
		}
	}
	private PlaymodeState prevPlaymodeState = new PlaymodeState(false, false, false);

	private void OnStateChanged() {
		if (EditorApplication.isPlayingOrWillChangePlaymode == false) {
			profiler.StopProfiling();
		} else if (startWithUnityPlayer == true && EditorApplication.isPlaying == true && prevPlaymodeState.isPlaying == false) {
			this.remoteProfiling = false;
			profiler.ipAddressString = DEFAULT_IP_ADDR;
			profiler.StartProfiling(saveLogFile, logFileDir);
		}
		if (this.pauseWithUnityPlayer == true) {
			this.isWindowPaused = EditorApplication.isPaused;
		}
		prevPlaymodeState = new PlaymodeState(EditorApplication.isPlaying, EditorApplication.isPaused, EditorApplication.isPlayingOrWillChangePlaymode);
	}

#if UNITY_2017_2_OR_NEWER
	private void OnPlayModeStateChanged(PlayModeStateChange state) {
		OnStateChanged();
	}
	private void OnPauseStateChanged(PauseState state) {
		OnStateChanged();
	}
#endif

	private void OnEnable() {
#if UNITY_2017_2_OR_NEWER
		UnityEditor.EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
		UnityEditor.EditorApplication.pauseStateChanged += OnPauseStateChanged;
#else
		UnityEditor.EditorApplication.playmodeStateChanged += OnStateChanged;
#endif
	}

	private void OnDisable() {
		if (profiler != null) {
			profiler.StopProfiling();
		}
		DestroyResources();

#if UNITY_2017_2_OR_NEWER
		UnityEditor.EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
		UnityEditor.EditorApplication.pauseStateChanged -= OnPauseStateChanged;
#else
		UnityEditor.EditorApplication.playmodeStateChanged -= OnStateChanged;
#endif
	}

	#endregion Unity Event Callbacks

	#region Data Slice Definition

	private struct DataSlicePerformance {
		public readonly Single cpuLoad;
		public readonly Single[] cpuLoadHistory;
		public readonly UInt32 maxVoices;
		public readonly UInt32 usedVoices;
		public readonly UInt32[] voiceUsageHistory;
		public readonly UInt32 maxStandardVoices;
		public readonly int usedStandardVoices;
		public readonly UInt32 maxHcaMxVoices;
		public readonly int usedHcaMxVoices;
		public readonly UInt32 usedStreams;
		public readonly Single totalBps;
		public DataSlicePerformance(CriProfiler profiler) {
			this.cpuLoad = profiler.cpu_load;
			this.cpuLoadHistory = profiler.CpuLoadHistory;
			this.maxVoices =
				profiler.maxVoice_stdStreaming +
				profiler.maxVoice_hcamxStreaming +
				profiler.maxVoice_stdOnMemory +
				profiler.maxVoice_hcamxOnMemory;
			this.usedVoices = profiler.num_used_voices;
			this.voiceUsageHistory = profiler.VoiceUsageHistory;
			this.maxStandardVoices = profiler.maxVoice_stdOnMemory + profiler.maxVoice_stdStreaming;
			this.usedStandardVoices = profiler.GetVoicePoolUsage(CriProfiler.VoicePoolFormat.Standard);
			this.maxHcaMxVoices = profiler.maxVoice_hcamxOnMemory + profiler.maxVoice_hcamxStreaming;
			this.usedHcaMxVoices = profiler.GetVoicePoolUsage(CriProfiler.VoicePoolFormat.HcaMx);
			this.usedStreams = profiler.num_used_streams;
			this.totalBps = profiler.total_bps;
		}
	}

	private struct DataSlicePlayback {
		public readonly CriProfiler.PlaybackInfo[] playbackList;
		public readonly StringBuilder playbackListString;
		public readonly string cuenameLastPlayed;
		public DataSlicePlayback(CriProfiler profiler) {
			this.playbackList = profiler.PlaybackList;
			this.playbackListString = new StringBuilder();
			for (int i = 0; i < playbackList.Length; ++i) {
				playbackListString.Append(playbackList[i].cueName + " ");
			}
			this.cuenameLastPlayed = string.IsNullOrEmpty(profiler.cuename_lastPlayed) ? "---" : profiler.cuename_lastPlayed + " (" + profiler.cuesheetName_lastPlayed + ")";
		}
	}
	private struct DataSliceLoudness {
		public readonly Single momentary;
		public readonly Single shortTerm;
		public readonly Single integrated;
		public DataSliceLoudness(CriProfiler profiler) {
			this.momentary = profiler.loudness_momentary;
			this.shortTerm = profiler.loudness_shortTerm;
			this.integrated = profiler.loudness_integrated;
		}
	}
	private struct DataSliceLevels {
		public readonly CriProfiler.LevelInfo[] levels;
		public readonly int outputCh;
		public DataSliceLevels(CriProfiler profiler) {
			this.levels = profiler.LevelsAllCh;
			this.outputCh = profiler.numChMasterOut;
		}
	}

	private struct DataSliceLocalRuntime {
		public readonly uint memUsageAtom;
		public readonly uint memUsageFs;
		public DataSliceLocalRuntime(bool isPlaying) {
			if (isPlaying == true) {
				memUsageAtom = CriAtomPlugin.IsLibraryInitialized() ? CriWare.Common.GetAtomMemoryUsage() : 0;
				memUsageFs = CriFsPlugin.IsLibraryInitialized() ? CriWare.Common.GetFsMemoryUsage() : 0;
			} else {
				memUsageAtom = 0;
				memUsageFs = 0;
			}
		}
	}

	#endregion Data Slice Definition

	#region Internal Fields

	/* configurations */
	private const Single DB_MIN = -96.0f;
	private const Single DB_MAX = 20.0f;
	private const Single DEFAULT_DB_FLOOR = -48.0f;
	private const Single DEFAULT_DB_CEILING = 0;
	private const Single DEFAULT_DB_TARGET = -24.0f;
	private const Single DB_RANGE_PLUSMINUS_MAX = 20.0f;
	private const Single DB_RANGE_PLUSMINUS_MIN = 0;
	private const Single DEFAULT_DB_RANGE_PLUSMINUS = 2.0f;
	private const Single DEFAULT_DB_WARNING = -6.0f;
	private const Single DEFAULT_LEVEL_FLOOR = -48.0f;
	private const Single DEFAULT_LEVEL_CEILING = 0;
	private const Single DEFAULT_LEVEL_WARNING = -6.0f;
	private const int DEFAULT_LEVEL_CAL_INTVL = 6;
	private const int DEFAULT_LEVEL_CAL_INTVL_MIN = 1;
	private const int DEFAULT_LEVEL_CAL_INTVL_MAX = 24;
	private const MultiChannelMeterType DEFAULT_LEVEL_TYPE = MultiChannelMeterType.Peak;
	private const string DEFAULT_IP_ADDR = "127.0.0.1";
	[SerializeField] private bool startWithUnityPlayer = true;
	[SerializeField] private bool pauseWithUnityPlayer = true;
	[SerializeField] private bool saveLogFile = false;
	[SerializeField] private string logFileDir = "";
	[SerializeField] private bool remoteProfiling = false;
	[SerializeField] private bool isWindowPaused = false;
	[SerializeField] private Single currentDbFloor = DEFAULT_DB_FLOOR;
	[SerializeField] private Single currentDbCeiling = DEFAULT_DB_CEILING;
	[SerializeField] private Single currentDbTarget = DEFAULT_DB_TARGET;
	[SerializeField] private Single currentDbRangePlusMinus = DEFAULT_DB_RANGE_PLUSMINUS;
	[SerializeField] private Single currentDbWarning = DEFAULT_DB_WARNING;
	[SerializeField] private Single currentLevelFloor = DEFAULT_LEVEL_FLOOR;
	[SerializeField] private Single currentLevelCeiling = DEFAULT_LEVEL_CEILING;
	[SerializeField] private Single currentLevelWarning = DEFAULT_LEVEL_WARNING;
	[SerializeField] private int currentLevelCalInterval = DEFAULT_LEVEL_CAL_INTVL;
	[SerializeField] private MultiChannelMeterType currentLevelType = DEFAULT_LEVEL_TYPE;

	/* GUI constants */
	private const float LABEL_WIDTH = 148.0f;
	private const float CHART_INDENT_WIDTH = 15.0f;
	private const float GUI_INDENT_WIDTH = 15.0f;
	private const int TEXT_SIZE_CALIBRATOR = 10;
	private const int TEXT_SIZE_LABEL = 11;
	private const int TEXT_SIZE_CUE_LIST = 12;
	private const int TEXT_SIZE_GIANT = 20;
	private const int LOG_ITEM_HEIGHT = 20;
	static private Vector2 textOffsetChartTitle = new Vector2(3.0f, 2.0f);
	static private Vector2 textOffsetCalibrator = new Vector2(-4.0f, -6.0f);
	static private Color colorWarningHigh = new Color(224 / 255.0f, 44 / 255.0f, 44 / 255.0f);
	static private Color colorWarningHighTolerant = new Color(244 / 255.0f, 170 / 255.0f, 44 / 255.0f);
	static private Color colorMidRange = new Color(107 / 255.0f, 224 / 255.0f, 44 / 255.0f);
	static private Color colorWarningLow = new Color(44 / 255.0f, 107 / 255.0f, 224 / 255.0f);
	static private Color colorGrid = new Color(0.2f, 0.2f, 0.2f);
	private const int CNT_CUSTOM_STYLES = 20;
	static private GUIStyle[] customStyles = new GUIStyle[CNT_CUSTOM_STYLES];
	static private Texture2D[] customStyleTex = new Texture2D[CNT_CUSTOM_STYLES];
	private GUIStyle barBorderStyle { get { return InitializeStyleWithBgColor(0, Color.gray); } }
	private GUIStyle barBackgroundStyle { get { return InitializeStyleWithBgColor(1, Color.black); } }
	private GUIStyle barStyle { get { return InitializeStyleWithBgColor(2, Color.gray); } }
	private GUIStyle barWarningHighStyle { get { return InitializeStyleWithBgColor(3, colorWarningHigh); } }
	private GUIStyle barWarningHighTolerantStyle { get { return InitializeStyleWithBgColor(4, colorWarningHighTolerant); } }
	private GUIStyle barMidRangeStyle { get { return InitializeStyleWithBgColor(5, colorMidRange); } }
	private GUIStyle barWarningLowStyle { get { return InitializeStyleWithBgColor(6, colorWarningLow); } }
	private GUIStyle barTextStyle { get { return InitializeTextStyle(7, TEXT_SIZE_LABEL, Color.white, TextAnchor.UpperLeft); } }
	private GUIStyle barTextShadowStyle { get { return InitializeTextStyle(8, TEXT_SIZE_LABEL, Color.black, TextAnchor.UpperLeft); } }
	private GUIStyle meterTextWarningHighStyle { get { return InitializeTextStyle(9, TEXT_SIZE_GIANT, colorWarningHigh, TextAnchor.MiddleCenter); } }
	private GUIStyle meterTextWarningHighTolerantStyle { get { return InitializeTextStyle(10, TEXT_SIZE_GIANT, colorWarningHighTolerant, TextAnchor.MiddleCenter); } }
	private GUIStyle meterTextMidRangeStyle { get { return InitializeTextStyle(11, TEXT_SIZE_GIANT, colorMidRange, TextAnchor.MiddleCenter); } }
	private GUIStyle meterTextWarningLowStyle { get { return InitializeTextStyle(12, TEXT_SIZE_GIANT, colorWarningLow, TextAnchor.MiddleCenter); } }
	private GUIStyle multiChMeterTextStyle { get { return InitializeTextStyle(13, TEXT_SIZE_LABEL, Color.white, TextAnchor.MiddleCenter); } }
	private GUIStyle cueListStyle { get { return InitializeTextStyle(14, TEXT_SIZE_CUE_LIST, Color.white, TextAnchor.UpperLeft, s => {
		s.fontStyle = FontStyle.Bold;
		s.wordWrap = true;
	}); } }
	private GUIStyle chartLabelStyle { get { return InitializeTextStyle(15, TEXT_SIZE_CALIBRATOR, Color.gray, TextAnchor.UpperRight); } }
	private GUIStyle timelineHeaderStyle { get { return InitializeStyleWithBgColor(16, new Color(0.15f, 0.15f, 0.15f)); } }
	private GUIStyle timelineStyle { get { return InitializeStyleWithBgColor(17, new Color(0.1f, 0.1f, 0.1f)); } }
	private GUIStyle timelineButtonStyle { get { return InitializeStyleWithGradColor(18, new Color(0.8f, 0.8f, 0.8f), new Color(0.7f, 0.7f, 0.7f), s => {
		s.fixedHeight = 18;
		s.alignment = TextAnchor.MiddleCenter;
		s.clipping = TextClipping.Clip;
		s.margin = new RectOffset(1, 1, 1, 1);
		s.padding = new RectOffset(1, 1, 1, 1);
	}); } }
	private GUIStyle timelineCueSheetStyle { get { return InitializeStyleWithGradColor(19, new Color(0.4f, 0.4f, 0.4f), new Color(0.3f, 0.3f, 0.3f), s => {
		s.alignment = TextAnchor.MiddleCenter;
		s.clipping = TextClipping.Clip;
		s.fontStyle = FontStyle.Bold;
		s.normal.textColor = Color.white;
		s.margin = new RectOffset(1, 1, 1, 1);
		s.padding = new RectOffset(1, 1, 1, 1);
	}); } }
	private delegate void AdditionalStyleDelegate(GUIStyle style);
	static private Color[] chartGradientColors = new Color[] {
		Color.green,
		Color.green * 0.8f + Color.yellow * 0.2f,
		Color.green * 0.6f + Color.yellow * 0.4f,
		Color.green * 0.4f + Color.yellow * 0.6f,
		Color.green * 0.2f + Color.yellow * 0.8f,
		Color.yellow,
		Color.yellow * 0.8f + Color.red * 0.2f,
		Color.yellow * 0.6f + Color.red * 0.4f,
		Color.yellow * 0.4f + Color.red * 0.6f,
		Color.yellow * 0.2f + Color.red * 0.8f,
		Color.red
	};

	/* GUI variables */
	[SerializeField] private bool foldTogglerConfig = false;
	[SerializeField] private bool foldTogglerExperimental = false;
	[SerializeField] private bool foldTogglerPerformance = true;
	[SerializeField] private bool foldTogglerPlayingList = true;
	[SerializeField] private bool foldTogglerLoudness = true;
	[SerializeField] private bool foldTogglerLoudnessAppearance = false;
	[SerializeField] private bool foldTogglerLevels = true;
	[SerializeField] private bool foldTogglerLevelAppearance = false;
	[SerializeField] private bool foldTogglerTimeline = true;
	[SerializeField] private bool foldTogglerLogList = true;
	[SerializeField] private bool timelineAutoScroll = true;
	private Vector2 monitorPanelScrollPos;
	private Vector2 timelinePanelScrollPos;
	private Vector2 logListPanelScrollPos;
	private Vector2 logListScrollPos;
	private Vector2 textareaScrollViewPostion;
	private int logItemCount = 0;
	private bool isLogItemCountChanged = false;
	private int logViewCount = 0;
	private int logFirstViewIndex = 0;
	private bool refreshLogListOnce = false;
	private List<byte[]> logViewSubList = null;
	private Color currentGuiColor;
	private Rect timelineWinRect;
	private Shader m_shader;
	private Material m_mat;
	private Material mat {
		get {
			if (m_shader == null) {
#if UNITY_5_2 || UNITY_5_3 || UNITY_5_4_OR_NEWER
				m_shader = Shader.Find("Hidden/Internal-Colored");
#else
				m_shader = Shader.Find("Sprites/Default");
#endif
			}
			if (m_mat == null && m_shader != null) {
				m_mat = new Material(m_shader);
			} else if (m_mat.shader != m_shader) {
				DestroyImmediate(m_mat);
				m_mat = new Material(m_shader);
			}
			return m_mat;
		}
	}
	private StringBuilder gStrBuilder = new StringBuilder(50);
	private StringBuilder gStrBuilderSub = new StringBuilder(50);

	/* Data slices */
	private DataSlicePerformance perfData = new DataSlicePerformance(profiler);
	private DataSlicePlayback playbackData = new DataSlicePlayback(profiler);
	private DataSliceLoudness loudnessData = new DataSliceLoudness(profiler);
	private DataSliceLevels levelsData = new DataSliceLevels(profiler);
	private DataSliceLocalRuntime runtimeData = new DataSliceLocalRuntime(false);
	private Dictionary<string, CriProfiler.CueSheetGroup> timeline = null;
	private ulong firstTimeStamp = 0;
	private ulong currentTimeStamp = 0;
	private CriProfiler.Playback? currentTimelinePlayback = null;

	private GUIStyle InitializeStyleWithBgColor(int id, Color color, AdditionalStyleDelegate func = null) {
		if (customStyleTex[id] == null) {
			customStyleTex[id] = new Texture2D(1, 1, TextureFormat.RGB24, false);
			customStyleTex[id].SetPixel(1, 1, color);
			customStyleTex[id].Apply();
		}
		if (customStyles[id] == null) {
			customStyles[id] = new GUIStyle();
			if (func != null) {
				func(customStyles[id]);
			}
		}
		customStyles[id].normal.background = customStyleTex[id];
		return customStyles[id];
	}

	private GUIStyle InitializeStyleWithGradColor(int id, Color color1, Color color2, AdditionalStyleDelegate func = null) {
		if (customStyleTex[id] == null) {
			customStyleTex[id] = new Texture2D(1, 4, TextureFormat.RGB24, false);
			customStyleTex[id].SetPixel(1, 1, color2);
			customStyleTex[id].SetPixel(1, 2, color1);
			customStyleTex[id].SetPixel(1, 3, color1);
			customStyleTex[id].SetPixel(1, 4, color2);
			customStyleTex[id].Apply();
		}
		if (customStyles[id] == null) {
			customStyles[id] = new GUIStyle();
			if (func != null) {
				func(customStyles[id]);
			}
		}
		customStyles[id].normal.background = customStyleTex[id];
		return customStyles[id];
	}

	private GUIStyle InitializeTextStyle(int id, int size, Color color, TextAnchor anchor, AdditionalStyleDelegate func = null) {
		if (customStyles[id] == null) {
			customStyles[id] = new GUIStyle();
			customStyles[id].normal.textColor = color;
			customStyles[id].fontSize = size;
			customStyles[id].alignment = anchor;
			if (func != null) {
				func(customStyles[id]);
			}
		}
		return customStyles[id];
	}

	private void DestroyResources() {
		if (m_mat != null) {
			DestroyImmediate(m_mat);
		}
	}

	#endregion Internal Fields

	#region Const strings on GUI
	private const string S_DIVIDER = " / ";
	private const string S_NAN = "-";
	private readonly GUIContent S_LOGFILEDIR = new GUIContent("Log file directory", "leave empty to use default location");
	private readonly GUIContent GC_LEVELRANGE = new GUIContent("Disp. Range (dB)");
	private readonly string[] S_MultiChMeterButton = { "Peak", "RMS" };
	private readonly string[] S_ChannelNames = { "L", "R", "C", "LFE", "Ls", "Rs", "Ex1", "Ex2" };
	#endregion Const strings on GUI

	#region OnGUI

	private void OnGUI() {
		GUILayoutOption buttonHeightSetting = GUILayout.Height(25);
		this.currentGuiColor = GUI.color;

		using (var globalHorizontal = new GUILayout.HorizontalScope()) {
			using (var leftPaneVertical = new GUILayout.VerticalScope(GUILayout.Width(430))) {
				GUILayout.Space(5.0f);
				using (var startControlHorizontal = new GUILayout.HorizontalScope()) {
					GUI.enabled = EditorApplication.isPlaying || remoteProfiling;
					if (profiler.IsProfiling == false) {
						if (GUILayout.Button("Start", buttonHeightSetting)) {
							profiler.StartProfiling(saveLogFile, logFileDir);
							this.isWindowPaused = false;
						}
					} else {
						if (GUILayout.Button("Stop", buttonHeightSetting)) {
							profiler.StopProfiling();
						}
					}
					GUI.enabled = profiler.IsProfiling;
					if (this.isWindowPaused == false) {
						if (GUILayout.Button("Pause", buttonHeightSetting)) {
							this.isWindowPaused = true;
						}
					} else {
						if (GUILayout.Button("Resume", buttonHeightSetting)) {
							this.isWindowPaused = false;
						}
					}
					GUI.enabled = true;
				}
				using (var monitorScrollView = new GUILayout.ScrollViewScope(monitorPanelScrollPos)) {
					monitorPanelScrollPos = monitorScrollView.scrollPosition;

					DrawConfigurationInfo();

					GUILayout.Label("Monitoring", EditorStyles.whiteBoldLabel);

					DrawrPerformanceInfo();
					DrawrPlaybackInfo();
					DrawLoudnessInfo();
					DrawLevelsInfo();

					GUILayout.Space(20.0f);
				}
			}

			using (var rightPaneVertical = new GUILayout.VerticalScope()) {
				DrawTimeline();
				DrawLogList();
			}
		}

		this.Repaint();
	}

	private void DrawConfigurationInfo() {
		EditorGUILayout.BeginVertical(EditorStyles.helpBox);
		foldTogglerConfig = CriFoldout(foldTogglerConfig, "Configuration");
		if (foldTogglerConfig) {
			EditorGUI.indentLevel++;
			startWithUnityPlayer = EditorGUILayout.Toggle("Start with Player", startWithUnityPlayer);
			pauseWithUnityPlayer = EditorGUILayout.Toggle("Pause with Player", pauseWithUnityPlayer);
			saveLogFile = EditorGUILayout.Toggle("Save log file", saveLogFile);
			GUI.enabled = saveLogFile;
			EditorGUI.indentLevel++;
			logFileDir = EditorGUILayout.TextField(S_LOGFILEDIR, profiler.LogFileSavePath);
			EditorGUI.indentLevel--;
			GUI.enabled = true;
			foldTogglerExperimental = CriFoldout(foldTogglerExperimental, "Experimental");
			if (foldTogglerExperimental) {
				EditorGUI.indentLevel++;
				GUI.enabled = (profiler.IsProfiling == false);
				remoteProfiling = EditorGUILayout.Toggle("Remote Profiling", remoteProfiling);
				if (remoteProfiling == false) {
					profiler.ipAddressString = DEFAULT_IP_ADDR;
				}
				GUI.enabled = remoteProfiling && (profiler.IsProfiling == false);
				profiler.ipAddressString = EditorGUILayout.TextField("IP Address", profiler.ipAddressString);
				GUI.enabled = true;
				EditorGUI.indentLevel--;
			}
			EditorGUI.indentLevel--;
		}
		EditorGUILayout.EndVertical();
	}

	private void DrawrPerformanceInfo() {
		EditorGUILayout.BeginVertical(EditorStyles.helpBox);
		foldTogglerPerformance = CriFoldout(foldTogglerPerformance, "Performance");
		if (foldTogglerPerformance) {
			if (isWindowPaused == false) {
				perfData = new DataSlicePerformance(profiler);
				if (this.remoteProfiling == false) {
					runtimeData = new DataSliceLocalRuntime(EditorApplication.isPlaying);
				}
			}
			EditorGUI.indentLevel++;
			Rect chartRect = EditorGUILayout.GetControlRect(GUILayout.Height(80));
			chartRect.xMin += CHART_INDENT_WIDTH;
			LineGraph(chartRect, ClearedStr.Append("CPU Usage: ").AppendFormat("{0:F2}", perfData.cpuLoad).Append("%").ToString(), perfData.cpuLoadHistory, 100.0f, 5, 5.0f, true);
			chartRect = EditorGUILayout.GetControlRect(GUILayout.Height(80));
			chartRect.xMin += CHART_INDENT_WIDTH;
			LineGraph(chartRect, ClearedStr.Append("Number of Voices: ").Append(perfData.usedVoices).Append(S_DIVIDER).Append(perfData.maxVoices).ToString(), Array.ConvertAll(perfData.voiceUsageHistory, x => (float)x), perfData.maxVoices, 5, 4.0f, false);
			GUILayout.BeginHorizontal();
			{
				EditorGUILayout.LabelField("Standard Pool Usage", GUILayout.Width(LABEL_WIDTH));
				SimpleBarGraph(EditorGUILayout.GetControlRect(), perfData.usedStandardVoices, 0, perfData.maxStandardVoices,
					ClearedStr.Append(perfData.usedStandardVoices >= 0 ? perfData.usedStandardVoices.ToString() : S_NAN)
					.Append(S_DIVIDER)
					.Append(perfData.maxStandardVoices > 0 ? perfData.maxStandardVoices.ToString() : S_NAN)
					.ToString());
			}
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal();
			{
				EditorGUILayout.LabelField("HCA-MX Pool Usage", GUILayout.Width(LABEL_WIDTH));
				SimpleBarGraph(EditorGUILayout.GetControlRect(), perfData.usedHcaMxVoices, 0, perfData.maxHcaMxVoices,
					ClearedStr.Append(perfData.usedHcaMxVoices >= 0 ? perfData.usedHcaMxVoices.ToString() : S_NAN)
					.Append(S_DIVIDER)
					.Append(perfData.maxHcaMxVoices > 0 ? perfData.maxHcaMxVoices.ToString() : S_NAN)
					.ToString());
			}
			GUILayout.EndHorizontal();
			if (this.remoteProfiling == false) {
				EditorGUILayout.LabelField("Atom Memory Usage", DataSize2String(runtimeData.memUsageAtom));
				EditorGUILayout.LabelField("FS Memory Usage", DataSize2String(runtimeData.memUsageFs));
			}
			EditorGUILayout.LabelField("Streaming Usage", ClearedStr.Append(perfData.usedStreams).Append(" voice(s) (").AppendFormat("{0:F2}", perfData.totalBps / 1000000).Append(" Mbps)").ToString());
			EditorGUI.indentLevel--;
		}
		EditorGUILayout.EndVertical();
	}

	private void DrawrPlaybackInfo() {
		EditorGUILayout.BeginVertical(EditorStyles.helpBox);
		foldTogglerPlayingList = CriFoldout(foldTogglerPlayingList, "Playing");
		if (foldTogglerPlayingList) {
			if (isWindowPaused == false) {
				playbackData = new DataSlicePlayback(profiler);
			}
			GUILayout.BeginHorizontal();
			{
				GUILayout.Space(GUI_INDENT_WIDTH);
				textareaScrollViewPostion = GUILayout.BeginScrollView(new Vector2(textareaScrollViewPostion.x, Mathf.Infinity), EditorStyles.helpBox, GUILayout.Height(60));
				GUILayout.Label(playbackData.playbackListString.ToString(), cueListStyle, GUILayout.ExpandHeight(true));
				GUILayout.EndScrollView();
			}
			GUILayout.EndHorizontal();
			EditorGUI.indentLevel++;
			EditorGUILayout.LabelField("Last Played", ClearedStr.Append("[ ").Append(playbackData.cuenameLastPlayed).Append(" ]").ToString());
			EditorGUI.indentLevel--;
		}
		EditorGUILayout.EndVertical();
	}

	private void DrawLoudnessInfo() {
		EditorGUILayout.BeginVertical(EditorStyles.helpBox);
		foldTogglerLoudness = CriFoldout(foldTogglerLoudness, "Loudness");
		if (foldTogglerLoudness) {
			if (isWindowPaused == false) {
				loudnessData = new DataSliceLoudness(profiler);
			}
			Rect chartRect;
			GUILayoutOption[] numberRectGUISettings = { GUILayout.Height(60), GUILayout.MinWidth(60) };
			GUILayoutOption barHeightSetting = GUILayout.Height(15);
			const float paddingOffset = 2.0f;
			GUILayout.BeginHorizontal();
			{
				chartRect = EditorGUILayout.GetControlRect(numberRectGUISettings);
				chartRect.x += CHART_INDENT_WIDTH;
				NumericMeter(chartRect, "Momentary  LKFS", loudnessData.momentary);
				chartRect = EditorGUILayout.GetControlRect(numberRectGUISettings);
				chartRect.x += CHART_INDENT_WIDTH - paddingOffset;
				NumericMeter(chartRect, "Short Term  LKFS", loudnessData.shortTerm);
				chartRect = EditorGUILayout.GetControlRect(numberRectGUISettings);
				chartRect.xMin += CHART_INDENT_WIDTH - paddingOffset * 2;
				NumericMeter(chartRect, "Integrated  LKFS", loudnessData.integrated);
			}
			GUILayout.EndHorizontal();
			chartRect = EditorGUILayout.GetControlRect(barHeightSetting);
			chartRect.xMin += CHART_INDENT_WIDTH;
			LoudnessBarGraph(chartRect, loudnessData.momentary, currentDbFloor, currentDbCeiling, currentDbTarget, currentDbRangePlusMinus, currentDbWarning, "Momentary");
			chartRect = EditorGUILayout.GetControlRect(barHeightSetting);
			chartRect.xMin += CHART_INDENT_WIDTH;
			LoudnessBarGraph(chartRect, loudnessData.shortTerm, currentDbFloor, currentDbCeiling, currentDbTarget, currentDbRangePlusMinus, currentDbWarning, "Short Term");
			chartRect = EditorGUILayout.GetControlRect(barHeightSetting);
			chartRect.xMin += CHART_INDENT_WIDTH;
			LoudnessBarGraph(chartRect, loudnessData.integrated, currentDbFloor, currentDbCeiling, currentDbTarget, currentDbRangePlusMinus, currentDbWarning, "Integrated");
			EditorGUI.indentLevel++;
			foldTogglerLoudnessAppearance = CriFoldout(foldTogglerLoudnessAppearance, "Appearance");
			if (foldTogglerLoudnessAppearance) {
				EditorGUI.indentLevel++;
				EditorGUILayout.MinMaxSlider(GC_LEVELRANGE, ref currentDbFloor, ref currentDbCeiling, DB_MIN, DB_MAX);
				EditorGUILayout.LabelField(" ", ClearedStr.Append("Min: ").AppendFormat("{0:F2}", currentDbFloor).Append(S_DIVIDER).Append("Max: ").AppendFormat("{0:F2}", currentDbCeiling).ToString());
				currentDbTarget = EditorGUILayout.Slider("Target (dB)", currentDbTarget, DB_MIN, DB_MAX);
				currentDbRangePlusMinus = EditorGUILayout.Slider("Tgt. Range (+/-dB)", currentDbRangePlusMinus, DB_RANGE_PLUSMINUS_MIN, DB_RANGE_PLUSMINUS_MAX);
				currentDbWarning = EditorGUILayout.Slider("Warning (dB)", currentDbWarning, DB_MIN, DB_MAX);
				GUILayout.BeginHorizontal();
				{
					EditorGUILayout.LabelField("", GUILayout.Width(LABEL_WIDTH));
					if (GUILayout.Button("Reset", GUILayout.ExpandWidth(false))) {
						currentDbFloor = DEFAULT_DB_FLOOR;
						currentDbCeiling = DEFAULT_DB_CEILING;
						currentDbTarget = DEFAULT_DB_TARGET;
						currentDbRangePlusMinus = DEFAULT_DB_RANGE_PLUSMINUS;
						currentDbWarning = DEFAULT_DB_WARNING;
					}
				}
				GUILayout.EndHorizontal();
				EditorGUI.indentLevel--;
			}
			EditorGUI.indentLevel--;
		}
		EditorGUILayout.EndVertical();
	}

	private void DrawLevelsInfo() {
		EditorGUILayout.BeginVertical(EditorStyles.helpBox);
		foldTogglerLevels = CriFoldout(foldTogglerLevels, "Peak / RMS");
		if (foldTogglerLevels) {
			if (isWindowPaused == false) {
				levelsData = new DataSliceLevels(profiler);
			}
			GUILayout.BeginHorizontal();
			{
				GUILayout.Space(GUI_INDENT_WIDTH);
				currentLevelType = (MultiChannelMeterType)GUILayout.Toolbar((int)currentLevelType, S_MultiChMeterButton);
			}
			GUILayout.EndHorizontal();
			Rect chartRect = EditorGUILayout.GetControlRect(GUILayout.Height(200));
			chartRect.xMin += CHART_INDENT_WIDTH;
			MultiChannelMeter(chartRect, levelsData.levels, levelsData.outputCh, currentLevelType, currentLevelFloor, currentLevelCeiling, currentLevelWarning, S_MultiChMeterButton[(int)currentLevelType]);
			EditorGUI.indentLevel++;
			foldTogglerLevelAppearance = CriFoldout(foldTogglerLevelAppearance, "Appearance");
			if (foldTogglerLevelAppearance) {
				EditorGUI.indentLevel++;
				EditorGUILayout.MinMaxSlider(GC_LEVELRANGE, ref currentLevelFloor, ref currentLevelCeiling, DB_MIN, DB_MAX);
				EditorGUILayout.LabelField(" ", ClearedStr.Append("Min: ").AppendFormat("{0:F2}", currentLevelFloor).Append(S_DIVIDER).Append("Max: ").AppendFormat("{0:F2}", currentLevelCeiling).ToString());
				currentLevelWarning = EditorGUILayout.Slider("Warning (dB)", currentLevelWarning, DB_MIN, DB_MAX);
				currentLevelCalInterval = EditorGUILayout.IntSlider("Grid Interval (dB)", currentLevelCalInterval, DEFAULT_LEVEL_CAL_INTVL_MIN, DEFAULT_LEVEL_CAL_INTVL_MAX);
				GUILayout.BeginHorizontal();
				{
					EditorGUILayout.LabelField("", GUILayout.Width(LABEL_WIDTH));
					if (GUILayout.Button("Reset", GUILayout.ExpandWidth(false))) {
						currentLevelFloor = DEFAULT_LEVEL_FLOOR;
						currentLevelCeiling = DEFAULT_LEVEL_CEILING;
						currentLevelWarning = DEFAULT_LEVEL_WARNING;
						currentLevelCalInterval = DEFAULT_LEVEL_CAL_INTVL;
					}
				}
				GUILayout.EndHorizontal();
				EditorGUI.indentLevel--;
			}
			EditorGUI.indentLevel--;
		}
		EditorGUILayout.EndVertical();
	}

	private void DrawTimeline() {
		const float timeScaleFromMicroSec = 0.00005f;

		if (Event.current.type == EventType.Layout) {
			if (this.isWindowPaused == false && profiler.IsProfiling) {
				this.timeline = profiler.TimelineDataSlice;
				this.firstTimeStamp = profiler.timestampStart;
				this.currentTimeStamp = profiler.timestamp;
			}
		}

		using (var scope = new GUILayout.HorizontalScope("ToolBar")) {
			this.foldTogglerTimeline = GUILayout.Toggle(this.foldTogglerTimeline, "Timeline", EditorStyles.toolbarButton, GUILayout.Width(100));
			GUILayout.Label(currentTimelinePlayback.HasValue ? currentTimelinePlayback.Value.cuename + " (" + currentTimelinePlayback.Value.cuesheet + ") started at " + MicroSec2String(currentTimelinePlayback.Value.timestamp) : " ");
			this.timelineAutoScroll = GUILayout.Toggle(this.timelineAutoScroll, "Auto Scroll", EditorStyles.toolbarButton, GUILayout.Width(100));
		}

		if (this.foldTogglerTimeline == false) { return; }

		GUI.color = Color.white;
		EditorGUILayout.BeginHorizontal();

		var heightStyle = this.foldTogglerLogList ? GUILayout.MinHeight(300) : GUILayout.ExpandHeight(true);

		/* CueSheet Group */
		using (var scrollView = new GUILayout.ScrollViewScope(new Vector2(0, timelinePanelScrollPos.y), false, false, GUIStyle.none, GUIStyle.none, timelineHeaderStyle, heightStyle, GUILayout.Width(100))) {
			GUILayout.Space(0);
			if (timeline != null && timeline.Count > 0) foreach (var elem in timeline) {
				GUI.color = elem.Value.cuesheetColor;
					GUILayout.Label(elem.Value.name, timelineCueSheetStyle, GUILayout.Height((timelineButtonStyle.fixedHeight + 1) * elem.Value.TotalLaneCount - 1));
				GUI.color = Color.white;
			}
			GUILayout.Space(18);
		}
		/* Timeline */
		using (var scrollView = new GUILayout.ScrollViewScope(timelinePanelScrollPos, timelineStyle, heightStyle)) {
			float timelinePixelLength = (this.currentTimeStamp - this.firstTimeStamp) * timeScaleFromMicroSec;
			timelinePanelScrollPos = (this.timelineAutoScroll && this.isWindowPaused == false && profiler.IsProfiling) ? new Vector2(Mathf.Max(0, timelinePixelLength + 12 - this.timelineWinRect.width), scrollView.scrollPosition.y) : scrollView.scrollPosition;

			EditorGUILayout.BeginHorizontal();
			EditorGUILayout.BeginVertical(GUILayout.Width(timelinePixelLength));
			if (timeline == null || timeline.Count <= 0) {
				GUILayout.Space(timelinePixelLength);
			} else {
				foreach (var cuesheetGroup in timeline) {
					EditorGUILayout.BeginVertical();
					foreach (var cuelane in cuesheetGroup.Value.cueLaneDict) {
						EditorGUILayout.BeginVertical();
						for (int j = 0; j < cuelane.Value.playbackLaneList.Count; ++j) {
							EditorGUILayout.BeginHorizontal();
							var pbList = cuelane.Value.playbackLaneList[j].playbackList;
							bool firstPlaybackDrawn = false;
							if (timelinePanelScrollPos.x > 0) {
								GUILayout.Space(timelinePanelScrollPos.x);
							}
							for (int k = 0; k < pbList.Count; ++k) {
								float posRelative = ((long)pbList[k].timestamp - (long)this.firstTimeStamp) * timeScaleFromMicroSec - timelinePanelScrollPos.x;

								if (posRelative > this.timelineWinRect.width) { break; } /* skip invisible */

								if (pbList[k].isStart) {
									float currentLength = 0;
									float priorSpaceWidth = 0;

									if (k < pbList.Count - 1) {
										if (((long)pbList[k + 1].timestamp - (long)this.firstTimeStamp) * timeScaleFromMicroSec - timelinePanelScrollPos.x < 0) { /* skip invisible */
											k++;
											continue;
										}
										currentLength = ((long)pbList[k + 1].timestamp - (long)pbList[k].timestamp) * timeScaleFromMicroSec;
									} else {
										currentLength = ((long)this.currentTimeStamp - (long)pbList[k].timestamp) * timeScaleFromMicroSec;
									}
									currentLength = Mathf.Min(currentLength + Mathf.Min(0, posRelative), this.timelineWinRect.width - Mathf.Max(0, posRelative));

									if (firstPlaybackDrawn == false) {
										priorSpaceWidth = Mathf.Max(0, posRelative);
									} else {
										priorSpaceWidth = ((long)pbList[k].timestamp - (long)pbList[k - 1].timestamp) * timeScaleFromMicroSec;
									}

									GUILayout.Space(priorSpaceWidth);
									GUI.color = cuelane.Value.cueColor;
									DrawPlaybackEventButton(pbList[k], currentLength);
									GUI.color = Color.white;
									firstPlaybackDrawn = true;
								} else if (firstPlaybackDrawn == false && posRelative > 0) {
									GUI.color = cuelane.Value.cueColor;
									DrawPlaybackEventButton(pbList[k], posRelative);
									GUI.color = Color.white;
									firstPlaybackDrawn = true;
								}
							}

							if (firstPlaybackDrawn == false) {
								GUILayout.Label("", timelineButtonStyle, GUILayout.Width(0)); /* empty lane */
							}
							EditorGUILayout.EndHorizontal();
						}
						EditorGUILayout.EndVertical();
					}
					EditorGUILayout.EndVertical();
				}
			}
			EditorGUILayout.EndVertical();
			GUILayout.Box("", barBorderStyle, GUILayout.Width(1), GUILayout.ExpandHeight(true));
			GUILayout.Box("", barBorderStyle, GUILayout.Width(2), GUILayout.Height(30));
			GUILayout.Space(9);
			EditorGUILayout.EndHorizontal();
		}
		if (Event.current.type == EventType.Repaint) {
			this.timelineWinRect = GUILayoutUtility.GetLastRect();
		}

		EditorGUILayout.EndHorizontal();
		GUI.color = this.currentGuiColor;
	}

	private void DrawPlaybackEventButton(CriProfiler.Playback playback, float width) {
		if (GUILayout.Button(playback.cuename, timelineButtonStyle, GUILayout.Width(width - 1), GUILayout.MinWidth(-100))) {
			this.currentTimelinePlayback = playback;
			this.timelineAutoScroll = false;
		}
	}

	private void DrawLogList() {
		if (Event.current.type == EventType.Layout && ((this.isWindowPaused == false && profiler.IsProfiling) || refreshLogListOnce)) {
			refreshLogListOnce = false;
			int logListlen = profiler.GetLogList().Count;
			if (this.logItemCount != logListlen) {
				this.logItemCount = logListlen;
				this.isLogItemCountChanged = true;
			}
			this.logViewCount = (int)position.height / LOG_ITEM_HEIGHT;
			this.logFirstViewIndex = Mathf.Clamp((int)logListScrollPos.y / LOG_ITEM_HEIGHT, 0, Mathf.Max(0, logItemCount - logViewCount));
			this.logViewSubList = profiler.GetLogList(logFirstViewIndex, logViewCount);
		}

		using (var scope = new GUILayout.HorizontalScope("ToolBar", GUILayout.ExpandWidth(true))) {
			this.foldTogglerLogList = GUILayout.Toggle(this.foldTogglerLogList, "Event Log", EditorStyles.toolbarButton, GUILayout.Width(100));
			GUILayout.Label(logItemCount.ToString() + " lines", EditorStyles.miniLabel);
			if (GUILayout.Button("Open Log File", EditorStyles.toolbarButton, GUILayout.Width(120))) {
				var path = EditorUtility.OpenFilePanel("Open Log File", profiler.LogFileSavePath, CriProfiler.LOG_FILE_EXTENSION);
				if (string.IsNullOrEmpty(path) == false) {
					profiler.ClearLog();
					profiler.LoadLogFromFile(path);
					refreshLogListOnce = true;
				}
			}
			if (GUILayout.Button("Clear Log", EditorStyles.toolbarButton, GUILayout.Width(100))) {
				profiler.ClearLog();
				refreshLogListOnce = true;
			}
		}

		if (this.foldTogglerLogList == false) { return; }

		using (var scrollView = new GUILayout.ScrollViewScope(logListPanelScrollPos, "CN Box")) {
			logListPanelScrollPos = scrollView.scrollPosition;
			if (this.isLogItemCountChanged) {
				this.isLogItemCountChanged = false;
				this.logListScrollPos = new Vector2(0, logItemCount * LOG_ITEM_HEIGHT);
			}
			logListScrollPos = GUILayout.BeginScrollView(logListScrollPos);
			{
				/* only render visible UI elements */
				if (logViewSubList != null) {
					GUILayout.Space(logFirstViewIndex * LOG_ITEM_HEIGHT);
					for (int i = 0; i < logViewSubList.Count; i++) {
						GUILayout.Label(profiler.FilteredLog2String(logViewSubList[i]), EditorStyles.textArea, GUILayout.Height(LOG_ITEM_HEIGHT));
					}
					GUILayout.Space(Mathf.Max(0, logItemCount - logFirstViewIndex - logViewCount) * LOG_ITEM_HEIGHT);
				}
			}
			GUILayout.EndScrollView();
		}
	}

	#endregion OnGUI

	#region Drawing Functions

	private void SimpleBarGraph(UnityEngine.Rect rect, Single val, Single min, Single max, String text) {
		Single ratio = Value2Ratio(val, min, max);

		UnityEngine.GUI.Box(rect, "", barBorderStyle);
		rect.xMin++;
		rect.yMin++;
		rect.xMax--;
		rect.yMax--;
		UnityEngine.GUI.Box(rect, "", barBackgroundStyle);
		if (Event.current.type == EventType.Repaint) {
			if (ratio > 0.0f) {
				Rect barRect = rect;
				if (ratio > 0.999999f) {
					UnityEngine.GUI.Box(barRect, "", barWarningHighStyle);
				} else {
					barRect.width *= ratio;
					UnityEngine.GUI.Box(barRect, "", barStyle);
				}
			}
		}
		rect.xMin += 2;
		UnityEngine.GUI.Label(new Rect(rect.x + 1, rect.y + 1, rect.width, rect.height), text, barTextShadowStyle);
		UnityEngine.GUI.Label(rect, text, barTextStyle);
	}

	private void LoudnessBarGraph(
		UnityEngine.Rect rect,
		Single val,
		Single min,
		Single max,
		Single valTarget,
		Single valRangePlusMinus,
		Single valWarningHigh,
		String text)
	{
		Single ratio = Value2Ratio(val, min, max);
		Single ratioTarget = Value2Ratio(valTarget, min, max);
		Single ratioRange = Value2Ratio(min + valRangePlusMinus, min, max);
		Single ratioWarning = Value2Ratio(valWarningHigh, min, max);

		UnityEngine.GUI.Box(rect, "", barBorderStyle);
		rect.xMin++;
		rect.yMin++;
		rect.xMax--;
		rect.yMax--;
		UnityEngine.GUI.Box(rect, "", barBackgroundStyle);
		if (Event.current.type == EventType.Repaint) {
			if (ratio > 0.0f) {
				Rect barRect = rect;
				if (ratio > ratioWarning) {
					barRect.width = rect.width * ratio;
					UnityEngine.GUI.Box(barRect, "", barWarningHighStyle);
				}
				if (ratio > ratioTarget + ratioRange) {
					barRect.width = rect.width * Mathf.Min(ratio, ratioWarning);
					UnityEngine.GUI.Box(barRect, "", barWarningHighTolerantStyle);
				}
				if (ratio > ratioTarget - ratioRange) {
					barRect.width = rect.width * Mathf.Min(ratio, ratioTarget + ratioRange);
					UnityEngine.GUI.Box(barRect, "", barMidRangeStyle);
				}
				if (ratio > 0) {
					barRect.width = rect.width * Mathf.Max(0, Mathf.Min(ratio, ratioTarget - ratioRange));
					UnityEngine.GUI.Box(barRect, "", barWarningLowStyle);
				}
			}
		}
		rect.xMin += 2;
		UnityEngine.GUI.Label(new Rect(rect.x + 1, rect.y + 1, rect.width, rect.height), text, barTextShadowStyle);
		UnityEngine.GUI.Label(rect, text, barTextStyle);
	}

	/* Positive values only */
	private void LineGraph(Rect rect, string title, float[] data, float maxVal, int sections, float minInterVal, bool adaptiveScale) {
		const float titleHeight = 20;
		const float labelWidth = 30;
		const float labelHeight = 10.0f;
		float cellHeight = 0;
		int count = 0;
		Rect labelRect;

		float currentMaxVal = maxVal;
		if(adaptiveScale == true) {
			currentMaxVal = Mathf.Min(Mathf.Max(data) * 2.0f, maxVal);
		}
		float interVal = (int)(currentMaxVal / sections);
		if (interVal < minInterVal) {
			interVal = minInterVal;
		}

		GUI.Box(rect, "", barBorderStyle);
		rect.xMin++;
		rect.yMin++;
		rect.xMax--;
		rect.yMax--;
		GUI.Box(rect, "", barBackgroundStyle);
		labelRect = new Rect(rect.x + textOffsetChartTitle.x, rect.y + textOffsetChartTitle.y, rect.width, titleHeight);
		GUI.Label(labelRect, title, barTextStyle);
		rect.xMin += labelWidth;
		rect.yMin += titleHeight;
		GUI.Box(rect, "", barBorderStyle);
		rect.xMin++;
		rect.yMin++;
		GUI.Box(rect, "", barBackgroundStyle);

		if (mat != null && Event.current.type == EventType.Repaint) {
			GUI.BeginClip(rect);
			GL.PushMatrix();
			{
				mat.SetPass(0);

				/* grid */
				GL.Begin(GL.LINES);
				cellHeight = rect.height * interVal / currentMaxVal;
				count = (int)(currentMaxVal / interVal);
				if(currentMaxVal % interVal == 0) {
					count -= 1;
				}
				GL.Color(colorGrid);
				float lineYPos = 0;
				for (int i = 1; i <= count; i++) {
					lineYPos = rect.height - i * cellHeight;
					GL.Vertex3(0, lineYPos, 0);
					GL.Vertex3(rect.width, lineYPos, 0);
				}
				GL.End();

                /* data */
#if UNITY_5_6_OR_NEWER
                GL.Begin(GL.LINE_STRIP);
                for (int i = 0; i < data.Length; i++) {
					GL.Color(ColorSelector(data[i], maxVal));
					GL.Vertex3((rect.width / (data.Length - 1)) * i, rect.height * (1 - data[i] / currentMaxVal), 0);
				}
				GL.End();
#else
                GL.Begin(GL.LINES);
                for (int i = 1; i < data.Length; i++)
                {
                    GL.Color(ColorSelector(data[i - 1], maxVal));
                    GL.Vertex3((rect.width / (data.Length - 1)) * (i - 1), rect.height * (1 - data[i - 1] / currentMaxVal), 0);
                    GL.Color(ColorSelector(data[i], maxVal));
                    GL.Vertex3((rect.width / (data.Length - 1)) * i, rect.height * (1 - data[i] / currentMaxVal), 0);
                }
                GL.End();
#endif
			}
			GL.PopMatrix();
			GUI.EndClip();

			/* labels*/
			rect.xMin -= labelWidth;
			labelRect = new Rect(rect.x + textOffsetCalibrator.x, rect.y + textOffsetCalibrator.y, labelWidth, labelHeight);
			GUI.Label(labelRect, currentMaxVal.ToString("F0"), chartLabelStyle);
			for (int i = 1; i <= count; i++) {
				labelRect.y = rect.y + rect.height - i * cellHeight + textOffsetCalibrator.y;
				if (labelRect.y > rect.y) {
					GUI.Label(labelRect, (interVal * i).ToString("F0"), chartLabelStyle);
				}
			}
		} /* if Event == Repaint */
	}

	private void NumericMeter(UnityEngine.Rect rect, String meterLabel, Single meterVal) {
		const Single titleHeight = 20;
		Rect labelRect;
		Rect numRect;

		UnityEngine.GUI.Box(rect, "", barBorderStyle);
		rect.xMin++;
		rect.yMin++;
		rect.xMax--;
		rect.yMax--;
		UnityEngine.GUI.Box(rect, "", barBackgroundStyle);

		labelRect = new Rect(rect.x + textOffsetChartTitle.x, rect.y + textOffsetChartTitle.y, rect.width, titleHeight);
		GUI.Label(labelRect, meterLabel, barTextStyle);
		numRect = new Rect(rect.center.x, rect.center.y, 10, 10);
		if (meterVal > currentDbWarning) {
			GUI.Label(numRect, Dbfs2String(meterVal, "F1"), meterTextWarningHighStyle);
		} else if (meterVal > currentDbTarget + currentDbRangePlusMinus) {
			GUI.Label(numRect, Dbfs2String(meterVal, "F1"), meterTextWarningHighTolerantStyle);
		} else if (meterVal > currentDbTarget - currentDbRangePlusMinus) {
			GUI.Label(numRect, Dbfs2String(meterVal, "F1"), meterTextMidRangeStyle);
		} else {
			GUI.Label(numRect, Dbfs2String(meterVal, "F1"), meterTextWarningLowStyle);
		}
	}

	private enum MultiChannelMeterType {
		Peak,
		RMS
	}
	private void MultiChannelMeter(
		UnityEngine.Rect rect,
		CriProfiler.LevelInfo[] info,
		int numCh,
		MultiChannelMeterType type,
		float min,
		float max,
		float warningThresh,
		string title)
	{
		float warningRatio = (warningThresh - min) / (max - min);

		const float barInterval = 20.0f;
		const float calWidth = 30.0f;
		const float calHeight = 10.0f;
		const float titleHeight = 20.0f;
		const float headHeight = 20.0f;
		const float footHeight = 20.0f;
		const float baseValue = 0.0f;
		Color barColor = Color.green;
		Color barWarningColor = Color.red;
		Color holderColor = Color.green;
		Color holderWarningColor = Color.red;
		Rect chartRect;

		GUI.Box(rect, "", barBorderStyle);
		rect.xMin++;
		rect.yMin++;
		rect.xMax--;
		rect.yMax--;
		GUI.Box(rect, "", barBackgroundStyle);

		chartRect = rect;
		chartRect.xMin += calWidth;
		chartRect.xMax++;
		chartRect.yMin += titleHeight + headHeight;
		chartRect.yMax -= footHeight;
		GUI.Box(chartRect, "", barBorderStyle);
		chartRect.xMin++;
		chartRect.yMin++;
		chartRect.xMax--;
		chartRect.yMax--;
		GUI.Box(chartRect, "", barBackgroundStyle);

		/* title */
		GUI.Label(new Rect(rect.x + textOffsetChartTitle.x, rect.y + textOffsetChartTitle.y, rect.width, rect.height), title, barTextStyle);
		rect.yMin += titleHeight;

		/* horizontal labels */
		float barWidth = (chartRect.width - barInterval * (numCh + 1)) / numCh;
		float labelXStart = calWidth + 1 + barInterval;
		if (info != null && info.Length >= numCh) {
			for (int i = 0; i < numCh; ++i) {
				switch (type) {
					case MultiChannelMeterType.Peak:
						GUI.Label(new Rect(rect.x + labelXStart, rect.y, barWidth, headHeight), Dbfs2String(Level2Db(info[i].levelPeak), "F1"), multiChMeterTextStyle);
						break;
					case MultiChannelMeterType.RMS:
						GUI.Label(new Rect(rect.x + labelXStart, rect.y, barWidth, headHeight), Dbfs2String(Level2Db(info[i].levelRms), "F1"), multiChMeterTextStyle);
						break;
					default:
						break;
				}
				GUI.Label(new Rect(rect.x + labelXStart, rect.y + rect.height - footHeight, barWidth, footHeight), i < S_ChannelNames.Length ? S_ChannelNames[i] : "CH" + (i + 1), multiChMeterTextStyle);
				labelXStart += barInterval + barWidth;
			}
		}

		/* vertical labels */
		rect.yMin += headHeight + 1;
		rect.yMax -= footHeight + 1;
		float cellHeight = rect.height * currentLevelCalInterval / (currentLevelCeiling - currentLevelFloor);

		float firstCellInterval = ((currentLevelCeiling - baseValue) % currentLevelCalInterval + currentLevelCalInterval) % currentLevelCalInterval;
		if (firstCellInterval == 0) {
			firstCellInterval = currentLevelCalInterval;
		}
		float firstCellHeight = rect.height * firstCellInterval / (currentLevelCeiling - currentLevelFloor);

		float remainingInterval = currentLevelCeiling - firstCellInterval - currentLevelFloor;
		int cellCount = (int)(remainingInterval / currentLevelCalInterval);
		if (remainingInterval % currentLevelCalInterval == 0) {
			cellCount -= 1;
		}

		GUI.Label(new Rect(rect.x + textOffsetCalibrator.x, rect.y + textOffsetCalibrator.y, calWidth, calHeight), currentLevelCeiling.ToString("F0"), chartLabelStyle);
		GUI.Label(new Rect(rect.x + textOffsetCalibrator.x, rect.y + rect.height + textOffsetCalibrator.y, calWidth, calHeight), currentLevelFloor.ToString("F0"), chartLabelStyle);
		for (int i=0; i < cellCount + 1; ++i) {
			GUI.Label(new Rect(rect.x + textOffsetCalibrator.x, rect.y + firstCellHeight + cellHeight * i + textOffsetCalibrator.y, calWidth, calHeight), (currentLevelCeiling - firstCellInterval - currentLevelCalInterval * i).ToString("F0"), chartLabelStyle);
		}

		/* GL drawing */
		if (mat != null && Event.current.type == EventType.Repaint) {
			GUI.BeginClip(chartRect);
			GL.PushMatrix();
			{
				mat.SetPass(0);

				/* grid */
				float lineYPos = 0;
				GL.Begin(GL.LINES);
				GL.Color(colorGrid);
				for (int i = 0; i < cellCount + 1; ++i) {
					lineYPos = firstCellHeight + i * cellHeight;
					GL.Vertex3(0, lineYPos, 0);
					GL.Vertex3(chartRect.width, lineYPos, 0);
				}
				GL.End();

				/* bars */
				if (info != null && info.Length >= numCh && barInterval * (numCh + 1) < chartRect.width) {
					float barHeight;
					float barWarningHeight;
					float barXStart;
					for (int i = 0; i < numCh; ++i) {
						float heightRatio = 0;
						switch (type) {
							case MultiChannelMeterType.Peak:
								heightRatio = ((Level2Db(info[i].levelPeak) - currentLevelFloor) / (currentLevelCeiling - currentLevelFloor));
								break;
							case MultiChannelMeterType.RMS:
								heightRatio = ((Level2Db(info[i].levelRms) - currentLevelFloor) / (currentLevelCeiling - currentLevelFloor));
								break;
							default:
								break;
						}
						if (heightRatio > 1.0f) {
							heightRatio = 1.0f;
						}
						if (heightRatio < 0) {
							heightRatio = 0;
						}
						barHeight = chartRect.height * heightRatio;
						barWarningHeight = chartRect.height * warningRatio;
						barXStart = (barInterval + barWidth) * i + barInterval;
						GL.Begin(GL.QUADS);
						if (heightRatio > warningRatio) {
							GL.Color(barWarningColor);
							GL.Vertex3(barXStart, chartRect.height, 0);
							GL.Vertex3(barXStart + barWidth, chartRect.height, 0);
							GL.Vertex3(barXStart + barWidth, chartRect.height - barHeight, 0);
							GL.Vertex3(barXStart, chartRect.height - barHeight, 0);
						}
						GL.Color(barColor);
						GL.Vertex3(barXStart, chartRect.height, 0);
						GL.Vertex3(barXStart + barWidth, chartRect.height, 0);
						GL.Vertex3(barXStart + barWidth, chartRect.height - Mathf.Max(0, Mathf.Min(barHeight, barWarningHeight)), 0);
						GL.Vertex3(barXStart, chartRect.height - Mathf.Max(0, Mathf.Min(barHeight, barWarningHeight)), 0);
						GL.End();

						/* holders */
						if (type == MultiChannelMeterType.Peak) {
							heightRatio = ((Level2Db(info[i].levelPeakHold) - currentLevelFloor) / (currentLevelCeiling - currentLevelFloor));
							if (heightRatio > 1.0f) {
								heightRatio = 1.0f;
							}
							if (heightRatio < 0) {
								heightRatio = 0;
							}
							barHeight = chartRect.height * heightRatio;
							GL.Begin(GL.LINES);
							if (heightRatio > warningRatio) {
								GL.Color(holderWarningColor);
							} else {
								GL.Color(holderColor);
							}
							GL.Vertex3(barXStart, chartRect.height - barHeight, 0);
							GL.Vertex3(barXStart + barWidth, chartRect.height - barHeight, 0);
							GL.End();
						}
					}
				}
			}
			GL.PopMatrix();
			GUI.EndClip();
		}
	}

	#endregion Drawing Functions

	#region Utilities

	static private bool CriFoldout(bool foldout, string content) {
#if UNITY_5_5_OR_NEWER
		return EditorGUILayout.Foldout(foldout, content, true);
#else
        return EditorGUILayout.Foldout(foldout, content);
#endif
	}

	static private Single Level2Db(Single level) {
		return Mathf.Log10(level) * 20.0f;
	}

	static private Single Value2Ratio(Single val, Single min, Single max) {
		if (max - min == 0.0f) {
			return 0.0f;
		}
		if (val > max) {
			return 1.0f;
		}
		if (val < min) {
			return 0.0f;
		}
		return (val - min) / (max - min);
	}

	static private string Dbfs2String(Single dbVal, string format) {
		if (dbVal < Single.MinValue) {
			return "---";
		} else {
			return dbVal.ToString(format);
		}
	}

	static private string MicroSec2String(ulong microSec) {
		ulong hour = microSec / 3600000000;
		ulong min = (microSec % 3600000000) / 60000000;
		ulong sec = (microSec % 60000000) / 1000000;
		ulong msec = (microSec % 1000000) / 1000;
		return string.Format("{0}:{1}:{2}.{3}", hour, min, sec, msec);
	}

	/* Only to use on the first layer of OnGUI */
	private StringBuilder ClearedStr {
		get {
			this.gStrBuilder.Length = 0;
			return this.gStrBuilder;
		}
	}

	private string DataSize2String(float sizeByte) {
		gStrBuilderSub.Length = 0;
		if (sizeByte > 524288.0f) {
			return gStrBuilderSub.AppendFormat("{0:F2}", sizeByte / 1048576.0f).Append(" MB").ToString();
		} else if (sizeByte > 512.0f) {
			return gStrBuilderSub.AppendFormat("{0:F2}", sizeByte / 1024.0f).Append(" KB").ToString();
		} else {
			return gStrBuilderSub.AppendFormat("{0:F0}", sizeByte).Append(" Byte").ToString();
		}
	}

	private Color ColorSelector(float sample, float maxVal) {
		int index = (int)((chartGradientColors.Length - 1) * (maxVal == 0 ? 0 : sample / maxVal));
		index = Mathf.Clamp(index, 0, chartGradientColors.Length - 1);

		return (chartGradientColors.Length > 0) ? chartGradientColors[index] : Color.white;
	}

	#endregion Utilities
}

} //namespace CriWare