#if UNITY_EDITOR
using UnityEngine;
using System.Collections.Generic;
using VRC.SDK3.Avatars.ScriptableObjects;
using nadena.dev.ndmf;
using gay.lilyy.MusicPlayer.Editor;
using AnimatorAsCode.V1;
using VRC.SDK3.Avatars.Components;
using AnimatorAsCode.V1.ModularAvatar;
using UnityEditor;
using AnimatorAsCode.V1.VRC;
using NUnit.Framework.Constraints;
using System.Linq;
using System.Threading.Tasks;
using nadena.dev.modular_avatar.core;

[assembly: ExportsPlugin(typeof(MusicPlayerPlugin))]

namespace gay.lilyy.MusicPlayer.Editor
{
    public static class ParameterNames
    {
        public const string SongIndex = "Music_SongIndex";
        public const string Volume = "Music_Volume";
        public const string IsLocal = "Music_IsLocal";
    }

    public static class LayerNames
    {
        public const string Volume = "Music_Volume";
        public const string Player = "Music_Player";
    }

    public class MusicPlayerPlugin : Plugin<MusicPlayerPlugin>
    {
        public override string QualifiedName => "gay.lilyy.MusicPlayer";
        public override string DisplayName => "Music Player";

        private const string SystemName = "MusicPlayer";
        private const bool UseWriteDefaults = true;
        protected override void Configure()
        {
            InPhase(BuildPhase.Generating).Run($"Generate {DisplayName}", Generate);
        }
        private void Generate(BuildContext ctx)
        {
            // Find all components of type ExampleToggle in this avatar.
            var songs = ctx.AvatarRootTransform.GetComponentsInChildren<MusicPlayerSong>();
            if (songs.Length == 0) return; // If there are none in the avatar, skip this entirely.

            var songGo = songs[0].gameObject;
            int trackNumber = 0;
            foreach (var song in songs)
            {
                if (song.gameObject != songGo)
                {
                    throw new System.Exception("All MusicPlayerSong components must be on the same GameObject.");
                }
                trackNumber++;
                song.TrackNumber = trackNumber;
            }

            var audioSource = songGo.GetComponent<AudioSource>();



            // Initialize Animator As Code.
            var modularAvatar = MaAc.Create(songGo);


            var controller = CreateController(ctx, modularAvatar, songs, audioSource);

            foreach (var song in songs)
            {
                UnityEngine.Object.DestroyImmediate(song);
            }

            // Create a new object in the scene. We will add Modular Avatar components inside it.

            // By creating a Modular Avatar Merge Animator component,
            // our animator controller will be added to the avatar's FX layer.
            modularAvatar.NewMergeAnimator(controller.AnimatorController, VRCAvatarDescriptor.AnimLayerType.FX);
            Debug.Log("Added music player");
        }

        private AacFlController CreateController(BuildContext ctx, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource)
        {
            var aac = AacV1.Create(new AacConfiguration
            {
                SystemName = SystemName,
                AnimatorRoot = ctx.AvatarRootTransform,
                DefaultValueRoot = ctx.AvatarRootTransform,
                AssetKey = GUID.Generate().ToString(),
                AssetContainer = ctx.AssetContainer,
                ContainerMode = AacConfiguration.Container.OnlyWhenPersistenceRequired,
                // (For AAC 1.2.0 and above) The next line is recommended starting from NDMF 1.6.0.
                // If you use a lower version of NDMF or if you don't use it, remove that line.
                AssetContainerProvider = new NDMFContainerProvider(ctx),
                // States will be created with Write Defaults set to ON or OFF based on whether UseWriteDefaults is true or false.
                DefaultsProvider = new AacDefaultsProvider(UseWriteDefaults)
            });
            // Create a new animator controller.
            // This will be merged with the rest of the playable layer at the end of this function.
            var controller = aac.NewAnimatorController();
            
            var songIndexParam = CreatePlayerLayer(controller, modularAvatar, songs, audioSource);
            var volumeParam = CreateVolumeLayer(aac, controller, modularAvatar, audioSource);

            CreateMenu(songIndexParam, volumeParam, modularAvatar, songs);

            return controller;
        }

        private void CreateMenu(AacFlIntParameter songIndexParam, AacFlFloatParameter volumeParam, MaAc modularAvatar, MusicPlayerSong[] songs) {
            var menuGo = new GameObject("Menu");
            menuGo.transform.SetParent(songs[0].transform);
            var menuItem = menuGo.AddComponent<ModularAvatarMenuItem>();
            menuItem.label = "Music";
            menuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
            menuItem.MenuSource = SubmenuSource.Children;
            menuGo.AddComponent<ModularAvatarMenuInstaller>();

            // Dictionary to store folder hierarchy
            var folderHierarchy = new Dictionary<string, GameObject>();

            foreach (var song in songs)
            {
                GameObject currentParent = menuGo;
                
                // Create folder hierarchy if specified
                if (!string.IsNullOrEmpty(song.Folder))
                {
                    string[] folderPath = song.Folder.Split('/');
                    
                    // Create nested folder structure
                    for (int i = 0; i < folderPath.Length; i++)
                    {
                        string folderName = folderPath[i];
                        string fullPath = string.Join("/", folderPath, 0, i + 1);
                        
                        if (!folderHierarchy.ContainsKey(fullPath))
                        {
                            var folderGo = new GameObject($"Folder_{folderName}");
                            folderGo.transform.SetParent(currentParent.transform);
                            var folderMenuItem = folderGo.AddComponent<ModularAvatarMenuItem>();
                            folderMenuItem.label = folderName;
                            folderMenuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
                            folderMenuItem.MenuSource = SubmenuSource.Children;
                            if (song.icon != null) folderMenuItem.Control.icon = song.icon;
                            
                            folderHierarchy[fullPath] = folderGo;
                        }
                        
                        currentParent = folderHierarchy[fullPath];
                    }
                }
                
                // Create the song toggle
                var go = new GameObject($"Song_{song.Name}");
                go.transform.SetParent(currentParent.transform);
                var toggle = modularAvatar.EditMenuItem(go).ToggleSets(songIndexParam, song.TrackNumber).Name(song.Name);
                if (song.icon != null) toggle.WithIcon(song.icon);
            }
            var volumeGo = new GameObject("Volume");
            volumeGo.transform.SetParent(menuGo.transform);
            modularAvatar.EditMenuItem(volumeGo).Radial(volumeParam).Name("Volume");
        }

        private AacFlIntParameter CreatePlayerLayer(AacFlController controller, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource)
        {
            var playerLayer = controller.NewLayer(LayerNames.Player);
            var songIndexParam = playerLayer.IntParameter(ParameterNames.SongIndex);
            modularAvatar.NewParameter(songIndexParam).NotSaved();
            var isLocal = playerLayer.Av3().IsLocal;

            var nonLocalState = playerLayer.NewState("NonLocal");
            var idleState = playerLayer.NewState("Idle");

            nonLocalState.TransitionsTo(idleState).When(isLocal.IsEqualTo(true));
            // will never appear ingame, useful for testing
            idleState.TransitionsTo(nonLocalState).When(isLocal.IsEqualTo(false));
            
            

            foreach (var song in songs)
            {
                var songState = playerLayer.NewState($"Song ${song.Name}");
                
                songState.TransitionsTo(idleState).When(songIndexParam.IsNotEqualTo(song.TrackNumber));
                // again, will never happen ingame. still useful
                songState.TransitionsTo(nonLocalState).When(isLocal.IsEqualTo(false));
                idleState.TransitionsTo(songState).When(songIndexParam.IsEqualTo(song.TrackNumber));
                songState.Audio(audioSource, (audio) =>
                {
                    
                    audio.SelectsClip(0, new List<AudioClip> { song.song }.ToArray() );
                    audio.StopsPlayingOnExit();
                    audio.StartsPlayingOnEnter();
                });
            }
            return songIndexParam;
        }
        private AacFlFloatParameter CreateVolumeLayer(AacFlBase aac, AacFlController controller, MaAc modularAvatar, AudioSource audioSource) {
            var songVolumeLayer = controller.NewLayer(LayerNames.Volume);
            var volumeParam = songVolumeLayer.FloatParameter(ParameterNames.Volume);
            modularAvatar.NewParameter(volumeParam).WithDefaultValue(0.2f);

            var clip = aac.NewClip().Animating((action) => {
                var animate = action.Animates(audioSource, "m_Volume");
                animate.WithSecondsUnit((seconds) => {
                    seconds.Linear(0, 0);
                    seconds.Linear(1, 0.3f);
                });
            });
            var state = songVolumeLayer.NewState("Volume");
            state.WithAnimation(clip).WithMotionTime(volumeParam);

            return volumeParam;
        }
    }

    internal class NDMFContainerProvider : IAacAssetContainerProvider
    {
        private readonly BuildContext _ctx;
        public NDMFContainerProvider(BuildContext ctx) => _ctx = ctx;
        public void SaveAsPersistenceRequired(UnityEngine.Object objectToAdd) => _ctx.AssetSaver.SaveAsset(objectToAdd);
        public void SaveAsRegular(UnityEngine.Object objectToAdd) { } // Let NDMF crawl our assets when it finishes
        public void ClearPreviousAssets() { } // ClearPreviousAssets is never used in non-destructive contexts
    }
}
#endif
