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

//#define CRI_PROFILER_PERF_DEBUG

using System;
using System.IO;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Diagnostics;
using UnityEngine;

namespace CriWare {

public partial class CriProfiler
{
	private static CriProfiler singleton = new CriProfiler();
	public static CriProfiler GetSingleton() { return singleton; }

	#region Structs and Classes
	// TCP connection parameters to be send to the thread
	protected struct TcpParams {
		public readonly IPAddress ipAddress;
		public readonly int port;
		public readonly int chunkSize;
		public readonly int connectionTimeoutMillisec;
		public readonly int connectionRetryIntervalMillisec;
		public readonly int connectionRetryLimit;
		public readonly int enBufferIntervalMillisec;
		public TcpParams(
			IPAddress ipAddress,
			int port,
			int chunkSize,
			int connectionTimeoutMillisec,
			int connectionRetryIntervalMillisec,
			int connectionRetryLimit,
			int enBufferIntervalMillisec)
		{
			this.ipAddress = ipAddress;
			this.port = port;
			this.chunkSize = chunkSize;
			this.connectionTimeoutMillisec = connectionTimeoutMillisec;
			this.connectionRetryIntervalMillisec = connectionRetryIntervalMillisec;
			this.connectionRetryLimit = connectionRetryLimit;
			this.enBufferIntervalMillisec = enBufferIntervalMillisec;
		}
	}

	// TCP data buffer
	private class RingBuffer<T> {
		protected T[] data;
		protected int size;
		protected int head;
		protected int tail;
		protected int mask;   /* for remainder operation */

		static private int RegulateToPow2(uint n) {
			int res = 0;
			for (n -= 1; n != 0; n >>= 1) {
				res = (res << 1) + 1;
			}
			return res + 1;
		}

		// Specify the size of the buffer.
		// <para>The resulted size will be rounded up to the nearest 2^n number for the sake of performance.</para>
		public RingBuffer(int _size) {
			size = RegulateToPow2((uint)_size);
			data = new T[size];
			head = 0;
			tail = 0;
			mask = size - 1;
		}

		// Get the number of elements in the buffer
		public int Count {
			get {
				int _count = this.tail - this.head;
				if (_count < 0) _count += this.size;
				return _count;
			}
		}

		public int Capacity {
			get {
				return size - 1;
			}
		}

		// Index access to the buffer.
		public T this[int i] {
			get {
				return this.data[(i + this.head) & this.mask];
			}
			private set {
				this.data[(i + this.head) & this.mask] = value;
			}
		}

		// Copy [inputSize] of elements from inputData to the tail of the buffer.
		// return false if the buffer is full.
		public bool EnBuffer(T[] inputData, int inputSize) {
			if (this.Count + inputSize >= this.size) {
				return false;
			}

			for (int i = 0; i < inputSize; ++i) {
				this.data[this.tail] = inputData[i];
				this.tail = (this.tail + 1) & this.mask;
			}

			return true;
		}

		// Read [outputSize] of elements and delete it from the head of the buffer.
		// <para>Return null if there is nothing to read.</para>
		// <para>The resulted array may be smaller in length than requested.</para>
		public T[] DeBuffer(int outputSize) {
			if (outputSize <= 0 || this.Count <= 0) {
				return null;
			}
			if (outputSize > this.Count) {
				return null;
			}

			T[] res = new T[outputSize];
			for (int i = 0; i < outputSize; ++i) {
				res[i] = this[i];
			}
			this.head = (this.head + outputSize) & this.mask;

			return res;
		}

		// Clear the buffer. Memory spaces will still be occupied.
		public void Clear() {
			this.head = 0;
			this.tail = 0;
		}
	}

	private class RingBufferAutoDequeue<T> : RingBuffer<T> {
		public RingBufferAutoDequeue(int size) : base(size) { }

		public new bool EnBuffer(T[] inputData, int inputSize) {
			if (this.Count + inputSize >= this.size) {
				this.DeBuffer(this.Count + inputSize - this.size + 1);
			}

			for (int i = 0; i < inputSize; ++i) {
				this.data[this.tail] = inputData[i];
				this.tail = (this.tail + 1) & this.mask;
			}

			return true;
		}

		public void FillWithZeros() {
			T[] zeros = new T[Capacity];
			this.EnBuffer(zeros, Capacity);
		}
	}

	#region Loading / Saving data from big-endian data stream
	public static Byte LoadBigEndianByte(Byte[] data, int ofst) {
		return data[ofst];
	}
	public static void SaveBigEndianByte(Byte[] data, int ofst, Byte input) {
		data[ofst] = input;
	}

	public static UInt16 LoadBigEndianUInt16(Byte[] data, int ofst) {
		return (UInt16)( ((data[ofst + 0]) << 8)
					   |  (data[ofst + 1]));
	}
	public static void SaveBigEndianUInt16(Byte[] data, int ofst, UInt16 input) {
		data[ofst + 0] = (byte)((input >> 8) & 0xff);
		data[ofst + 1] = (byte)( input       & 0xff);
	}

	public static UInt32 LoadBigEndianUInt32(Byte[] data, int ofst) {
		return (UInt32)( ((data[ofst + 0]) << 24)
					   | ((data[ofst + 1]) << 16)
					   | ((data[ofst + 2]) << 8)
					   |  (data[ofst + 3]));
	}
	public static void SaveBigEndianUInt32(Byte[] data, int ofst, UInt32 input) {
		data[ofst + 0] = (byte)((input >> 24) & 0xff);
		data[ofst + 1] = (byte)((input >> 16) & 0xff);
		data[ofst + 2] = (byte)((input >> 8)  & 0xff);
		data[ofst + 3] = (byte)( input        & 0xff);
	}

	public static Single LoadBigEndianSingle(Byte[] data, int ofst) {
		/* convert to little-endian for System.BitConverter */
		byte[] byteVals = new[] { data[ofst + 3], data[ofst + 2], data[ofst + 1], data[ofst + 0] };
		return BitConverter.ToSingle(byteVals, 0);
	}

	public static UInt64 LoadBigEndianUInt64(Byte[] data, int ofst) {
		return (UInt64)( ((data[ofst + 0]) << 56)
					   | ((data[ofst + 1]) << 48)
					   | ((data[ofst + 2]) << 40)
					   | ((data[ofst + 3]) << 32)
					   | ((data[ofst + 4]) << 24)
					   | ((data[ofst + 5]) << 16)
					   | ((data[ofst + 6]) << 8)
					   |  (data[ofst + 7]));
	}
	public static void SaveBigEndianUInt64(Byte[] data, int ofst, UInt64 input) {
		data[ofst + 0] = (byte)((input >> 56) & 0xff);
		data[ofst + 1] = (byte)((input >> 48) & 0xff);
		data[ofst + 2] = (byte)((input >> 40) & 0xff);
		data[ofst + 3] = (byte)((input >> 32) & 0xff);
		data[ofst + 4] = (byte)((input >> 24) & 0xff);
		data[ofst + 5] = (byte)((input >> 16) & 0xff);
		data[ofst + 6] = (byte)((input >> 8)  & 0xff);
		data[ofst + 7] = (byte)( input        & 0xff);
	}
	#endregion

	protected struct TcpLogHeader {
		public const int properSize
			  /* = sizeof(UInt32) + sizeof(UInt16) + sizeof(Byte) + sizeof(Byte) + sizeof(UInt64)
			   * + sizeof(UInt16) + sizeof(UInt16) + sizeof(UInt32) + sizeof(UInt64);
			   */ = 32;
		public readonly UInt32 packetSize;
		public readonly UInt16 command;
		public readonly Byte ptr_size;
		public readonly Byte log_type;
		public readonly UInt64 time;
		public readonly UInt16 function_id;
		public readonly UInt16 padding_size;
		public readonly UInt32 control_id;
		public readonly UInt64 thread_id;
		public readonly bool isPtr64;
		public readonly int size;

		public TcpLogHeader(byte[] data) {
			int offset = 0;
			isPtr64 = false;

			packetSize = LoadBigEndianUInt32(data, offset);
			offset += sizeof(UInt32);

			command = LoadBigEndianUInt16(data, offset);
			offset += sizeof(UInt16);

			ptr_size = LoadBigEndianByte(data, offset);
			isPtr64 = ((ptr_size - sizeof(UInt32)) == 0) ? false : true;
			offset += sizeof(Byte);

			log_type = LoadBigEndianByte(data, offset);
			offset += sizeof(Byte);

			time = LoadBigEndianUInt64(data, offset);
			offset += sizeof(UInt64);

			function_id = LoadBigEndianUInt16(data, offset);
			offset += sizeof(UInt16);

			padding_size = LoadBigEndianUInt16(data, offset);
			offset += sizeof(UInt16);

			control_id = LoadBigEndianUInt32(data, offset);
			offset += sizeof(UInt32);

			if(isPtr64 == true) {
				thread_id = LoadBigEndianUInt64(data, offset);
				offset += sizeof(UInt64);
			} else {
				offset += sizeof(UInt32);
				thread_id = LoadBigEndianUInt32(data, offset);
				offset += sizeof(UInt32);
			}

			size = offset;
            if (size != properSize) {
                UnityEngine.Debug.LogError("[CRIWARE] (Profiler) Corrupted log header");
            }
		}
	}

	private class ParamIdDoesNotMatchException : Exception {
		public ParamIdDoesNotMatchException() { }

		public ParamIdDoesNotMatchException(string message) : base(message) { }

		public ParamIdDoesNotMatchException(string message, Exception inner) : base(message, inner) { }
	}

	public class PlaybackInfo {
		public readonly UInt32 playbackId;
		public readonly String cueName;
		public readonly String cueSheetName;
		public readonly UInt64 timestamp;
		public int? StreamType;
		public PlaybackInfo(UInt32 playbackId, String cueName, String cueSheetName, UInt64 timestamp)
		{
			this.playbackId = playbackId;
			this.cueName = cueName;
			this.cueSheetName = cueSheetName;
			this.timestamp = timestamp;
			this.StreamType = null;
		}
	}

	public class VoicePoolInfo {
		public UInt32 soundFormat;
		public int numUsedVoices;
		public int? StreamType;
		public VoicePoolInfo(UInt32 soundFormat, int? StreamType = null)
		{
			this.soundFormat = soundFormat;
			this.StreamType = StreamType;
			this.numUsedVoices = -1;    /* <0: no data yet */
		}
	}

	public struct LevelInfo {
		public Single levelPeak;
		public Single levelRms;
		public Single levelPeakHold;
		public LevelInfo(Single levelPeak, Single levelRms, Single levelPeakHold)
		{
			this.levelPeak = levelPeak;
			this.levelRms = levelRms;
			this.levelPeakHold = levelPeakHold;
		}
	}
	#endregion

	#region Internal data
	private Thread threadTcpClient;
	private Thread threadPacketReading;
	private ManualResetEvent threadTerminator = new ManualResetEvent(false);
	private RingBuffer<byte> TcpBuffer = new RingBuffer<byte>(TCP_CHUNK * 2);
	private List<byte[]> eventLogList = new List<byte[]>();
	private bool isProfiling = false;
	private bool isConnected = false;
	private bool isBufferCorrupted = false;
	private bool doSaveLog;
	private FileStream logStream = null;
	private const int cRwLockTimeoutMs = 100;
#if CRI_PROFILER_PERF_DEBUG
	private int totalReadPacket = 0;
	private DateTime lastReadCheck = DateTime.Now;
#endif

	public bool IsProfiling {
		get { return isProfiling; }
	}
#endregion

#region Fields / Properties
	public UInt64 timestamp { get; private set; }
    public UInt64 timestampStart { get; private set; }
	public Single cpu_load { get; private set; }
	private RingBufferAutoDequeue<Single> cpuLoadHistory = new RingBufferAutoDequeue<float>(100);
	public Single[] CpuLoadHistory {
		get {
			lock (cpuLoadHistory) {
				Single[] histArray = new Single[cpuLoadHistory.Count];
				for (int i = 0; i < cpuLoadHistory.Count; ++i) {
					histArray[i] = cpuLoadHistory[i];
				}
				return histArray;
			}
		}
	}
	public UInt32 num_used_voices { get; private set; }
	private RingBufferAutoDequeue<UInt32> voiceUsageHistory = new RingBufferAutoDequeue<UInt32>(100);
	public UInt32[] VoiceUsageHistory {
		get {
			lock (voiceUsageHistory) {
				UInt32[] histArray = new UInt32[voiceUsageHistory.Count];
				for (int i = 0; i < voiceUsageHistory.Count; ++i) {
					histArray[i] = voiceUsageHistory[i];
				}
				return histArray;
			}
		}
	}
	public UInt32 num_used_streams { get; private set; }
	public Single total_bps { get; private set; }
	public UInt32 maxVoice_stdOnMemory { get; private set; }
	public UInt32 maxVoice_stdStreaming { get; private set; }
	public UInt32 maxVoice_hcamxOnMemory { get; private set; }
	public UInt32 maxVoice_hcamxStreaming { get; private set; }
	public Single loudness_momentary { get; private set; }
	public Single loudness_shortTerm { get; private set; }
	public Single loudness_integrated { get; private set; }
	public int numChMasterOut { get; private set; }
	private List<LevelInfo> levels = new List<LevelInfo>();     /* Channel => Levels */
	public LevelInfo[] LevelsAllCh {
		get {
			if(levels.Count > 0) {
				LevelInfo[] levelsCopy;
				lock (levels) {
					levelsCopy = new LevelInfo[levels.Count];
					levels.CopyTo(levelsCopy);
				}
				return levelsCopy;
			} else {
				return null;
			}
		}
	}
	public String cuename_lastPlayed { get; private set; }
    public String cuesheetName_lastPlayed { get; private set; }
	private Hashtable playbackHashtable = new Hashtable();      /* Playback ID => Playback Info */
	public PlaybackInfo[] PlaybackList {
		get {
			lock (playbackHashtable) {
				PlaybackInfo[] playbackList = new PlaybackInfo[playbackHashtable.Count];
				playbackHashtable.Values.CopyTo(playbackList, 0);
				return playbackList.OrderBy(x => x.timestamp).ToArray();
			}
		}
	}
	private Dictionary<string, CueSheetGroup> multiLaneTimeline = new Dictionary<string, CueSheetGroup>();
	private Hashtable voicePoolInfoTable = new Hashtable();     /* VoicePool Handle => VoicePool Info */
	public enum VoicePoolFormat {
		Standard,
		HcaMx
	}
	public int GetVoicePoolUsage(VoicePoolFormat format) {
		int usage = 0;
		bool haveData = false;
		UInt32 formatBits = FORMAT_NONE;
		switch (format) {
			case VoicePoolFormat.Standard:
				formatBits = FORMAT_ADX | FORMAT_HCA;
				break;
			case VoicePoolFormat.HcaMx:
				formatBits = FORMAT_HCA_MX;
				break;
			default:
				break;
		}
		lock (voicePoolInfoTable) {
			foreach(VoicePoolInfo elem in voicePoolInfoTable.Values) {
				if (elem.soundFormat == formatBits && elem.numUsedVoices >= 0) {
					usage += (int)elem.numUsedVoices;
					haveData = true;
				}
			}
		}
		if(haveData) {
			return usage;
		} else {
			return -1;
		}
	}
	public List<byte[]> GetLogList() {
		return eventLogList;
	}
	public List<byte[]> GetLogList(int offset, int count) {
		try {
			offset = Mathf.Clamp(offset, 0, eventLogList.Count - 1);
			count = Mathf.Clamp(count, 0, eventLogList.Count - offset);
			return eventLogList.GetRange(offset, count);
		} catch {
			return new List<byte[]>();
		}
	}
	public void ClearLog() {
		eventLogList.Clear();
	}
#endregion Fields / Properties

#region Data Initialization
	private void ResetVals() {
		this.InitVals();
	}

	private void InitVals() {
		this.timestamp = 0;
		this.timestampStart = 0;
		this.cpu_load = 0;
		this.cpuLoadHistory.FillWithZeros();
		this.num_used_voices = 0;
		this.voiceUsageHistory.FillWithZeros();
		this.num_used_streams = 0;
		this.total_bps = 0.0f;
		this.maxVoice_stdOnMemory = 0;
		this.maxVoice_stdStreaming = 0;
		this.maxVoice_hcamxOnMemory = 0;
		this.maxVoice_hcamxStreaming = 0;
		this.loudness_momentary = Mathf.NegativeInfinity;
		this.loudness_shortTerm = Mathf.NegativeInfinity;
		this.loudness_integrated = Mathf.NegativeInfinity;
		this.cuename_lastPlayed = "";
		this.numChMasterOut = 0;
		this.voicePoolInfoTable.Clear();
		this.playbackHashtable.Clear();
		this.multiLaneTimeline.Clear();
		this.levels.Clear();
		this.eventLogList.Clear();
	}
#endregion

#region Profiler Control
	/* connection settings */
	private const int TCP_PORT = 2002;
	private const int TCP_CHUNK = 1048576;
	private const int TCP_CONNECT_TIMEOUT_MS = 500;
	private const int TCP_RETRY_INTERVAL_MS = 2000;
	private const int TCP_RETRY = 5;
	private const int BUFFERING_INTERVAL_MS = 10;
	public const string LOG_FILE_EXTENSION = "crimon";

	public string ipAddressString = "127.0.0.1";
	public string LogFileSavePath { get; private set; }
	// Starting the TCP sub thread.
	public void StartProfiling(bool saveLog, string logDirPath = null) {
		IPAddress validIp;
		if(IPAddress.TryParse(ipAddressString, out validIp) == false) {
			UnityEngine.Debug.LogWarning("[CRIWARE] (Profiler) IP address not valid - connection aborted. ");
			return;
		}

		this.doSaveLog = saveLog;
		if (this.doSaveLog) {
			if (string.IsNullOrEmpty(logDirPath)) {
				/* default log file path */
				logDirPath = Path.Combine(Directory.GetParent(Application.dataPath).ToString(), "CriWareProfilerLog");
			}
			this.LogFileSavePath = logDirPath;
			if (Directory.Exists(LogFileSavePath) == false) {
				try {
					Directory.CreateDirectory(LogFileSavePath);
				} catch (Exception e) {
					UnityEngine.Debug.LogError("[CRIWARE] Profiler: failed to create directory for log files. Msg: " + e.Message);
				}
			}
			var filePath = Path.Combine(LogFileSavePath, "CriProfilerLog-" + DateTime.Now.ToString("yyyy-MM-dd-HHmmss") + "." + LOG_FILE_EXTENSION);
			try {
				this.logStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
			} catch (Exception e) {
				UnityEngine.Debug.LogError("[CRIWARE] Profiler: failed to create log file. Msg: " + e.Message);
				this.logStream = null;
				this.doSaveLog = false;
			}
		}

		TcpParams tcpParams = new TcpParams(
			validIp,
			TCP_PORT,
			TCP_CHUNK,
			TCP_CONNECT_TIMEOUT_MS,
			TCP_RETRY_INTERVAL_MS,
			TCP_RETRY,
			BUFFERING_INTERVAL_MS);
		const int READ_INTERVAL_MS = 0;

		if ((threadTcpClient == null || threadTcpClient.IsAlive == false) &&
			(threadPacketReading == null || threadPacketReading.IsAlive == false)
			) {
			InitVals();
			threadTerminator.Reset();
			threadTcpClient = new Thread(() => TaskTcpClient(tcpParams));
			threadPacketReading = new Thread(() => TaskPacketReading(READ_INTERVAL_MS));
			threadTcpClient.Start();
			threadPacketReading.Start();
		}
	}

	// Stopping the TCP sub thread.
	public void StopProfiling() {
		threadTerminator.Set();
		ResetVals();

		if (this.logStream != null) {
			this.doSaveLog = false;
			try {
				this.logStream.Flush();
				this.logStream.Close();
				this.logStream = null;
			} catch (Exception e) {
				UnityEngine.Debug.LogError("[CRIWARE] Profiler: failed to flush log file. Msg: " + e.Message);
			}
		}
	}

	private CriProfiler() {
		/* hide constructor for singleton */
		InitVals();
	}

	~CriProfiler() {
		this.StopProfiling();
	}
#endregion

#region TCP Connection Management
	// TCP connection sub thread task.
	private void TaskTcpClient(TcpParams tcpParams) {
		int failedConnectCnt = 0;

		this.isProfiling = true;

		using (var tcpClient = new TcpClient()) {
			while (true) {
				try {
					tcpClient.Connect(tcpParams.ipAddress, tcpParams.port);
					this.isConnected = true;
					UnityEngine.Debug.Log("[CRIWARE] CRI Profiler connected.");
					BufferingLoop(tcpClient, tcpParams);
				} catch {
					failedConnectCnt += 1;
					if(failedConnectCnt > tcpParams.connectionRetryLimit) {
						UnityEngine.Debug.LogWarning("[CRIWARE] Retry count exceeded limit(" + tcpParams.connectionRetryLimit + "); Stopped.");
						threadTerminator.Set();
					} else {
						UnityEngine.Debug.LogWarning("[CRIWARE] Unable to connect CRI profiler. (Check if \"Uses In Game Preview\" is turned on)"
							+"\nWaiting for " + (tcpParams.connectionRetryIntervalMillisec / 1000.0f).ToString("F1") + " seconds to reconnect... (" + failedConnectCnt+ "/" + tcpParams.connectionRetryLimit + " tries)");
					}
				}
				this.isConnected = false;

				if (threadTerminator.WaitOne(tcpParams.connectionRetryIntervalMillisec)) {
					UnityEngine.Debug.Log("[CRIWARE] CRI Profiler disconnected.");
					this.isProfiling = false;
					break;
				}
			}
		}
	}

	// Looping process to push the socket input to the buffer.
	private void BufferingLoop(TcpClient tcpClient, TcpParams tcpParams) {
		int numBytesRead = 0;
		byte[] chunk = new byte[tcpParams.chunkSize];
		byte[] sendPacket;
		bool enbufferRes = true;

		this.TcpBuffer.Clear();
		using (var netStream = tcpClient.GetStream()) {
			if (netStream == null) {
				UnityEngine.Debug.LogError("[CRIWARE] (Profiler) TCP connection not available.");
				return;
			}

			if (netStream.CanRead == false) {
				UnityEngine.Debug.LogError("[CRIWARE] (Profiler) TCP connection not readable.");
				return;
			}

			sendPacket = MakeLogPacket4StartStopLog(SendPacketType.StartLog);
			if (sendPacket != null && netStream.CanWrite) {
				netStream.Write(sendPacket, 0, sendPacket.Length);
			}

			sendPacket = MakeLogPacket4StartStopLog(SendPacketType.SendLogVersion);
			if (sendPacket != null && netStream.CanWrite) {
				netStream.Write(sendPacket, 0, sendPacket.Length);
			}

			while (true) {
				try {
					if (netStream.DataAvailable) {
						numBytesRead = netStream.Read(chunk, 0, chunk.Length);
#if CRI_PROFILER_PERF_DEBUG
						UnityEngine.Debug.Log($"{numBytesRead} bytes read");
						totalReadPacket+= numBytesRead;
#endif
						if (this.isBufferCorrupted == false) {
							lock (this.TcpBuffer) {
								enbufferRes = this.TcpBuffer.EnBuffer(chunk, numBytesRead);
							}
						}
						if (enbufferRes == false) {
							UnityEngine.Debug.LogWarning("[CRIWARE] TCP reading buffer overflowed.");
#if CRI_PROFILER_PERF_DEBUG
							UnityEngine.Debug.LogWarning($"Failed to buffer {totalReadPacket} bytes; Buffer Usage: {this.TcpBuffer.Count}/{this.TcpBuffer.Capacity}");
							totalReadPacket = 0;
#endif
							
							// -- buffer corrupted -> prepare to reset --
							// Enbuffer failure may cause incomplete packets in the packet queue.
							// The solution we take is skipping all incoming data until a "tail" chunk (followed by no data) arrives,
							// right after which the buffer will be reset.
							this.isBufferCorrupted = true;
						}
					} else {
						if (this.isBufferCorrupted == true) {
							lock (this.TcpBuffer) {
								/* reset the corrupted buffer when all available data are received */
								this.TcpBuffer.Clear();
							}
							this.isBufferCorrupted = false;
						}
					}
				} catch (System.IO.IOException) {
					threadTerminator.Set();
				}

				if (threadTerminator.WaitOne(tcpParams.enBufferIntervalMillisec)) {
					break;
				}
#if CRI_PROFILER_PERF_DEBUG
				var now = DateTime.Now;
				if (now-lastReadCheck > new TimeSpan(0,0,0,5,0)) {
					lastReadCheck = now;
					UnityEngine.Debug.Log($"{totalReadPacket} bytes/5 sec; Buffer Usage: {this.TcpBuffer.Count}/{this.TcpBuffer.Capacity}");
					totalReadPacket = 0;
				}
#endif
			}

			sendPacket = MakeLogPacket4StartStopLog(SendPacketType.StopLog);
			if (sendPacket != null && netStream.CanWrite) {
				netStream.Write(sendPacket, 0, sendPacket.Length);
			}
		}
	}

	// Sending control packet to start / stop receiving log data
	private enum SendPacketType {
		StartLog,
		SendLogVersion,
		StopLog
	}
	private byte[] MakeLogPacket4StartStopLog(SendPacketType sendType) {
		int ptr_size = IntPtr.Size;
		const LogTypes log_type = LogTypes.NON;
		const UInt64 time = 0;
		const LogFuncId function_id = LogFuncId.LOG_COMMAND_Non;
		const UInt64 thread_id = 0;

		int size;
		TcpCommandId command;
		int padding_size;
		UInt32 control_id;

		switch (sendType) {
			case SendPacketType.StartLog:
				size = TcpLogHeader.properSize
					   /* + sizeof(UInt16) // Param ID
						* + CriProfiler.LogParams[(int)LogParamItems.LOG_STRINGS_ITEM_LogRecordMode].size32/64;
						*/ + 6;
				command = TcpCommandId.CRITCP_MAIL_START_LOG_RECORD;
				control_id = 0;
				padding_size = 2; /* 8 - (38 mod 8) */
				size += padding_size;
				break;
			case SendPacketType.StopLog:
				size = TcpLogHeader.properSize;
				command = TcpCommandId.CRITCP_MAIL_STOP_LOG_RECORD;
				control_id = 0;
				padding_size = 0; /* 8 - (32 mod 8) */
				size += padding_size;
				break;
			case SendPacketType.SendLogVersion:
				size = TcpLogHeader.properSize;
				command = TcpCommandId.CRITCP_MAIL_CONNECTION_VERSION;
				control_id = CriProfiler.CRI_ATOM_PREVIEW_CONNECTION_VERSION;
				padding_size = 0; /* 8 - (32 mod 8) */
				size += padding_size;
				break;
			default:
				return null;
		}

		byte[] packetBytes = new byte[size];
		int offset = 0;

		SaveBigEndianUInt32(packetBytes, offset, (UInt32)size);
		offset += sizeof(UInt32);
		SaveBigEndianUInt16(packetBytes, offset, (UInt16)command);
		offset += sizeof(UInt16);
		SaveBigEndianByte(packetBytes, offset, (Byte)ptr_size);
		offset += sizeof(Byte);
		SaveBigEndianByte(packetBytes, offset, (Byte)log_type);
		offset += sizeof(Byte);
		SaveBigEndianUInt64(packetBytes, offset, (UInt64)time);
		offset += sizeof(UInt64);
		SaveBigEndianUInt16(packetBytes, offset, (UInt16)(function_id + CriProfiler.logFuncBaseNum));
		offset += sizeof(UInt16);
		SaveBigEndianUInt16(packetBytes, offset, (UInt16)padding_size);
		offset += sizeof(UInt16);
		SaveBigEndianUInt32(packetBytes, offset, (UInt32)control_id);
		offset += sizeof(UInt32);
		SaveBigEndianUInt64(packetBytes, offset, (UInt64)thread_id);
		offset += sizeof(UInt64);

		switch (sendType) {
			case SendPacketType.StartLog:
				SaveBigEndianUInt16(packetBytes, offset, (UInt16)LogParamId.LOG_STRINGS_ITEM_LogRecordMode);
				offset += sizeof(UInt16);
				SaveBigEndianUInt32(packetBytes, offset, (UInt32)LOG_MODE_ALL); /* size of LOG_STRINGS_ITEM_LogRecordMode */
				offset += sizeof(UInt32);
				break;
			case SendPacketType.StopLog:
				break;
			case SendPacketType.SendLogVersion:
				break;
			default:
				return null;
		}

		for (int i = 0; i < padding_size; ++i) {
			packetBytes[offset] = 0;
			offset += 1;
		}

		if (offset == size) {
			return packetBytes;
		} else {
			return null;
		}
	}
#endregion

#region Parser
	// Sub thread task to extract TCP packets from the buffer.
	private void TaskPacketReading(int readIntervalMillisec) {
		Stopwatch stopwatch = new Stopwatch();
		const int threadMaxLifeMs = 20000;  /* should be longer than total TCP retrying time */
		const int threadRetryLifeMs = 500;

		byte[] data = null;
		TcpLogHeader packetHeader;
		int packetSize = 0;

		stopwatch.Start();
		while (true) {
			if (this.isBufferCorrupted == false) {
				lock (this.TcpBuffer) {
					packetSize = GetPacketSize(TcpBuffer);
					data = this.TcpBuffer.DeBuffer(packetSize);
				}
				if (data != null) {
					stopwatch.Reset();
					stopwatch.Start();
					packetHeader = new TcpLogHeader(data);
					try {
						if (packetHeader.size > 0) {
							if (this.doSaveLog && this.logStream != null) {
								try {
									this.logStream.Write(data, 0, data.Length);
								} catch (Exception e) {
									UnityEngine.Debug.LogError("[CRIWARE] Profiler: failed to write logs. Msg: " + e.Message);
									this.doSaveLog = false;
								}
							}
							this.Parser(data, packetHeader);
						} /* if headerSize > 0 */
					} catch (Exception ex) { /* Any parsing error */
						if(ex is ParamIdDoesNotMatchException) {
							UnityEngine.Debug.LogWarning("[CRIWARE] " + ex.Message.ToString());
						}
						UnityEngine.Debug.LogWarning("[CRIWARE] (Profiler) Packet parsing failed: buffer may be corrupted. Reconstructing buffer..."
							+"\nLog Function: " + GetLogFuncId(packetHeader.function_id).ToString());
						this.isBufferCorrupted = true;
					}
				} else {
					/* kill all threads if waited for too long */
					if (this.isConnected == true) {
						if (stopwatch.ElapsedMilliseconds > threadRetryLifeMs) {
							this.isProfiling = false;
							threadTerminator.Set();
						}
					} else {
						if (stopwatch.ElapsedMilliseconds > threadMaxLifeMs) {
							this.isProfiling = false;
							threadTerminator.Set();
						}
					}
				} /* if data != null */
			} /* if isBufferCorrupted == false */

			if (threadTerminator.WaitOne(readIntervalMillisec)) {
				this.isProfiling = false;
				break;
			}
		}
	}

	// Get the size of the packet from the header.
	// Return 0 if there is no enough data to determine the size.
	private int GetPacketSize(RingBuffer<byte> buffer) {
		if (buffer == null || buffer.Count < DATA_LENGTH_PARAM_SIZE) {
			return 0;
		}

		uint result = buffer[0];
		for (int i = 1; i < DATA_LENGTH_PARAM_SIZE; ++i) {
			result <<= 8;
			result += buffer[i];
		}

		return (int)result;
	}

	private int GetPacketSize(byte[] data, uint offset = 0) {
		if (data == null || data.Length - offset < DATA_LENGTH_PARAM_SIZE) {
			return 0;
		}

		uint result = data[offset];
		for (int i = 0; i < DATA_LENGTH_PARAM_SIZE; i++) {
			result <<= 8;
			result += data[i + offset];
		}

		return (int)result;
	}

	public void LoadLogFromFile(string path) {
		if (string.IsNullOrEmpty(path)) {
			UnityEngine.Debug.LogError("[CRIWARE] Profiler: Log file path cannot be empty");
		}

		try {
			using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)) {
				if (fileStream.CanRead) {
					eventLogList.Clear();
					byte[] reusedSizeData = new byte[DATA_LENGTH_PARAM_SIZE];
					while (fileStream.Position + DATA_LENGTH_PARAM_SIZE < fileStream.Length) {
						var read = fileStream.Read(reusedSizeData, 0, DATA_LENGTH_PARAM_SIZE);
						/* roll back read position */
						fileStream.Position -= read;
						/* get actual data size */
						var dataSize = GetPacketSize(reusedSizeData);
						if (dataSize > 0) {
							byte[] data = new byte[dataSize];
							fileStream.Read(data, 0, dataSize);
							this.LogParser(data);
						} else {
							UnityEngine.Debug.LogError("[CRIWARE] Profiler: Error while reading log");
							break;
						}
					}
				} else {
					UnityEngine.Debug.LogError("[CRIWARE] Profiler: Log file cannot be read");
				}
			}
		} catch (Exception e) {
			UnityEngine.Debug.LogError("[CRIWARE] Profiler: Error while opening log: " + e.Message);
		}
	}

	private void LogParser(byte[] data) {
		var header = new TcpLogHeader(data);

		switch ((TcpCommandId)header.command) {
			case TcpCommandId.CRITCP_MAIL_SEND_LOG:
				switch (GetLogFuncId(header.function_id)) {
					case LogFuncId.LOG_COMMAND_ExPlaybackId:
					case LogFuncId.LOG_COMMAND_ExPlaybackInfo_FreeInfo:
						this.AddEventLog(data);
						break;
					default:
						break;
				}
				break;
			default:
				break;
		}
	}

	private void Parser(byte[] data, TcpLogHeader header) {
		int offset = 0;
		int offset2 = 0;
		int dataSize = 0;
		int dataSize2 = 0;

		UInt32 playbackId;
		StreamTypes streamType;
		VoicePoolInfo voicePoolInfo;
		UInt64 voicePoolHn;
		Single levelPeak;
		Single levelRms;
		Single levelPeakHold;
		int numCh = 0;
		int numOutputCh;
		int numMaxCh = 0;
		uint rack_id = 0;

		switch ((TcpCommandId)header.command) {
			case TcpCommandId.CRITCP_MAIL_CONNECTION_VERSION:
				this.timestamp = header.time;
				if (this.timestampStart == 0) {
					this.timestampStart = this.timestamp;
				}
				if (header.control_id != CRI_ATOM_PREVIEW_CONNECTION_VERSION) {
					UnityEngine.Debug.Log("[CRIWARE] Profiler internal: Connection version does not match. Exiting profiling.");
					this.StopProfiling();
				}
				break;
			case TcpCommandId.CRITCP_MAIL_EXIT_TO_PROFILER:
			case TcpCommandId.CRITCP_MAIL_EXIT:
				this.StopProfiling();
				break;
			case TcpCommandId.CRITCP_MAIL_SEND_LOG:
				this.timestamp = header.time;
				if (this.timestampStart == 0) {
					this.timestampStart = this.timestamp;
				}
				switch (GetLogFuncId(header.function_id)) {
					case LogFuncId.LOG_COMMAND_CpuLoadAndNumUsedVoices:
						this.cpu_load = LoadBigEndianSingle(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_CpuLoad, out offset));
						this.num_used_voices = LoadBigEndianUInt32(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumUsedVoices, ref offset));
						lock (cpuLoadHistory) {
							cpuLoadHistory.EnBuffer(new Single[] { cpu_load }, 1);
						}
						lock (voiceUsageHistory) {
							voiceUsageHistory.EnBuffer(new UInt32[] { num_used_voices }, 1);
						}
						break;
					case LogFuncId.LOG_COMMAND_LoudnessInfoWithRackId:
						rack_id = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_RackId, out offset));
						if (rack_id == 0) {
							goto case LogFuncId.LOG_COMMAND_LoudnessInfo;
						}
						break;
					case LogFuncId.LOG_COMMAND_LoudnessInfo:
						this.loudness_momentary = LoadBigEndianSingle(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_MomentaryValue, out offset));
						this.loudness_shortTerm = LoadBigEndianSingle(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ShortTermValue, ref offset));
						this.loudness_integrated = LoadBigEndianSingle(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_IntegratedValue, ref offset));
						break;
					case LogFuncId.LOG_COMMAND_StreamingInfo:
						this.num_used_streams = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumUsedVoices, out offset));
						this.total_bps = LoadBigEndianSingle(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_TotalBps, ref offset));
						break;
					case LogFuncId.LOG_COMMAND_AsrBusAnalyzeInfoAllChWithRackId:
						rack_id = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_RackId, out offset));
						if (rack_id == 0) {
							goto case LogFuncId.LOG_COMMAND_AsrBusAnalyzeInfoAllCh;
						}
						break;
					case LogFuncId.LOG_COMMAND_AsrBusAnalyzeInfoAllCh:
						if (LoadBigEndianByte(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_BusNo, out offset)) == 0) {    /* if the bus is MasterOut(bus0) */
							numCh = LoadBigEndianByte(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumCh, out offset));
							lock (levels) {
								if (numMaxCh == 0) {
									numMaxCh = numCh;
									levels.Clear();
									for (int i = 0; i < numMaxCh; ++i) {
										levels.Add(new LevelInfo(Mathf.NegativeInfinity, Mathf.NegativeInfinity, Mathf.NegativeInfinity));
									}
								}
								if (numMaxCh == numCh) {
									for (int i = 0; i < numCh; ++i) {
										levelPeak = LoadBigEndianSingle(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_PeakLevel, ref offset));
										levelRms = LoadBigEndianSingle(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_RmsLevel, ref offset));
										levelPeakHold = LoadBigEndianSingle(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_PeakHoldLevel, ref offset));
										levels[i] = new LevelInfo(levelPeak, levelRms, levelPeakHold);
									}
								}
							}
						}
						break;
					case LogFuncId.LOG_COMMAND_ExStandardVoicePoolConfig:
						if (Convert.ToBoolean(LoadBigEndianByte(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_StreamingFlag, out offset))) == false) {
							this.maxVoice_stdOnMemory = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumVoices, out offset));
						} else {
							this.maxVoice_stdStreaming = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumVoices, out offset));
						}
						break;
					case LogFuncId.LOG_COMMAND_ExHcaMxVoicePoolConfig:
						if (Convert.ToBoolean(LoadBigEndianByte(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_StreamingFlag, out offset))) == false) {
							this.maxVoice_hcamxOnMemory = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumVoices, out offset));
						} else {
							this.maxVoice_hcamxStreaming = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumVoices, out offset));
						}
						break;
					case LogFuncId.LOG_COMMAND_ExVoicePoolHn:
						try {
							voicePoolHn = LoadBigEndianUInt64(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExVoicePoolHn, out offset));
							voicePoolInfo = new VoicePoolInfo(LoadBigEndianUInt32(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_SoundFormat, ref offset)), null);
							lock (voicePoolInfoTable) {
								voicePoolInfoTable.Add(voicePoolHn, voicePoolInfo);
							}
						} catch (ArgumentException) {
							/* Voice pool info already exists; Do nothing */
						}
						break;
					case LogFuncId.LOG_COMMAND_ExVoicePool_Free:
						voicePoolHn = LoadBigEndianUInt64(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExVoicePoolHn, out offset));
						lock (voicePoolInfoTable) {
							voicePoolInfoTable.Remove(voicePoolHn);
						}
						break;
					case LogFuncId.LOG_COMMAND_PlayerPool_NumVoices:
						voicePoolHn = LoadBigEndianUInt64(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExVoicePoolHn, out offset));
						lock (voicePoolInfoTable) {
							if (voicePoolInfoTable[voicePoolHn] != null) {
								(voicePoolInfoTable[voicePoolHn] as VoicePoolInfo).numUsedVoices = (int)LoadBigEndianUInt32(data, GetNextParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_NumUsedVoices, ref offset));
							}
						}
						break;
					case LogFuncId.LOG_COMMAND_ExPlaybackId:
						playbackId = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExPlaybackId, out offset));
						dataSize = LoadBigEndianUInt16(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_CueName, out offset)); /* cue name */
                        dataSize2 = LoadBigEndianUInt16(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExAcbName, out offset2)); /* cuesheet name */
						if (dataSize > 1 && dataSize2 > 1) {
							this.cuename_lastPlayed = System.Text.Encoding.Default.GetString(data, GetStrDataOffset(header, offset), dataSize).TrimEnd('\0');
                            this.cuesheetName_lastPlayed = System.Text.Encoding.Default.GetString(data, GetStrDataOffset(header, offset2), dataSize2).TrimEnd('\0');
							try {
								lock (playbackHashtable) {
									this.playbackHashtable.Add(playbackId, new PlaybackInfo(playbackId, cuename_lastPlayed, cuesheetName_lastPlayed, header.time));
								}
							} catch (ArgumentException) {
								/* Duplicated playback info received; Do nothing */
							}
							this.AddEventLog(data);
							this.AddPlaybackToTimeline(new Playback(playbackId, header.time, true, cuename_lastPlayed, cuesheetName_lastPlayed));
						} else {
							this.cuename_lastPlayed = "";
						}
						break;
					case LogFuncId.LOG_COMMAND_SoundVoice_Allocate:
						playbackId = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExPlaybackId, out offset));
						streamType = (StreamTypes)LoadBigEndianByte(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_StreamType, out offset));
						lock (playbackHashtable) {
							if (playbackHashtable.ContainsKey(playbackId)) {
								(playbackHashtable[playbackId] as PlaybackInfo).StreamType = (int)streamType;
							}
						}
						break;
					case LogFuncId.LOG_COMMAND_ExPlaybackInfo_FreeInfo:
						playbackId = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExPlaybackId, out offset));
						lock (playbackHashtable) {
							if (playbackHashtable.ContainsKey(playbackId)) {
								var info = playbackHashtable[playbackId] as PlaybackInfo;
								this.AddEventLog(data);
								this.AddPlaybackToTimeline(new Playback(playbackId, header.time, false, info.cueName, info.cueSheetName));
								playbackHashtable.Remove(playbackId);
							}
						}
						break;
					case LogFuncId.LOG_COMMAND_ExAsrConfig:
						numOutputCh = LoadBigEndianByte(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_OutputChannels, out offset));
						if (numOutputCh > this.numChMasterOut) {
							numChMasterOut = numOutputCh;
						}
						break;
					default:
						break;
				}
				break;
			default:
				UserDefinedParser(data, header);
				break;
		}
	}

	// Override this method to add new parsing procedure
	protected virtual void UserDefinedParser(byte[] data, TcpLogHeader packetHeader) {
		// e.g.
		//int offset;
		//switch ((TcpCommandId)packetHeader.command) {
		//	case TcpCommandId.<CRITCP_MAIL_xxx>:
		//		switch (GetLogFuncId(packetHeader.function_id)) {
		//			case LogFuncId.<LOG_COMMAND_xxx>:
		//				<parsing>
		//				break;
		//			default:
		//				break;
		//		}
		//		break;
		//	default:
		//		break;
		//}
	}

	private void AddEventLog(byte[] data) {
		this.eventLogList.Add(data);
	}

	static private LogFuncId GetLogFuncId(UInt16 rawId) {
		return (LogFuncId)(rawId - CriProfiler.logFuncBaseNum);
	}

	public string FilteredLog2String(byte[] data) {
		if (data == null || data.Length < TcpLogHeader.properSize) { return string.Empty; }

		var header = new TcpLogHeader(data);
		var funcId = GetLogFuncId(header.function_id);
		uint playbackId;
		int dataSize, dataSize2, offset, offset2;
		string str1, str2;

		string output = string.Empty;
		switch (funcId) {
			case LogFuncId.LOG_COMMAND_ExPlaybackId:
				playbackId = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExPlaybackId, out offset));
				dataSize = LoadBigEndianUInt16(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_CueName, out offset)); /* cue name */
				dataSize2 = LoadBigEndianUInt16(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExAcbName, out offset2)); /* cuesheet name */
				str1 = System.Text.Encoding.Default.GetString(data, GetStrDataOffset(header, offset), dataSize).TrimEnd('\0');
				str2 = System.Text.Encoding.Default.GetString(data, GetStrDataOffset(header, offset2), dataSize2).TrimEnd('\0');
				output = MicroSec2String(header.time) + " | Start ID : " + playbackId + " | cuename : " + str1 + " | cuesheet : " + str2;
				break;
			case LogFuncId.LOG_COMMAND_ExPlaybackInfo_FreeInfo:
				playbackId = LoadBigEndianUInt32(data, FindParamOffset(data, header, LogParamId.LOG_STRINGS_ITEM_ExPlaybackId, out offset));
				output = MicroSec2String(header.time) + " | End ID : " + playbackId;
				break;
			default:
				break;
		}

		return output;
	}

	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);
	}

#region Methods for data offset calculation
	private const int paramIdSize = 2;

	private int GetStrDataOffset(TcpLogHeader header, int paramIdOffset) {
		return paramIdOffset + CriProfiler.paramIdSize + (header.isPtr64
			? ParamTypeSizes64[(int)CriProfiler.LogParamTypes.TYPE_CHAR]
			: ParamTypeSizes32[(int)CriProfiler.LogParamTypes.TYPE_CHAR]);
	}

	/* use this to search for the parameter from the head of the packet */
	private int FindParamOffset(byte[] data, TcpLogHeader header, LogParamId logParamId, out int currentOffset) {
		if (data.Length < TcpLogHeader.properSize) {
			throw new ParamIdDoesNotMatchException("Could not find parameter: " + logParamId.ToString());
		}
		int offset = header.size;
		int paramId;
		int dataSize;
		while (offset < header.packetSize) {
			paramId = LoadBigEndianUInt16(data, offset);
			if ((LogParamId)paramId == logParamId) {
				currentOffset = offset;
				return offset + paramIdSize;
			}

			dataSize = header.isPtr64 ? LogParams[paramId].size64 : LogParams[paramId].size32;
			if (LogParams[paramId].type == LogParamTypes.TYPE_CHAR) {
				dataSize += LoadBigEndianUInt16(data, offset + paramIdSize);
			}
			offset += paramIdSize + dataSize;
		}

		throw new ParamIdDoesNotMatchException("Could not find parameter: " + logParamId.ToString());
	}

	/* use this to search for the next parameter from the current position of the packet */
	private int GetNextParamOffset(byte[] data, TcpLogHeader header, LogParamId logParamId, ref int currentOffset) {
		if (currentOffset >= header.packetSize) {
			throw new ParamIdDoesNotMatchException("Could not find parameter: " + logParamId.ToString());
		}
		int paramId = LoadBigEndianUInt16(data, currentOffset);
		int dataSize = header.isPtr64 ? CriProfiler.LogParams[paramId].size64 : CriProfiler.LogParams[paramId].size32;
		if (CriProfiler.LogParams[paramId].type == LogParamTypes.TYPE_CHAR) {
			dataSize += LoadBigEndianUInt16(data, currentOffset + paramIdSize);
		}
		/* set parameter id offset */
		currentOffset += paramIdSize + dataSize;

		if ((LogParamId)LoadBigEndianUInt16(data, currentOffset) != logParamId) {
			throw new ParamIdDoesNotMatchException("Parsed parameter ID does not match with " + logParamId.ToString());
		}

		/* return parameter data offset */
		return currentOffset + paramIdSize;
	}
#endregion Methods for data offset calculation

#endregion Parser


#region Timeline

	/* CueSheet - Cue - Playback Lane (multiple non-overlapping playback lists) - Timestamp (true = start, false = end) */

	private void AddPlaybackToTimeline(Playback playback) {
		CueSheetGroup group;
		lock (multiLaneTimeline) {
			if (multiLaneTimeline.TryGetValue(playback.cuesheet, out group) == false) {
				group = new CueSheetGroup(playback.cuesheet);
				multiLaneTimeline.Add(playback.cuesheet, group);
			}
		}
		group.AddPlayback(playback);
	}

	public Dictionary<string, CueSheetGroup> TimelineDataSlice {
		get {
			var output = new Dictionary<string, CueSheetGroup>();
			lock (multiLaneTimeline) {
				foreach (var elem in multiLaneTimeline) {
					output.Add(elem.Key, elem.Value.Clone());
				}
			}
			return output;
		}
	}

	public class CueSheetGroup : ICloneable {
		public string name;
		public Color cuesheetColor = Color.white;
		public Dictionary<string, CueLane> cueLaneDict;

		static private readonly Color baseColor = new Color(0.5412f, 1f, 0.3451f);

		public CueSheetGroup(string name) {
			this.name = name;
			{/* set distinguishing color */
				float h, s, v;
				Color.RGBToHSV(baseColor, out h, out s, out v);
				System.Random rand = new System.Random();
				this.cuesheetColor = Color.HSVToRGB(rand.Next(0, 10000) / 10000f, s, v);
			}
			cueLaneDict = new Dictionary<string, CueLane>();
		}

		public CueSheetGroup(CueSheetGroup group) {
			this.name = group.name;
			this.cuesheetColor = group.cuesheetColor;
			this.cueLaneDict = new Dictionary<string, CueLane>();
			lock (group.cueLaneDict) {
				foreach (var elem in group.cueLaneDict) {
					cueLaneDict.Add(elem.Key, elem.Value.Clone());
				}
			}
		}
		public CueSheetGroup Clone() { return new CueSheetGroup(this); }
		object ICloneable.Clone() { return Clone(); }

		public void AddPlayback(Playback playback) {
			CueLane lane;
			if (cueLaneDict.TryGetValue(playback.cuename, out lane) == false) {
				lane = new CueLane(playback.cuename, playback.cuesheet);
				cueLaneDict.Add(playback.cuename, lane);
			}
			lane.AddPlayback(playback);
		}

		public int TotalLaneCount {
			get {
				int cnt = 0;
				foreach (var elem in cueLaneDict) {
					cnt += elem.Value.playbackLaneList.Count;
				}
				return cnt;
			}
		}
	}

	public class CueLane : ICloneable {
		public string name;
		public string cuesheet;
		public Color cueColor;
		public List<PlaybackLane> playbackLaneList;

		static private readonly Color baseColor = new Color(0.537f, 0.922f, 0.424f);
		private Dictionary<uint, int> playingDict; /* <playback id, lane index>; wont be cloned */

		public CueLane(string name, string cuesheet) {
			this.name = name;
			this.cuesheet = cuesheet;
			{/* set distinguishing color */
				float h, s, v;
				Color.RGBToHSV(baseColor, out h, out s, out v);
				System.Random rand = new System.Random();
				this.cueColor = Color.HSVToRGB(rand.Next(0, 10000) / 10000f, s, v);
			}
			playbackLaneList = new List<PlaybackLane>();
			playingDict = new Dictionary<uint, int>();
		}

		public CueLane(CueLane cuelane) {
			this.name = cuelane.name;
			this.cuesheet = cuelane.cuesheet;
			this.cueColor = cuelane.cueColor;
			this.playbackLaneList = new List<PlaybackLane>();
			lock(cuelane.playbackLaneList) {
				this.playbackLaneList.AddRange(cuelane.playbackLaneList.Select(i => i.Clone()));
			}
		}

		public CueLane Clone() { return new CueLane(this); }
		object ICloneable.Clone() { return Clone(); }

		public void AddPlayback(Playback playback) {
			if (playback.isStart) { /* playback start */
				bool added = false;
				for (int i = 0; i < playbackLaneList.Count; ++i) {
					if (playbackLaneList[i].isPlaying == false) {
						if (playbackLaneList[i].AddPlayback(playback)) {
							playingDict.Add(playback.id, i);
							added = true;
							break;
						}
					}
				}
				if (added == false) { /* no empty lane */
					playbackLaneList.Add(new PlaybackLane());
					if (playbackLaneList[playbackLaneList.Count - 1].AddPlayback(playback)) {
						playingDict.Add(playback.id, playbackLaneList.Count - 1);
					}
				}
			} else { /* playback end */
				int laneId;
				if (playingDict.TryGetValue(playback.id, out laneId)) {
					playbackLaneList[laneId].AddPlayback(playback);
					playingDict.Remove(playback.id);
				}
			}
		}
	}

	public class PlaybackLane : ICloneable {
		public List<Playback> playbackList;
		private int balance;

		public bool isConsistent {
			get {
				if (balance > 1 || balance < 0) {
					return false;
				}
				return true;
			}
		}

		public PlaybackLane() {
			playbackList = new List<Playback>();
			balance = 0;
		}

		public PlaybackLane(PlaybackLane playbackLane) {
			lock (playbackLane.playbackList) {
				this.playbackList = new List<Playback>(playbackLane.playbackList);
			}
			this.balance = playbackLane.balance;
		}

		public PlaybackLane Clone() { return new PlaybackLane(this); }
		object ICloneable.Clone() { return Clone(); }

		public bool AddPlayback(Playback playback) {
			if (playbackList.Count > 0 &&
				(playbackList[playbackList.Count - 1].timestamp > playback.timestamp ||
				playbackList[playbackList.Count - 1].isStart == playback.isStart)) {
				UnityEngine.Debug.LogError("[CRIWARE] Profiler internal: playback timestamp order error");
				return false;
			}
			playbackList.Add(playback);
			balance += playback.isStart ? 1 : -1;
			return true;
		}

		public bool isPlaying {
			get {
				return playbackList[playbackList.Count - 1].isStart;
			}
		}
	}

	public struct Playback {
		public uint id;
		public ulong timestamp;
		public bool isStart;
		public string cuename;
		public string cuesheet;
		public Playback(uint id, ulong timestamp, bool isStart, string cuename, string cuesheet) {
			this.id = id;
			this.timestamp = timestamp;
			this.isStart = isStart;
			this.cuename = cuename;
			this.cuesheet = cuesheet;
		}
	}
#endregion
}

} //namespace CriWare

#endif