﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Codice.Client.Common.Tree;
using UnityEditor;
using UnityEditor.Animations;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.UIElements;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;

namespace PleasureArcade.ThiccWater
{
    public class ThiccWaterInstaller : EditorWindow
    {
        private const string GeneratedAssetsFolder = "Assets/PleasureArcade/ThiccWater/GeneratedAssets";

        private const string progressBarTitle = "Installing";

        private const string defaultGroupName = "DefaultGroup";

        private ThiccWaterResources _resources;
        private VRCAvatarDescriptor avatar;
        private VRCExpressionsMenu parentMenu;

        private ScrollView root;
        private Vector2 _defaultScrollPosition;

        private EmitterGroup[] _emitterGroups;

        private const float phi = 0.618034f;

        [MenuItem("PleasureArcade/ThiccWater Installer")]
        public static void OpenInstaller()
        {
            // This method is called when the user selects the menu item in the Editor
            EditorWindow wnd = GetWindow<ThiccWaterInstaller>();
            wnd.titleContent = new GUIContent("ThiccWater Installer");

            // Limit size of the window
            wnd.minSize = new Vector2(360, 583);
            wnd.maxSize = new Vector2(360, 9999);
        }

        private void OnEnable()
        {
            // Storing emitter data in a scriptable object so I don't have to do a ton of loading in code
            // Looks like this object isn't updated when updating the package, make a new one per-version
            _resources = Resources.Load<ThiccWaterResources>("ThiccWaterResources1.3");
            
            _emitterGroups = Array.Empty<EmitterGroup>();
            _defaultScrollPosition = Vector2.zero;
            SelectDefaultAvatar();
            if(avatar != null) ReadCurrentConfig();
        }

        private void CreateGUI()
        {
            if(_emitterGroups == null || _resources == null) OnEnable();
            
            // Add a container later? styling?
            root = new ScrollView(ScrollViewMode.Vertical);

            // Avatar selector
            var avatarSelectorField = new ObjectField("Avatar")
            {
                objectType = typeof(VRCAvatarDescriptor),
                allowSceneObjects = true,
                value = avatar
            };
            avatarSelectorField.RegisterValueChangedCallback(e =>
            {
                avatar = (VRCAvatarDescriptor)e.newValue;
                if (avatar != null)
                {
                    ReadCurrentConfig();
                }
            });
            root.Add(avatarSelectorField);

            root.Add(new Label(" "));

            // Add emitters
            root.Add(new Label("Emitter Groups"));
            
            for (var i = 0; i < _emitterGroups.Length; i++)
            {
                var index = i;      // local copy for closures
                var groupBox = new Box();
                
                // Remove button
                var removeButton = new Button(new Action(() =>
                {
                    var emitterGroupsList = _emitterGroups.ToList();
                    emitterGroupsList.RemoveAt(index);
                    _emitterGroups = emitterGroupsList.ToArray();
                    Redraw();
                }))
                {
                    text = "X",
                    tooltip = "Remove this emitter group"
                };
                removeButton.style.width = new StyleLength(32);
                groupBox.Add(removeButton);

                var parentSuffixField = new TextField("Group Name")
                {
                    value = _emitterGroups[i].name,
                    tooltip = "A unique name for this group of emitters"
                };
                parentSuffixField.RegisterValueChangedCallback(e =>
                {
                    _emitterGroups[index].name = e.newValue;
                });
                groupBox.Add(parentSuffixField);
                
                // Parent bone[s]
                var parentBoneLabel = "Parent Bone[s]";
                for (var j = 0; j < _emitterGroups[i].parentBones.Length; j++)
                {
                    var jndex = j;  // copy
                    var parentBoneField = new ObjectField(parentBoneLabel)
                    {
                        objectType = typeof(Transform),
                        allowSceneObjects = true,
                        value = _emitterGroups[i].parentBones[j],
                        tooltip = "The bone[s] that emitters will attach to. One emitter of each type will be attached to each bone selected here."
                    };
                    parentBoneField.RegisterValueChangedCallback(e =>
                    {
                        _emitterGroups[index].parentBones[jndex] = (Transform)e.newValue;
                        FilterParentBones(index);
                        Redraw();
                    });
                    groupBox.Add(parentBoneField);

                    // Only label first selector
                    parentBoneLabel = " ";
                }
                var newParentBoneField = new ObjectField(parentBoneLabel)
                {
                    objectType = typeof(Transform),
                    allowSceneObjects = true,
                    value = null
                };
                
                newParentBoneField.RegisterValueChangedCallback(e =>
                {
                    _emitterGroups[index].parentBones = _emitterGroups[index].parentBones.Append((Transform)e.newValue).ToArray();
                    FilterParentBones(index);
                    Redraw();
                });
                groupBox.Add(newParentBoneField);
                
                groupBox.Add(new Label(" "));
                
                var scalingToggle = new Toggle("Enable Scaling")
                {
                    value = _emitterGroups[index].scalingEnabled,
                    tooltip = "Enable this if you want the particles to scale up/down with the parent bone"
                };
                scalingToggle.RegisterValueChangedCallback(e => _emitterGroups[index].scalingEnabled = e.newValue);
                groupBox.Add(scalingToggle);
                
                groupBox.Add(new Label(" "));
                
                groupBox.Add(new Label("Emitter Types"));
                
                var emitterToggles = new Toggle[_resources.emitterNames.Length];
                
                for (var j = 0; j < _resources.emitterNames.Length; j++)
                {
                    var jndex = j;  // copy

                    emitterToggles[j] = new Toggle(_resources.emitterNames[j])
                    {
                        value = _emitterGroups[i].selectedEmitters[j]
                    };
                    emitterToggles[j].RegisterValueChangedCallback(e =>
                    {
                        _emitterGroups[index].selectedEmitters[jndex] = e.newValue;
                    });
                    groupBox.Add(emitterToggles[j]);
                }
                
                root.Add(groupBox);
            }

            if (_emitterGroups.Length < 8)
            {
                root.Add(new Button(AddEmitterGroupButton)
                {
                    text = "Add Emitter Group"
                });
            }
            else
            {
                var noMoreGroupsButton = new Button()
                {
                    text = "Max 8 Emitter Groups"
                };
                noMoreGroupsButton.SetEnabled(false);
                root.Add(noMoreGroupsButton);
            }

            root.Add(new Label(" "));
            
            // Extra options
            var menuSelectField = new ObjectField("Add to Menu")
            {
                objectType = typeof(VRCExpressionsMenu),
                value = parentMenu
            };
            menuSelectField.RegisterValueChangedCallback(e =>
            {
                parentMenu = (VRCExpressionsMenu)e.newValue;
            });
            root.Add(menuSelectField);

            root.Add(new Label(" "));
            root.Add(new Label(" "));
        
            // Finish
            root.Add(new Button(InstallButton)
            {
                text = "Install ThiccWater"
            });
        
            // Adjustments
            root.Add(new Label(" "));
            root.Add(new Button(RemoveBottles)
            {
                text = "Remove Bottles"
            });
            
            // Uninstall
            root.Add(new Label(" "));
            root.Add(new Label(" "));
           
            root.Add(new Button(UninstallButton)
            {
                text = "Uninstall ThiccWater"
            });

            rootVisualElement.Clear();
            rootVisualElement.Add(root);
            
            root.scrollOffset = _defaultScrollPosition;
        }

        /**
         * Redraw, in case there's a better way to do this
         */
        public void Redraw()
        {
            _defaultScrollPosition = root?.scrollOffset ?? Vector2.zero;
            CreateGUI();
        }

        /**
         * Removed blank entries from parent bones & resize arrays
         */
        public void FilterParentBones(int i)
        {
            _emitterGroups[i].parentBones = _emitterGroups[i].parentBones.Where(b => b != null).ToArray();
            _emitterGroups[i].adjustedPositions = new Vector3[_emitterGroups[i].parentBones.Length, _resources.emitterNames.Length];
            _emitterGroups[i].adjustedRotations = new Quaternion[_emitterGroups[i].parentBones.Length, _resources.emitterNames.Length];
            _emitterGroups[i].adjustedScales = new Vector3[_emitterGroups[i].parentBones.Length, _resources.emitterNames.Length];
        }

        /**
         * Return the avatar if there's only one in the scene, otherwise null
         */
        public void SelectDefaultAvatar()
        {
            var avatars = FindObjectsOfType<VRCAvatarDescriptor>();
            if (avatars.Length == 1)
            {
                avatar = avatars.First();
                parentMenu = avatar.expressionsMenu;
            }
        }

        /**
         * Update the menu to be the root menu whenever an avatar is selected
         */
        public void UpdateMenuSelector()
        {
            parentMenu = FindMenuWithSubMenuRecursive(avatar.expressionsMenu, "Thicc Water") ?? avatar?.expressionsMenu;
        }

        public void InstallButton()
        {
            Undo.RecordObject(avatar, "Install ThiccWater");
            Install();
        }

        /**
         * Add an emitter group - limiting this to 8 so the menu doesn't get unruly
         */
        public void AddEmitterGroupButton()
        {
            var newGroup = new EmitterGroup()
            {
                name = "",
                parentBones = Array.Empty<Transform>(),
                scalingEnabled = false,
                selectedEmitters = new bool[_resources.emitters.Length],
                adjustedPositions = new Vector3[0, _resources.emitterNames.Length],
                adjustedRotations = new Quaternion[0, _resources.emitterNames.Length],
                adjustedScales = new Vector3[0, _resources.emitterNames.Length],
            };
            _emitterGroups = _emitterGroups.Append(newGroup).ToArray();
            
            Redraw();
        }

        /**
         * Do the thing!
         */
        public void Install()
        {
            // Sanity checks
            if (!avatar)
            {
                EditorUtility.DisplayDialog("Error", "Please select an avatar to install emitters onto", "OK");
                return;
            }

            if(_emitterGroups.Length == 0)
            {
                EditorUtility.DisplayDialog("Error", "Please add at least one emitter group to create", "OK");
                return;
            }

            if (_emitterGroups.Any(g => g.name.Trim() == ""))
            {
                EditorUtility.DisplayDialog("Error", "Every emitter group requires a distinct name", "OK");
                return;
            }
            
            if (_emitterGroups.Any(g => g.parentBones.Length == 0))
            {
                EditorUtility.DisplayDialog("Error", "Every emitter group requires at least one parent bone", "OK");
                return;
            }
            
            if (_emitterGroups.Any(g => g.selectedEmitters.All(s => !s)))
            {
                EditorUtility.DisplayDialog("Error", "Every emitter group requires at least one emitter type", "OK");
                return;
            }
            
            if (parentMenu == null)
            {
                EditorUtility.DisplayDialog("Error", "No parent menu selected", "OK");
                return;
            }
            
            if (parentMenu.controls == null)
            {
                EditorUtility.DisplayDialog("Error", "No parent menu controls", "OK");
                return;
            }
            
            if (_resources == null)
            {
                EditorUtility.DisplayDialog("Error", "Resources have disappeared", "OK");
                return;
            }
            
            // Make sure we have a folder to put generated assets in
            InitGeneratedAssetFolder();
            
            // Install now acts as a reinstall: delete anything currently on the av
            Uninstall();

            // Keep track of how many things to install (all emitters in all groups)
            var totalThings = _emitterGroups.Sum(g => g.selectedEmitters.Count(e => e));
            var thingsInstalled = 0;

            // Add container to avatar base
            var container = new GameObject("ThiccWater").transform;
            container.SetParent(avatar.transform);
            container.localPosition = Vector3.zero;
            container.localRotation = Quaternion.identity;

            // Add depth provider
            var depthProvider = Instantiate(_resources.depthProvider, Vector3.up, Quaternion.identity, container);
            depthProvider.name = "Depth Provider";

            // Add scale constraint for general avatar scaling (can be overridden per emitter)
            var defaultScaleConstraint = container.gameObject.AddComponent<ScaleConstraint>();
            defaultScaleConstraint.AddSource(new ConstraintSource()
            {
                sourceTransform = avatar.transform,
                weight = 1.0f
            });
            defaultScaleConstraint.constraintActive = true;

            // Find/Create FX controller
            avatar.customizeAnimationLayers = true;

            var fxAnimIndex = -1;
            for (var i = 0; i < avatar.baseAnimationLayers.Length; i++)
            {
                if (avatar.baseAnimationLayers[i].type == VRCAvatarDescriptor.AnimLayerType.FX) fxAnimIndex = i;
            }
        
            if (avatar.baseAnimationLayers[fxAnimIndex].isDefault || avatar.baseAnimationLayers[fxAnimIndex].animatorController == null)
            {
                var newFxController = AnimatorController.CreateAnimatorControllerAtPath(GeneratedAssetsFolder + "/ThiccWaterFX.controller");
                avatar.baseAnimationLayers[fxAnimIndex].isDefault = false;
                avatar.baseAnimationLayers[fxAnimIndex].animatorController = newFxController;
            }

            // Look up menus/params
            var expressionParams = avatar.expressionParameters.parameters.ToList();
            var fxController = (AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController;

            var menus = new Dictionary<string, VRCExpressionsMenu>();
            
            // Create emitter groups
            foreach (var group in _emitterGroups)
            {
                // Create a new menu for each group
                var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
                menu.controls = new List<VRCExpressionsMenu.Control>();

                var groupContainer = new GameObject(group.name).transform;
                groupContainer.parent = container;
                groupContainer.localPosition = Vector3.zero;
                groupContainer.localRotation = Quaternion.identity;
                groupContainer.localScale = Vector3.one;

                for (var i = 0; i < _resources.emitterNames.Length; i++)
                {
                    if (!group.selectedEmitters[i]) continue;
                    
                    var emitterName = _resources.emitterNames[i];

                    // Progress...
                    thingsInstalled++;
                    EditorUtility.DisplayProgressBar("Install", $"Creating {group.name}/{emitterName}...", (float)thingsInstalled/totalThings);
                    
                    for (var parentIndex = 0; parentIndex < group.parentBones.Length; parentIndex++)
                    {
                        // Parent constraints per object makes them impossible to adjust. Add a parent container with the constraint
                        var parentContainerName = "Parent" + parentIndex;
                        var parentContainer = groupContainer.Find(parentContainerName);
                        if (!parentContainer)
                        {
                            parentContainer = new GameObject("Parent" + parentIndex).transform;
                            parentContainer.SetParent(groupContainer);
                            
                            // Add parent constraint
                            var parentConstraint = parentContainer.gameObject.AddComponent<ParentConstraint>();
                            parentConstraint.SetSources(new List<ConstraintSource>(new []
                            {
                                new ConstraintSource()
                                {
                                    sourceTransform = group.parentBones[parentIndex],
                                    weight = 1.0f
                                }
                            }));
                            parentConstraint.locked = false;
                            parentConstraint.SetRotationOffset(0, Vector3.zero);
                            parentConstraint.SetTranslationOffset(0, Vector3.zero);
                            parentConstraint.locked = true;
                            parentConstraint.constraintActive = true;
                            
                            // Add scale constraint too, if needed
                            if (group.scalingEnabled)
                            {
                                var scaleConstraint = parentContainer.gameObject.AddComponent<ScaleConstraint>();
                                scaleConstraint.SetSources(new List<ConstraintSource>(new[]
                                {
                                    new ConstraintSource()
                                    {
                                        sourceTransform = group.parentBones[parentIndex],
                                        weight = 1.0f
                                    }
                                }));
                                scaleConstraint.locked = false;
                                scaleConstraint.scaleAtRest = Vector3.one;
                                var parentDefaultWorldScale = group.parentBones[parentIndex].lossyScale; 
                                scaleConstraint.scaleOffset = new Vector3(1f/parentDefaultWorldScale.x, 1f/parentDefaultWorldScale.y, 1f/parentDefaultWorldScale.z);
                                scaleConstraint.locked = true;
                                scaleConstraint.constraintActive = true;
                            }
                        }

                        // Add component[s]
                        var emitter = Instantiate(_resources.emitters[i], parentContainer);
                        emitter.name = emitterName + parentIndex;

                        // Instantiate emitters with previous values (if available)
                        emitter.transform.localPosition = group.adjustedPositions[parentIndex, i];
                        emitter.transform.localRotation = group.adjustedRotations[parentIndex, i] == default ? Quaternion.identity : group.adjustedRotations[parentIndex, i];
                        emitter.transform.localScale = group.adjustedScales[parentIndex, i] == default ? Vector3.one : group.adjustedScales[parentIndex, i];

                        // Special case; offset drip timing for each parent
                        if (_resources.emitterNames[i] == "Drip" || _resources.emitterNames[i] == "WhiteDrip")
                        {
                            var drips = emitter.transform.Find("DroolBones/Drool0/Drool1/Drool2/Drips").GetComponent<ParticleSystem>();
                            
                            // Particle system properties are weird, but this is correct
                            var mainModule = drips.main;
                            mainModule.startDelay = (1 - (phi * parentIndex) % 1.0f) * mainModule.duration;
                        }
                        
                        // Special case: Remove screen shader & clear buffer from all but the first emitter
                        if (parentIndex > 0)
                        {
                            var screenShader = emitter.transform.Find("ScreenShader");
                            if(screenShader != null) DestroyImmediate(screenShader.gameObject);
                            
                            var clearBuffer = emitter.transform.Find("ClearBuffer");
                            if(clearBuffer != null) DestroyImmediate(clearBuffer.gameObject);
                        }
                    }

                    // Add (non-existing) expression params
                    var paramName = group.name + "_" + emitterName;
                    if (expressionParams.All(p => p.name != paramName))
                    {
                        expressionParams.Add(new VRCExpressionParameters.Parameter()
                        {
                            name = group.name + "_" + emitterName,
                            valueType = VRCExpressionParameters.ValueType.Bool,
                            defaultValue = 0,
                            saved = false
                        });
                    }
                    
                    // Clone controller layer, update animations with new path
                    var parameterNameSwap = new Dictionary<string, string>
                    {
                        { emitterName, paramName }
                    };
                    fxController = AnimatorCloner.MergeControllers(fxController, _resources.AnimatorControllers[i], parameterNameSwap);
                    var fxLayers = fxController.layers;
                    var fxLayer = fxLayers.Last();
                    fxLayer.name = paramName;

                    // Edit animations & transitions
                    // All animations are [type]Start, [type]Stop, [type]Disable, or [type]Loop (Drip-only), but animator
                    //  state names may vary
                    var stateSuffixes = new List<string> { "Start", "Stop", "Disable", "Loop" };
                    foreach (var state in fxLayer.stateMachine.states)
                    {
                        // If this state isn't a valid animation (e.g. AnyState, Start, Exit), skip it
                        if (stateSuffixes.All(s => state.state.motion.name != emitterName + s)) continue;

                        // Create a copy of the animation in GeneratedAssets & reimport it
                        var motionPath = AssetDatabase.GetAssetPath(state.state.motion);
                        var newMotionPath = GeneratedAssetsFolder + "/" + group.name + "_" + state.state.motion.name + ".anim";
                        AnimationClipEditor.RegroupAnimation(motionPath, newMotionPath, emitterName, group.name,
                            group.parentBones.Length);
                        state.state.motion = AssetDatabase.LoadAssetAtPath<AnimationClip>(newMotionPath);
                    }

                    // Reassign modified layer array to controller (.layers only returns a copy, not a ref)
                    fxController.layers = fxLayers;

                    // Add control to menu (should only ever have 5 max per menu)
                    var baseMenuControl = _resources.menu.controls.Find(c => c.name == emitterName);
                    menu.controls.Add(new VRCExpressionsMenu.Control()
                    {
                        icon = baseMenuControl.icon,
                        name = emitterName,
                        labels = baseMenuControl.labels,
                        parameter = new VRCExpressionsMenu.Control.Parameter(){ name = paramName },
                        type = baseMenuControl.type,
                        style = baseMenuControl.style,
                        value = baseMenuControl.value
                    });
                }
                
                // Add to menus
                menus.Add(group.name, menu);
            }
            
            // Hide progress bar
            EditorUtility.ClearProgressBar();

            // If we have more than one group, save menus as sub-menus
            InitGeneratedAssetFolder();
            if (menus.Count > 1)
            {
                var groupMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
                AssetDatabase.CreateAsset(groupMenu, GeneratedAssetsFolder + "/ThiccWaterMenu.asset");
                
                foreach (var groupName in menus.Keys)
                {
                    AssetDatabase.CreateAsset(menus[groupName], GeneratedAssetsFolder + "/" + groupName + "GroupMenu.asset");
                    groupMenu.controls.Add(new VRCExpressionsMenu.Control()
                    {
                        icon = _resources.menuIcon,
                        name = groupName,
                        type = VRCExpressionsMenu.Control.ControlType.SubMenu,
                        subMenu = menus[groupName]
                    });
                    
                    EditorUtility.SetDirty(menus[groupName]);
                }
                EditorUtility.SetDirty(groupMenu);
                
                AssetDatabase.SaveAssets();
                
                parentMenu.controls.Add(new VRCExpressionsMenu.Control()
                {
                    icon = _resources.menuIcon,
                    name = "Thicc Water",
                    type = VRCExpressionsMenu.Control.ControlType.SubMenu,
                    subMenu = groupMenu,
                });
                
                EditorUtility.SetDirty(parentMenu);
            }
            else
            {
                var twMenu = menus.Values.First();
                AssetDatabase.CreateAsset(twMenu, GeneratedAssetsFolder + "/ThiccWaterMenu.asset");
                
                parentMenu.controls.Add(new VRCExpressionsMenu.Control()
                {
                    icon = _resources.menuIcon,
                    name = "Thicc Water",
                    type = VRCExpressionsMenu.Control.ControlType.SubMenu,
                    subMenu = twMenu
                });
                
                EditorUtility.SetDirty(parentMenu);
                EditorUtility.SetDirty(twMenu);
            }
            
            // Reassign animator arrays
            avatar.expressionParameters.parameters = expressionParams.ToArray();
            avatar.baseAnimationLayers[fxAnimIndex].animatorController = fxController;
            
            // Save changed assets
            EditorUtility.SetDirty(avatar.expressionParameters);
            EditorUtility.SetDirty(avatar.baseAnimationLayers[fxAnimIndex].animatorController);

            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            
            PrefabUtility.RecordPrefabInstancePropertyModifications(avatar.gameObject);
            EditorUtility.DisplayDialog("Complete", "Installation complete", "OK");
        }

        /**
         * Remove bottle meshes from root objects
         */
        public void RemoveBottles()
        {
            // Sanity checks
            if (!avatar)
            {
                EditorUtility.DisplayDialog("Error", "Please select an avatar to remove bottles from", "OK");
                return;
            }
        
            var parent = avatar.transform.Find("ThiccWater");

            if (!parent) return;

            var bottles = parent.GetComponentsInChildren<MeshFilter>();
            foreach (var meshFilter in bottles)
            {
                if (meshFilter.sharedMesh.name != "sauceBottle") continue;

                var meshRenderer = meshFilter.transform.GetComponent<MeshRenderer>();
                if(meshRenderer) DestroyImmediate(meshRenderer);
                DestroyImmediate(meshFilter);
            }
        }

        /**
         * Ensure GeneratedAssets folder exists; if not, create it
         */
        public void InitGeneratedAssetFolder()
        {
            if (AssetDatabase.IsValidFolder(GeneratedAssetsFolder)) return;
            
            var i = GeneratedAssetsFolder.LastIndexOf('/');
            AssetDatabase.CreateFolder(GeneratedAssetsFolder.Substring(0, i), GeneratedAssetsFolder.Substring(i+1));
        }

        public void UninstallButton()
        {
            // Sanity checks
            if (!avatar)
            {
                EditorUtility.DisplayDialog("Error", "Please select an avatar to uninstall from", "OK");
                return;
            }

            if (!EditorUtility.DisplayDialog("Confirm",
                "Are you sure you want to uninstall ThiccWater from this avatar?",
                "Yes", "No")) return;
            
            Undo.RecordObject(avatar, "Uninstall ThiccWater");
            Uninstall();
            
            EditorUtility.DisplayDialog("Done", "Uninstall Complete", "OK");
        }

        /**
         * Check the current avatar for a ThiccWater installation & update _emitterGroups in the window 
         */
        public void ReadCurrentConfig()
        {
            var activeEmitters = new List<EmitterData>();
            var container = avatar.transform.Find("ThiccWater");

            if (container == null) return;
            
            var constraint = container.GetComponent<ParentConstraint>();
            
            for (var i = 0; i < container.childCount; i++)
            {
                var child = container.GetChild(i);

                if (child.name == "Depth Provider") continue;
                
                // Legacy emitter found - use default group
                if (_resources.emitterNames.Contains(child.name))
                {
                    activeEmitters.Add(new EmitterData
                    {
                        name = child.name,
                        groupName = defaultGroupName,
                        scalingEnabled = container.GetComponent<ScaleConstraint>() != null,
                        parentBone = constraint?.GetSource(0).sourceTransform,
                        parentIndex = 0,
                        position = child.localPosition,
                        rotation = child.localRotation,
                        scale = child.localScale
                    });
                }

                // LEGACY - ThinStreamGold -> GoldStream
                if (child.name == "ThinStreamGold")
                {
                    activeEmitters.Add(new EmitterData
                    {
                        name = "GoldStream",
                        groupName = defaultGroupName,
                        scalingEnabled = container.GetComponent<ScaleConstraint>() != null,
                        parentBone = constraint?.GetSource(0).sourceTransform,
                        parentIndex = 0,
                        position = child.localPosition,
                        rotation = child.localRotation,
                        scale = child.localScale
                    });
                }
                
                // v1.3+ child is a group
                for (var j = 0; j < child.childCount; j++)
                {
                    var parentContainer = child.GetChild(j);
                    
                    // Sanity check
                    if (!parentContainer.name.StartsWith("Parent")) continue;

                    var parentConstraint = parentContainer.GetComponent<ParentConstraint>();
                    var scaleConstraint = parentContainer.GetComponent<ScaleConstraint>();
                    var parentBone = parentConstraint.GetSource(0).sourceTransform;
                    
                    for (var k = 0; k < parentContainer.childCount; k++)
                    {
                        var emitter = parentContainer.GetChild(k);
                        
                        var emitterName = Regex.Replace(emitter.name, @"\d", "");
                        var parentIndex = int.Parse(Regex.Replace(emitter.name, @"\D", ""));
                        
                        // Strip numbers off emitter names
                        activeEmitters.Add(new EmitterData
                        {
                            name = emitterName,
                            groupName = child.name,
                            scalingEnabled = scaleConstraint != null,
                            parentBone = parentBone,
                            parentIndex = parentIndex,
                            position = emitter.localPosition,
                            rotation = emitter.localRotation,
                            scale = emitter.localScale
                        }); 
                    }
                }
            }
            
            // Update emitter groups
            var emitterGroups = new List<EmitterGroup>();
            var allGroups = activeEmitters.Select(e => e.groupName).Distinct().ToList();
            foreach (var groupName in allGroups)
            {
                var emitters = activeEmitters.Where(e => e.groupName == groupName).ToList();
                
                var emitterGroup = new EmitterGroup()
                {
                    name = groupName,
                    scalingEnabled = emitters.First().scalingEnabled,
                    selectedEmitters = new bool[_resources.emitterNames.Length]
                };
                
                // All of the parent indices should be continuous i.e. highest index = 3 => 4 parent bones (0, 1, 2, and 3)
                var parentBonesCount = emitters.Select(e => e.parentIndex).Max() + 1;
                var parentBones = new Transform[parentBonesCount];
                var adjustedPositions = new Vector3[parentBonesCount, _resources.emitterNames.Length];
                var adjustedRotations = new Quaternion[parentBonesCount, _resources.emitterNames.Length];
                var adjustedScales = new Vector3[parentBonesCount, _resources.emitterNames.Length];
                
                foreach (var emitter in emitters)
                {
                    parentBones[emitter.parentIndex] = emitter.parentBone;
                    var emitterTypeIndex = _resources.GetEmitterIndex(emitter.name);
                    if (emitterTypeIndex == -1) continue;

                    emitterGroup.selectedEmitters[emitterTypeIndex] = true;
                    adjustedPositions[emitter.parentIndex, emitterTypeIndex] = emitter.position;
                    adjustedRotations[emitter.parentIndex, emitterTypeIndex] = emitter.rotation;
                    adjustedScales[emitter.parentIndex, emitterTypeIndex] = emitter.scale;
                }

                emitterGroup.parentBones = parentBones;
                emitterGroup.adjustedPositions = adjustedPositions;
                emitterGroup.adjustedRotations = adjustedRotations;
                emitterGroup.adjustedScales = adjustedScales;
                
                emitterGroups.Add(emitterGroup);
            }

            _emitterGroups = emitterGroups.ToArray();
            
            UpdateMenuSelector();
            Redraw();
        }

        /**
         * Uninstall EVERYTHING (most folks probably won't need this, but I want it)
         */
        public void Uninstall()
        {
            // Remove emitters
            var container = avatar.transform.Find("ThiccWater");

            if (container) DestroyImmediate(container.gameObject);

            // Get FX controller
            var fxAnimIndex = -1;
            for (var i = 0; i < avatar.baseAnimationLayers.Length; i++)
            {
                if (avatar.baseAnimationLayers[i].type == VRCAvatarDescriptor.AnimLayerType.FX) fxAnimIndex = i;
            }
            var fxController = (AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController;
            var fxLayers = fxController.layers.ToList();
            var fxParams = fxController.parameters.ToList();
            var expressionParams = avatar.expressionParameters.parameters.ToList();

            // Remove layers from FX controller & avatar params (both legacy "Drip" and 1.3+ "Group_Drip")
            foreach (var emitter in _resources.emitterNames)
            {
                fxLayers.RemoveAll(l => l.name.EndsWith(emitter));
                fxParams.RemoveAll(p => p.name.EndsWith(emitter));
                expressionParams.RemoveAll(p => p.name.EndsWith(emitter));
            }
            
            // LEGACY - v1 -> v1.1 changes "ThinStreamGold" to "GoldStream", make sure the uninstaller drops that, too
            fxLayers.RemoveAll(l => l.name == "ThinStreamGold");
            fxParams.RemoveAll(p => p.name == "ThinStreamGold");
            expressionParams.RemoveAll(p => p.name == "ThinStreamGold");

            // Save arrays
            ((AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController).layers =
                fxLayers.ToArray();
            ((AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController).parameters =
                fxParams.ToArray();
            avatar.expressionParameters.parameters = expressionParams.ToArray();
            
            // Save changed assets
            EditorUtility.SetDirty(avatar.expressionParameters);
            EditorUtility.SetDirty(avatar.baseAnimationLayers[fxAnimIndex].animatorController);

            // Find parent menu, remove thicc water menu
            var oldParentMenu = FindMenuWithSubMenuRecursive(avatar.expressionsMenu, "Thicc Water");
            if (oldParentMenu)
            {
                oldParentMenu.controls.RemoveAll(m => m.name == "Thicc Water");
                EditorUtility.SetDirty(oldParentMenu);
            }
            
            // For reinstalls, reload the parent menu from its asset, in case we just changed it
            if (parentMenu && AssetDatabase.Contains(parentMenu))
            {
                parentMenu = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>(AssetDatabase.GetAssetPath(parentMenu));
            }

            AssetDatabase.SaveAssets();
            
            PrefabUtility.RecordPrefabInstancePropertyModifications(avatar.gameObject);
        }

        /**
         * Recursive search for Thicc Water's parent menu
         */
        public VRCExpressionsMenu FindMenuWithSubMenuRecursive(VRCExpressionsMenu menu, string submenuName, int maxDepth = 10)
        {
            if (maxDepth == 0) return null;
            
            if (menu.controls.Any(c => c.name == submenuName && c.type == VRCExpressionsMenu.Control.ControlType.SubMenu))
            {
                return menu;
            }
            
            foreach (var submenu in menu.controls.Where(c => c.type == VRCExpressionsMenu.Control.ControlType.SubMenu))
            {
                // Wolfman avatar contains a submenu control without a menu, ensure we don't crash
                if (submenu.subMenu == null) continue;
                
                var found = FindMenuWithSubMenuRecursive(submenu.subMenu, submenuName, maxDepth - 1);
                if (found) return found;
            }

            return null;
        }
    }

    public struct EmitterData
    {
        public string name;
        public string groupName;
        public Transform parentBone;
        public bool scalingEnabled;
        public int parentIndex;
        public Vector3 position;
        public Quaternion rotation;
        public Vector3 scale;
    }

    public struct EmitterGroup
    {
        public string name;
        public Transform[] parentBones;
        public bool scalingEnabled;
        public bool[] selectedEmitters;
        
        // Emitter adjustments are saved for loaded configs, but not visible/editable in the installer 
        public Vector3[,] adjustedPositions;
        public Quaternion[,] adjustedRotations;
        public Vector3[,] adjustedScales;
    }
}