#if UNITY_EDITOR

using Newtonsoft.Json;
using Snerble.VRC.OSCLink.Unity;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Snerble.OSCLink;
using UnityEditor;
using UnityEngine;
using VRC.Core;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDKBase.Editor.BuildPipeline;
using Menu = Snerble.VRC.OSCLink.Unity.Menu;
using Object = UnityEngine.Object;
using System.Reflection;
using VRC.SDKBase;

#nullable enable

public static class ExportMenuToJsonContextAction
{
	[MenuItem("CONTEXT/VRCExpressionsMenu/Export to JSON")]
	public static void Action(MenuCommand command)
	{
		var vrcmenu = (VRCExpressionsMenu)command.context;
		var avatar = GetSelectedAvatar();

		if (!avatar)
		{
			EditorUtility.DisplayDialog(
				"Notice",
				"This utility requires you to select the corresponding avatar in the VRCSDK panel",
				"aight");
			return;
		}

		var pipeline = avatar!.GetComponent<PipelineManager>();
		var avatarId = pipeline ? pipeline.blueprintId : null;
		if (string.IsNullOrEmpty(avatarId))
		{
			EditorUtility.DisplayDialog(
				"Notice",
				"That avatar has no blueprint ID and probably hasn't been uploaded yet",
				"aight");
			return;
		}

		ExportOscLinkMenu(avatar, vrcmenu, true);
	}

    public static void ExportOscLinkMenu(VRCAvatarDescriptor avatar, VRCExpressionsMenu vrcmenu, bool openFile)
	{
		var pipeline = avatar.GetComponent<PipelineManager>();
		var avatarId = pipeline ? pipeline.blueprintId : null;
		if (string.IsNullOrEmpty(avatarId)) return;

		var path = Environment.ExpandEnvironmentVariables($"%LocalAppData%/snerble/OSCLink/Menus/{avatarId}.json");
		var menu = GetDescriptor(vrcmenu);

		Serialize(path, menu, openFile);
	}

	private static void Serialize(string path, MenuDescriptor menu, bool open)
	{
		var dir = Path.GetDirectoryName(path)!;
		Directory.CreateDirectory(dir);

		var json = JsonConvert.SerializeObject(menu, new JsonSerializerSettings
        {
			NullValueHandling = NullValueHandling.Ignore
        });
		File.WriteAllText(path, json);

		if (open) Application.OpenURL(path);
	}

	private static MenuDescriptor GetDescriptor(VRCExpressionsMenu menu)
    {
        var descriptor = new MenuDescriptor()
        {
            Menus = GetMenus(menu).ToList(),
            Resources = GetResources(menu).ToList()
        };

		// validate
        var menus = descriptor.Menus.Select(x => x.Id).ToHashSet();

        foreach (var control in descriptor.Menus.SelectMany(x => x.Controls))
        {
            if (control.SubMenu == null) continue;
            if (menus.Contains(control.SubMenu)) continue;
			Debug.Log($"Control {control.Name} points to a menu that doesn't exist");
        }

        return descriptor;
    }

	private static IEnumerable<Menu> GetMenus(VRCExpressionsMenu menu)
	{
		// ReSharper disable once LoopCanBeConvertedToQuery
		foreach (var group in menu.Walk().GroupBy(x => x.Parent, x => x.Control))
		{
			var vrcmenu = group.Key;

			yield return new()
			{
				Id = AssetGuid(vrcmenu)!,
				Controls = GetControls(group).ToList()
			};
		}
	}

	private static IEnumerable<MenuControl> GetControls(IEnumerable<VRCExpressionsMenu.Control> controls)
	{
        foreach (var control in controls)
        {
            var menuControl = new MenuControl();
            menuControl.Name = control.name;
            menuControl.Type = control.type switch
            {
                VRCExpressionsMenu.Control.ControlType.Button => MenuControlType.Button,
                VRCExpressionsMenu.Control.ControlType.Toggle => MenuControlType.Toggle,
                VRCExpressionsMenu.Control.ControlType.SubMenu => MenuControlType.Folder,
                VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet => MenuControlType.TwoAxis,
                VRCExpressionsMenu.Control.ControlType.FourAxisPuppet => MenuControlType.FourAxis,
                VRCExpressionsMenu.Control.ControlType.RadialPuppet => MenuControlType.Radial,
                _ => throw new ArgumentOutOfRangeException()
            };
            menuControl.Icon = AssetGuid(control.icon);
            menuControl.SubMenu = control.type switch
            {
                VRCExpressionsMenu.Control.ControlType.SubMenu => AssetGuid(control.subMenu),
                _ => null
            };
            menuControl.Parameter = (control.parameter?.name).NullIfEmpty();
            menuControl.Value = menuControl.Parameter == null ? null : control.value;
            menuControl.SubParameters = (control.subParameters?
                .Select(x => x?.name.NullIfEmpty())
                .SkipLastWhile(x => x == null)
                .ToList())
                .NullIfEmpty();
            menuControl.Labels = (control.labels?
                .Select(x => new MenuControlLabel { Icon = AssetGuid(x.icon), Label = x.name })
                .Select(x => x.IsEmpty() ? null : x)
                .SkipLastWhile(x => x == null)
                .ToList())
                .NullIfEmpty();

            yield return menuControl;
        }
	}

	private static IEnumerable<MenuResource> GetResources(VRCExpressionsMenu menu)
	{
		var images = menu.Walk()
			.SelectMany(x => (x.Control.labels ?? Array.Empty<VRCExpressionsMenu.Control.Label>())
                .Select(y => y.icon)
                .Prepend(x.Control.icon))
			.Where(x => x)
			.ToHashSet();

		foreach (var image in images)
		{
			var imagePath = AssetDatabase.GetAssetPath(image);
			var imageExtension = Path.GetExtension(imagePath);
			var mime = MimeMappings.Get(imageExtension);

			byte[] data;
			if (mime.StartsWith("image/"))
			{
				data = File.ReadAllBytes(imagePath);
			}
			else
			{
				mime = "image/png";
				data = image.EncodeToPNG();
			}

			yield return new()
			{
				Id = AssetGuid(image)!,
				MimeType = mime,
				Data = Convert.ToBase64String(data)
			};
		}
	}

	private static string? AssetGuid(Object asset)
	{
		if (!asset) return null;

		var path = AssetDatabase.GetAssetPath(asset);
		var extension = Path.GetExtension(path);
		var mime = MimeMappings.Get(extension) ?? "";

		if (mime.StartsWith("image/"))
			return AssetDatabase.GUIDFromAssetPath(path).ToString();

		if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var guid, out long localId))
			return AssetDatabase.GUIDFromAssetPath(path).ToString();

		var unsigned = unchecked((ulong)localId);
		return $"{guid}:{unsigned:x}";
	}

	private static string? NullIfEmpty(this string? s) => string.IsNullOrEmpty(s) ? null : s;

    private static T? NullIfEmpty<T>(this T? list) where T : IList
    {
        if (list == null || list.Count == 0) return default;
        return list;
    }

    private static IEnumerable<T> SkipLastWhile<T>(this IEnumerable<T> list, Func<T, bool> predicate)
    {
        return list.Reverse().SkipWhile(predicate).Reverse();
    }

	#region VRCSDK Utils
	private static VRCAvatarDescriptor? GetSelectedAvatar()
	{
		return Type
			.GetType("VRC.SDK3A.Editor.VRCSdkControlPanelAvatarBuilder, VRC.SDK3A.Editor")!
			.GetFields(BindingFlags.Static | BindingFlags.NonPublic)
			.First(x => x.FieldType == typeof(VRC_AvatarDescriptor))
			.GetValue(null) as VRCAvatarDescriptor;
	}
	#endregion

	#region VRC Expressions menu extensions
	public static VRCExpressionsMenu? GetMenu(this VRCExpressionsMenu.Control control)
	{
		return control.type == VRCExpressionsMenu.Control.ControlType.SubMenu ? control.subMenu : null;
	}

	public static IEnumerable<(VRCExpressionsMenu Parent, VRCExpressionsMenu.Control Control)> Walk(this VRCExpressionsMenu menu)
	{
		return Walk_Inner(menu, new());
	}

	private static IEnumerable<(VRCExpressionsMenu Parent, VRCExpressionsMenu.Control Control)> Walk_Inner(VRCExpressionsMenu menu, HashSet<VRCExpressionsMenu> passed)
	{
		if (!menu) yield break;
		if (!passed.Add(menu)) yield break;

		foreach (var control in menu.controls.ToList())
		{
			yield return (menu, control);

			var controlMenu = control.GetMenu();
			if (!controlMenu) continue;

			foreach (var submenu in Walk_Inner(controlMenu!, passed))
			{
				yield return submenu;
			}
		}
	}
	#endregion
}

public class ExportMenuOnUploadAvatarPreprocessor : IVRCSDKPreprocessAvatarCallback
{
	public int callbackOrder => int.MaxValue;
	
	public bool OnPreprocessAvatar(GameObject avatarGameObject)
	{
		try
		{
			var avatar = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
			var menu = avatar.expressionsMenu;

			ExportMenuToJsonContextAction.ExportOscLinkMenu(avatar, menu, false);
		}
		catch (Exception e)
		{
			Debug.LogError($"Failed to export avatar menu to JSON: {e.Message}");
			Debug.LogException(e);
		}

		return true;
	}
}

#endif