/******************************************************************************
 * Spine Runtimes License Agreement
 * Last updated July 28, 2023. Replaces all prior versions.
 *
 * Copyright (c) 2013-2023, Esoteric Software LLC
 *
 * Integration of the Spine Runtimes into software or otherwise creating
 * derivative works of the Spine Runtimes is permitted under the terms and
 * conditions of Section 2 of the Spine Editor License Agreement:
 * http://esotericsoftware.com/spine-editor-license
 *
 * Otherwise, it is permitted to integrate the Spine Runtimes into software or
 * otherwise create derivative works of the Spine Runtimes (collectively,
 * "Products"), provided that each user of the Products must obtain their own
 * Spine Editor license and redistribution of the Products in any form must
 * include this license and copyright notice.
 *
 * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
 * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
 * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
#if UNITY_EDITOR
using System.Globalization;
using System.Text.RegularExpressions;
#endif

namespace Spine.Unity {

	public static class SkeletonDataCompatibility {

#if UNITY_EDITOR
		static readonly int[][] compatibleBinaryVersions = { new[] { 4, 1, 0 } };
		static readonly int[][] compatibleJsonVersions = { new[] { 4, 1, 0 } };

		static bool wasVersionDialogShown = false;
		static readonly Regex jsonVersionRegex = new Regex(@"""spine""\s*:\s*""([^""]+)""", RegexOptions.CultureInvariant);
#endif

		public enum SourceType {
			Json,
			Binary
		}

		[System.Serializable]
		public class VersionInfo {
			public string rawVersion = null;
			public int[] version = null;
			public SourceType sourceType;
		}

		[System.Serializable]
		public class CompatibilityProblemInfo {
			public VersionInfo actualVersion;
			public int[][] compatibleVersions;
			public string explicitProblemDescription = null;

			public string DescriptionString () {
				if (!string.IsNullOrEmpty(explicitProblemDescription))
					return explicitProblemDescription;

				string compatibleVersionString = "";
				string optionalOr = null;
				foreach (int[] version in compatibleVersions) {
					compatibleVersionString += string.Format("{0}{1}.{2}", optionalOr, version[0], version[1]);
					optionalOr = " or ";
				}
				return string.Format("Skeleton data could not be loaded. Data version: {0}. Required version: {1}.\nPlease re-export skeleton data with Spine {1} or change runtime to version {2}.{3}.",
					actualVersion.rawVersion, compatibleVersionString, actualVersion.version[0], actualVersion.version[1]);
			}
		}

#if UNITY_EDITOR
		public static VersionInfo GetVersionInfo (TextAsset asset, out bool isSpineSkeletonData, ref string problemDescription) {
			isSpineSkeletonData = false;
			if (asset == null)
				return null;

			VersionInfo fileVersion = new VersionInfo();
			bool hasBinaryExtension = asset.name.Contains(".skel");
			fileVersion.sourceType = hasBinaryExtension ? SourceType.Binary : SourceType.Json;

			bool isJsonFileByContent = IsJsonFile(asset);
			if (hasBinaryExtension == isJsonFileByContent) {
				if (hasBinaryExtension) {
					problemDescription = string.Format("Failed to read '{0}'. Extension is '.skel.bytes' but content looks like a '.json' file.\n"
						+ "Did you choose the wrong extension upon export?\n", asset.name);
				} else {
					problemDescription = string.Format("Failed to read '{0}'. Extension is '.json' but content looks like binary 'skel.bytes' file.\n"
						+ "Did you choose the wrong extension upon export?\n", asset.name);
				}
				isSpineSkeletonData = false;
				return null;
			}

			if (fileVersion.sourceType == SourceType.Binary) {
				try {
					using (MemoryStream memStream = new MemoryStream(asset.bytes)) {
						fileVersion.rawVersion = SkeletonBinary.GetVersionString(memStream);
					}
				} catch (System.Exception e) {
					problemDescription = string.Format("Failed to read '{0}'. It is likely not a binary Spine SkeletonData file.\n{1}", asset.name, e);
					isSpineSkeletonData = false;
					return null;
				}
			} else {
				Match match = jsonVersionRegex.Match(asset.text);
				if (match != null) {
					fileVersion.rawVersion = match.Groups[1].Value;
				} else {
					object obj = Json.Deserialize(new StringReader(asset.text));
					if (obj == null) {
						problemDescription = string.Format("'{0}' is not valid JSON.", asset.name);
						isSpineSkeletonData = false;
						return null;
					}

					Dictionary<string, object> root = obj as Dictionary<string, object>;
					if (root == null) {
						problemDescription = string.Format("'{0}' is not compatible JSON. Parser returned an incorrect type while parsing version info.", asset.name);
						isSpineSkeletonData = false;
						return null;
					}

					if (root.ContainsKey("skeleton")) {
						Dictionary<string, object> skeletonInfo = (Dictionary<string, object>)root["skeleton"];
						object jv;
						skeletonInfo.TryGetValue("spine", out jv);
						fileVersion.rawVersion = jv as string;
					}
				}
			}

			if (string.IsNullOrEmpty(fileVersion.rawVersion)) {
				// very likely not a Spine skeleton json file at all. Could be another valid json file, don't report errors.
				isSpineSkeletonData = false;
				return null;
			}

			string[] versionSplit = fileVersion.rawVersion.Split('.');
			try {
				fileVersion.version = new[]{ int.Parse(versionSplit[0], CultureInfo.InvariantCulture),
									int.Parse(versionSplit[1], CultureInfo.InvariantCulture) };
			} catch (System.Exception e) {
				problemDescription = string.Format("Failed to read version info at skeleton '{0}'. It is likely not a valid Spine SkeletonData file.\n{1}", asset.name, e);
				isSpineSkeletonData = false;
				return null;
			}
			isSpineSkeletonData = true;
			return fileVersion;
		}

		public static bool IsJsonFile (TextAsset file) {
			byte[] content = file.bytes;

			// check for binary skeleton version number string, starts after 8 byte hash
			char majorVersionChar = compatibleBinaryVersions[0][0].ToString()[0];
			if (content.Length > 10 && content[9] == majorVersionChar && content[10] == '.')
				return false;

			const int maxCharsToCheck = 256;
			int numCharsToCheck = Math.Min(content.Length, maxCharsToCheck);
			int i = 0;
			if (content.Length >= 3 && content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF) // skip potential BOM
				i = 3;
			bool openingBraceFound = false;
			for (; i < numCharsToCheck; ++i) {
				char c = (char)content[i];
				if (char.IsWhiteSpace(c))
					continue;
				if (!openingBraceFound) {
					if (c == '{' || c == '[') openingBraceFound = true;
					else return false;
				} else if (c == '{' || c == '[' || c == ']' || c == '}' || c == ',')
					continue;
				else
					return c == '"';
			}
			return true;
		}

		public static CompatibilityProblemInfo GetCompatibilityProblemInfo (VersionInfo fileVersion) {
			if (fileVersion == null) {
				return null; // it's most likely not a Spine skeleton file, e.g. another json file. don't report problems.
			}

			CompatibilityProblemInfo info = new CompatibilityProblemInfo();
			info.actualVersion = fileVersion;
			info.compatibleVersions = (fileVersion.sourceType == SourceType.Binary) ? compatibleBinaryVersions
				: compatibleJsonVersions;

			foreach (int[] compatibleVersion in info.compatibleVersions) {
				bool majorMatch = fileVersion.version[0] == compatibleVersion[0];
				bool minorMatch = fileVersion.version[1] == compatibleVersion[1];
				if (majorMatch && minorMatch) {
					return null; // is compatible, thus no problem info returned
				}
			}
			return info;
		}

		public static void DisplayCompatibilityProblem (string descriptionString, TextAsset spineJson) {
			if (!wasVersionDialogShown) {
				wasVersionDialogShown = true;
				UnityEditor.EditorUtility.DisplayDialog("Version mismatch!", descriptionString, "OK");
			}
			Debug.LogError(string.Format("Error importing skeleton '{0}': {1}",
				spineJson.name, descriptionString), spineJson);
		}
#endif // UNITY_EDITOR
	}
}
