using UnityEngine;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using System.IO;
using System;
using System.Linq;
using System.Text.RegularExpressions;

namespace FMODUnity
{
    [CustomEditor(typeof(Settings))]
    public class SettingsEditor : Editor
    {
        string[] ToggleDisplay = new string[] { "Disabled", "Enabled", "Development Build Only",  };

        string[] FrequencyDisplay = new string[] {
            "Platform Default",
            "22.05 kHz",
            "24 kHz",
            "32 kHz",
            "44.1 kHz",
            "48 kHz"
        };

        int[] FrequencyValues = new int[] { 0, 22050, 24000, 32000, 44100, 48000 };

        static readonly string[] SpeakerModeDisplay = new string[] {
            "Stereo",
            "Surround 5.1",
            "Surround 7.1",
            "Surround 7.1.4"
        };

        static readonly FMOD.SPEAKERMODE[] SpeakerModeValues = new FMOD.SPEAKERMODE[] {
            FMOD.SPEAKERMODE.STEREO,
            FMOD.SPEAKERMODE._5POINT1,
            FMOD.SPEAKERMODE._7POINT1,
            FMOD.SPEAKERMODE._7POINT1POINT4
        };

        bool hasBankSourceChanged = false;
        bool hasBankTargetChanged = false;

        bool expandThreadAffinity;
        bool expandDynamicPlugins;
        bool expandStaticPlugins;

        static Section sExpandedSections;

        SerializedProperty automaticEventLoading;
        SerializedProperty automaticSampleLoading;
        SerializedProperty bankLoadType;
        SerializedProperty banksToLoad;
        SerializedProperty enableMemoryTracking;
        SerializedProperty encryptionKey;
        SerializedProperty hasSourceProject;
        SerializedProperty hasPlatforms;
        SerializedProperty importType;
        SerializedProperty loggingLevel;
        SerializedProperty meterChannelOrdering;
        SerializedProperty sourceBankPath;
        SerializedProperty sourceProjectPath;
        SerializedProperty stopEventsOutsideMaxDistance;
        SerializedProperty enableErrorCallback;
        SerializedProperty targetAssetPath;
        SerializedProperty targetBankFolder;
        SerializedProperty bankRefreshCooldown;
        SerializedProperty showBankRefreshWindow;
        SerializedProperty eventLinkage;

        [NonSerialized]
        bool resourcesLoaded = false;

        GUIStyle mainHeaderStyle;
        GUIStyle sectionHeaderStyle;
        GUIStyle platformHeaderStyle;
        GUIStyle dropdownStyle;
        GUIStyle inheritedPropertyLabelStyle;
        GUIStyle overriddenPropertyLabelStyle;
        GUIStyle inheritedPropertyFoldoutStyle;
        GUIStyle overriddenPropertyFoldoutStyle;

        GUIContent mainHeaderIcon;

        Texture2D propertyOverrideIndicator;

        const int THREAD_AFFINITY_CORES_PER_ROW = 8;

        const string EditPlatformUndoMessage = "Edit FMOD Platform Properties";

        private PlatformPropertyStringListView staticPluginsView;
        private PlatformPropertyStringListView dynamicPluginsView;

        static readonly int[] LoggingValues = new int[] {
            (int)FMOD.DEBUG_FLAGS.NONE,
            (int)FMOD.DEBUG_FLAGS.ERROR,
            (int)FMOD.DEBUG_FLAGS.WARNING,
            (int)FMOD.DEBUG_FLAGS.LOG,
        };

        static readonly string[] LoggingDisplay = new string[] {
            "None",
            "Error",
            "Warning",
            "Log",
        };

        private ReorderableList banksToLoadView;

        PlatformsView platformsView;
        TreeViewState platformTreeViewState = new TreeViewState();

        string lastSourceBankPath;

        static readonly GUIContent BankRefreshLabel = new GUIContent("Refresh Banks");

        static readonly GUIContent[] BankRefreshCooldownLabels = new GUIContent[] {
            new GUIContent("After 1 second"),
            new GUIContent("After 5 seconds"),
            new GUIContent("After 10 seconds"),
            new GUIContent("After 20 seconds"),
            new GUIContent("After 30 seconds"),
            new GUIContent("After 1 minute"),
            new GUIContent("Prompt Me"),
            new GUIContent("Manually"),
        };

        static readonly int[] BankRefreshCooldownValues = new int[] {
            1,
            5,
            10,
            20,
            30,
            60,
            Settings.BankRefreshPrompt,
            Settings.BankRefreshManual,
        };

        internal enum SourceType : uint
        {
            FMODStudioProject = 0,
            SinglePlatformBuild,
            MultiplePlatformBuild
        }

        [Flags]
        enum Section
        {
            BankImport = 1 << 0,
            Initialization = 1 << 1,
            Behavior = 1 << 2,
            UserInterface = 1 << 3,
            PlatformSpecific = 1 << 4,
        }

        void OnEnable()
        {
            automaticEventLoading = serializedObject.FindProperty("AutomaticEventLoading");
            automaticSampleLoading = serializedObject.FindProperty("AutomaticSampleLoading");
            bankLoadType = serializedObject.FindProperty("BankLoadType");
            banksToLoad = serializedObject.FindProperty("BanksToLoad");
            enableMemoryTracking = serializedObject.FindProperty("EnableMemoryTracking");
            encryptionKey = serializedObject.FindProperty("EncryptionKey");
            hasSourceProject = serializedObject.FindProperty("HasSourceProject");
            hasPlatforms = serializedObject.FindProperty("HasPlatforms");
            importType = serializedObject.FindProperty("ImportType");
            loggingLevel = serializedObject.FindProperty("LoggingLevel");
            meterChannelOrdering = serializedObject.FindProperty("MeterChannelOrdering");
            sourceBankPath = serializedObject.FindProperty("sourceBankPath");
            sourceProjectPath = serializedObject.FindProperty("sourceProjectPath");
            stopEventsOutsideMaxDistance = serializedObject.FindProperty("StopEventsOutsideMaxDistance");
            enableErrorCallback = serializedObject.FindProperty("EnableErrorCallback");
            targetAssetPath = serializedObject.FindProperty("TargetAssetPath");
            targetBankFolder = serializedObject.FindProperty("TargetBankFolder");
            bankRefreshCooldown = serializedObject.FindProperty("BankRefreshCooldown");
            showBankRefreshWindow = serializedObject.FindProperty("ShowBankRefreshWindow");
            eventLinkage = serializedObject.FindProperty("EventLinkage");

            platformsView = new PlatformsView(target as Settings, platformTreeViewState);

            banksToLoadView = new ReorderableList(banksToLoad);
            banksToLoadView.onAddDropdownCallback = (rect, list) => {
                GenericMenu menu = new GenericMenu();

                menu.AddItem(new GUIContent("Browse..."), false, BrowseForBankToLoad);
                menu.AddItem(new GUIContent("Add All"), false, AddAllBanksToLoad);

                menu.DropDown(rect);
            };

            staticPluginsView = new PlatformPropertyStringListView(Platform.PropertyAccessors.StaticPlugins);
            dynamicPluginsView = new PlatformPropertyStringListView(Platform.PropertyAccessors.Plugins);

            Undo.undoRedoPerformed += OnUndoRedo;
        }

        void OnDestroy()
        {
            Undo.undoRedoPerformed -= OnUndoRedo;
        }

        void OnUndoRedo()
        {
            platformsView.ForceReload();

            RefreshBanks();

            Repaint();
        }

        void AffirmResources()
        {
            if (!resourcesLoaded)
            {
                resourcesLoaded = true;

                mainHeaderStyle = new GUIStyle(EditorStyles.label) {
                    fontStyle = FontStyle.Bold,
                    fontSize = 18,
                };
                mainHeaderStyle.normal.textColor = EditorGUIUtility.isProSkin ? Color.white : Color.black;

                sectionHeaderStyle = new GUIStyle(GUI.skin.FindStyle("Foldout")) {
                    fontStyle = FontStyle.Bold,
                };

                platformHeaderStyle = new GUIStyle(GUI.skin.label) {
                    richText = true,
                };

                dropdownStyle = new GUIStyle(GUI.skin.FindStyle("dropdownButton"));
                dropdownStyle.fixedHeight = 0;

                inheritedPropertyLabelStyle = GUI.skin.label;

                overriddenPropertyLabelStyle = new GUIStyle(inheritedPropertyLabelStyle) {
                    fontStyle = FontStyle.Bold,
                };

                inheritedPropertyFoldoutStyle = EditorStyles.foldout;

                overriddenPropertyFoldoutStyle = new GUIStyle(inheritedPropertyFoldoutStyle) {
                    fontStyle = FontStyle.Bold,
                };

                mainHeaderIcon = new GUIContent(EditorUtils.LoadImage("StudioIcon.png"));

                propertyOverrideIndicator = new Texture2D(2, 1);

                Color darkBlue;
                ColorUtility.TryParseHtmlString("#1974a5", out darkBlue);

                Color blue;
                ColorUtility.TryParseHtmlString("#0f81be", out blue);

                propertyOverrideIndicator.SetPixel(0, 0, darkBlue);
                propertyOverrideIndicator.SetPixel(1, 0, blue);

                propertyOverrideIndicator.Apply();
            }
        }

        Rect DrawPlatformPropertyLabel(string label, Platform platform,
            params Platform.PropertyOverrideControl[] properties)
        {
            PlatformPropertyLabelData data;
            PreparePlatformPropertyLabel(platform, properties, out data);

            GUI.Label(data.labelRect, label, data.overridden ? overriddenPropertyLabelStyle : inheritedPropertyLabelStyle);

            DecoratePlatformPropertyLabel(data, platform, properties);

            return data.remainderRect;
        }

        Rect DrawPlatformPropertyFoldout(string label, ref bool expand, Platform platform,
            params Platform.PropertyOverrideControl[] properties)
        {
            PlatformPropertyLabelData data;
            PreparePlatformPropertyLabel(platform, properties, out data);

            using (new NoIndentScope())
            {
                expand = EditorGUI.Foldout(data.labelRect, expand, label, true,
                    data.overridden ? overriddenPropertyFoldoutStyle : inheritedPropertyFoldoutStyle);
            }

            DecoratePlatformPropertyLabel(data, platform, properties);

            return data.remainderRect;
        }

        struct PlatformPropertyLabelData
        {
            public bool hasParent;
            public bool overridden;
            public Rect labelRect;
            public Rect remainderRect;
        }

        void PreparePlatformPropertyLabel(Platform platform, Platform.PropertyOverrideControl[] properties,
            out PlatformPropertyLabelData data)
        {
            AffirmResources();

            Rect rect = EditorGUILayout.GetControlRect();

            data.hasParent = (platform.Parent != null || platform is PlatformPlayInEditor);
            data.overridden = data.hasParent && properties.Any(p => p.HasValue(platform));
            data.labelRect = LabelRect(rect);
            data.remainderRect = new Rect(rect) { xMin = data.labelRect.xMax };
        }

        void DecoratePlatformPropertyLabel(PlatformPropertyLabelData data, Platform platform,
            Platform.PropertyOverrideControl[] properties)
        {
            if (data.hasParent)
            {
                if (data.overridden)
                {
                    Rect indicatorRect = new Rect(data.labelRect) { x = 1, width = 2 };
                    GUI.DrawTexture(indicatorRect, propertyOverrideIndicator);
                }

                if (Event.current.type == EventType.MouseUp
                    && Event.current.button == 1
                    && data.labelRect.Contains(Event.current.mousePosition))
                {
                    GenericMenu menu = new GenericMenu();

                    GUIContent revertContent = new GUIContent("Revert");

                    if (data.overridden)
                    {
                        menu.AddItem(revertContent, false, () => {
                            Undo.RecordObject(platform, "Revert FMOD Platform Properties");

                            foreach (Platform.PropertyOverrideControl property in properties)
                            {
                                property.Clear(platform);
                            }
                        });
                    }
                    else
                    {
                        menu.AddDisabledItem(revertContent);
                    }

                    menu.ShowAsContext();
                }
            }
        }

        static int DrawPopup(Rect position, int selectedIndex, string[] displayedOptions)
        {
            using (new NoIndentScope())
            {
                return EditorGUI.Popup(position, selectedIndex, displayedOptions);
            }
        }

        void DisplayTriStateBool(string label, Platform platform, Platform.PropertyAccessor<TriStateBool> property)
        {
            Rect rect = DrawPlatformPropertyLabel(label, platform, property);

            EditorGUI.BeginChangeCheck();

            int next = DrawPopup(rect, (int)property.Get(platform), ToggleDisplay);

            if (EditorGUI.EndChangeCheck())
            {
                property.Set(platform, (TriStateBool)next);
            }
        }

        void DisplayOutputMode(string label, Platform platform)
        {
            if (platform.ValidOutputTypes != null)
            {
                string[] valuesChild = new string[platform.ValidOutputTypes.Length + 3];
                string[] valuesChildEnum = new string[platform.ValidOutputTypes.Length + 3];
                valuesChild[0] = string.Format("Auto");
                valuesChild[1] = string.Format("No Sound");
                valuesChild[2] = string.Format("Wav Writer");
                valuesChildEnum[0] = Enum.GetName(typeof(FMOD.OUTPUTTYPE), FMOD.OUTPUTTYPE.AUTODETECT);
                valuesChildEnum[1] = Enum.GetName(typeof(FMOD.OUTPUTTYPE), FMOD.OUTPUTTYPE.NOSOUND);
                valuesChildEnum[2] = Enum.GetName(typeof(FMOD.OUTPUTTYPE), FMOD.OUTPUTTYPE.WAVWRITER);
                for (int i = 0; i < platform.ValidOutputTypes.Length; i++)
                {
                    valuesChild[i + 3] = platform.ValidOutputTypes[i].displayName;
                    valuesChildEnum[i + 3] = Enum.GetName(typeof(FMOD.OUTPUTTYPE), platform.ValidOutputTypes[i].outputType);
                }
                int currentIndex = Array.IndexOf(valuesChildEnum, platform.outputType);
                if (currentIndex == -1)
                {
                    currentIndex = 0;
                    platform.outputType = Enum.GetName(typeof(FMOD.OUTPUTTYPE), FMOD.OUTPUTTYPE.AUTODETECT);
                }
                int next = EditorGUILayout.Popup(label, currentIndex, valuesChild);
                platform.outputType = valuesChildEnum[next];
            }
        }

        void DisplayThreadAffinity(string label, Platform platform)
        {
            if (platform.CoreCount > 0 && DisplayThreadAffinityFoldout(label, platform))
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    DisplayThreadAffinityGroups(platform);
                }
            }
        }

        bool DisplayThreadAffinityFoldout(string label, Platform platform)
        {
            Rect headerRect = EditorGUILayout.GetControlRect();

            Rect labelRect = headerRect;
            labelRect.width = EditorGUIUtility.labelWidth;

            expandThreadAffinity = EditorGUI.Foldout(labelRect, expandThreadAffinity, label, true);

            bool useDefaults = !platform.ThreadAffinitiesProperty.HasValue;

            EditorGUI.BeginChangeCheck();

            Rect toggleRect = headerRect;
            toggleRect.xMin = labelRect.xMax;

            useDefaults = GUI.Toggle(toggleRect, useDefaults, "Use Defaults");

            if (EditorGUI.EndChangeCheck())
            {
                if (useDefaults)
                {
                    platform.ThreadAffinitiesProperty.Value.Clear();
                    platform.ThreadAffinitiesProperty.HasValue = false;
                }
                else
                {
                    platform.ThreadAffinitiesProperty.Value = new List<ThreadAffinityGroup>();
                    platform.ThreadAffinitiesProperty.HasValue = true;

                    foreach (ThreadAffinityGroup group in platform.DefaultThreadAffinities)
                    {
                        platform.ThreadAffinitiesProperty.Value.Add(new ThreadAffinityGroup(group));
                    }
                }
            }

            return expandThreadAffinity;
        }

        void DisplayThreadAffinityGroups(Platform platform)
        {
            GUIStyle affinityStyle = EditorStyles.miniButton;
            float affinityWidth = affinityStyle.CalcSize(new GUIContent("00")).x;

            GUIContent anyButtonContent = new GUIContent("Any");
            float anyButtonWidth = affinityStyle.CalcSize(anyButtonContent).x;

            float threadsWidth = EditorGUIUtility.labelWidth;
            float affinitiesWidth = affinityWidth * THREAD_AFFINITY_CORES_PER_ROW + anyButtonWidth;

            bool editable = platform.ThreadAffinitiesProperty.HasValue;

            if (platform.ThreadAffinities.Any())
            {
                DisplayThreadAffinitiesHeader(threadsWidth, affinitiesWidth);

                using (new EditorGUI.DisabledScope(!editable))
                {
                    ThreadAffinityGroup groupToDelete = null;

                    foreach (ThreadAffinityGroup group in platform.ThreadAffinities)
                    {
                        bool delete;
                        DisplayThreadAffinityGroup(group, platform, threadsWidth, affinitiesWidth,
                            anyButtonWidth, anyButtonContent, affinityStyle, affinityWidth, out delete);

                        if (delete)
                        {
                            groupToDelete = group;
                        }
                    }

                    if (groupToDelete != null)
                    {
                        platform.ThreadAffinitiesProperty.Value.Remove(groupToDelete);
                    }
                }
            }
            else
            {
                Rect messageRect = EditorGUILayout.GetControlRect();
                messageRect.width = threadsWidth + affinitiesWidth;
                messageRect = EditorGUI.IndentedRect(messageRect);

                GUI.Label(messageRect, "List is Empty");
            }

            if (editable)
            {
                Rect addButtonRect = EditorGUILayout.GetControlRect();
                addButtonRect.width = threadsWidth + affinitiesWidth;
                addButtonRect = EditorGUI.IndentedRect(addButtonRect);

                if (GUI.Button(addButtonRect, "Add"))
                {
                    platform.ThreadAffinitiesProperty.Value.Add(new ThreadAffinityGroup());
                }
            }
        }

        void DisplayThreadAffinitiesHeader(float threadsWidth, float affinitiesWidth)
        {
            Rect controlRect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight);

            Rect threadsRect = controlRect;
            threadsRect.width = threadsWidth;

            threadsRect = EditorGUI.IndentedRect(threadsRect);

            GUI.Label(threadsRect, "Threads");

            Rect coresRect = controlRect;
            coresRect.x = threadsRect.xMax;
            coresRect.width = affinitiesWidth;

            GUI.Label(coresRect, "Cores");
        }

        void DisplayThreadAffinityGroup(ThreadAffinityGroup group, Platform platform,
            float threadsWidth, float affinitiesWidth, float anyButtonWidth, GUIContent anyButtonContent,
            GUIStyle affinityStyle, float affinityWidth, out bool delete)
        {
            delete = false;

            GUIStyle editButtonStyle = EditorStyles.popup;

            GUIContent editButtonContent = new GUIContent("Edit");
            Rect editButtonRect = new Rect(Vector2.zero, editButtonStyle.CalcSize(editButtonContent));

            float threadsHeight = group.threads.Count * EditorGUIUtility.singleLineHeight;

            bool editable = platform.ThreadAffinitiesProperty.HasValue;

            if (editable)
            {
                 threadsHeight += EditorGUIUtility.standardVerticalSpacing + editButtonRect.height;
            }

            float affinitiesHeight =
                Mathf.Ceil(platform.CoreCount / (float)THREAD_AFFINITY_CORES_PER_ROW) * EditorGUIUtility.singleLineHeight;

            Rect controlRect = EditorGUILayout.GetControlRect(false, Math.Max(threadsHeight, affinitiesHeight));

            Rect threadsRect = controlRect;
            threadsRect.width = threadsWidth;

            threadsRect = EditorGUI.IndentedRect(threadsRect);

            GUIStyle boxStyle = EditorStyles.textArea;

            GUI.Box(threadsRect, string.Empty, boxStyle);

            Rect threadRect = threadsRect;
            threadRect.height = EditorGUIUtility.singleLineHeight;

            foreach (ThreadType thread in group.threads)
            {
                GUI.Label(threadRect, thread.DisplayName());
                threadRect.y += threadRect.height;
            }

            if (editable)
            {
                editButtonRect.y = threadsRect.yMax - editButtonRect.height - editButtonStyle.margin.bottom;
                editButtonRect.center = new Vector2(threadsRect.center.x, editButtonRect.center.y);

                if (EditorGUI.DropdownButton(editButtonRect, editButtonContent, FocusType.Passive, editButtonStyle))
                {
                    ThreadListEditor.Show(editButtonRect, group, platform, this);
                }
            }

            Rect affinitiesRect = controlRect;
            affinitiesRect.xMin = threadsRect.xMax;
            affinitiesRect.width = affinitiesWidth;

            GUI.Box(affinitiesRect, string.Empty, boxStyle);

            Rect anyButtonRect = affinitiesRect;
            anyButtonRect.height = affinitiesHeight;
            anyButtonRect.width = anyButtonWidth;

            if (GUI.Toggle(anyButtonRect, group.affinity == ThreadAffinity.Any, anyButtonContent, affinityStyle))
            {
                group.affinity = ThreadAffinity.Any;
            }

            Rect affinityRect = affinitiesRect;
            affinityRect.x = anyButtonRect.xMax;
            affinityRect.height = EditorGUIUtility.singleLineHeight;
            affinityRect.width = affinityWidth;

            for (int i = 0; i < platform.CoreCount; ++i)
            {
                ThreadAffinity mask = (ThreadAffinity)(1U << i);

                if (GUI.Toggle(affinityRect, (group.affinity & mask) == mask, i.ToString(), affinityStyle))
                {
                    group.affinity |= mask;
                }
                else
                {
                    group.affinity &= ~mask;
                }

                if (i % THREAD_AFFINITY_CORES_PER_ROW == THREAD_AFFINITY_CORES_PER_ROW - 1)
                {
                    affinityRect.x = anyButtonRect.xMax;
                    affinityRect.y += affinityRect.height;
                }
                else
                {
                    affinityRect.x += affinityRect.width;
                }
            }

            if (editable)
            {
                GUIStyle deleteButtonStyle = GUI.skin.button;
                GUIContent deleteButtonContent = new GUIContent("Delete");

                Rect deleteButtonRect = controlRect;
                deleteButtonRect.x = affinitiesRect.xMax;
                deleteButtonRect.width = deleteButtonStyle.CalcSize(deleteButtonContent).x;

                if (GUI.Button(deleteButtonRect, deleteButtonContent, deleteButtonStyle))
                {
                    delete = true;
                }
            }
        }

        class ThreadListEditor : EditorWindow
        {
            ThreadAffinityGroup group;
            Platform platform;
            Editor parent;

            public static void Show(Rect buttonRect, ThreadAffinityGroup group, Platform platform, Editor parent)
            {
                ThreadListEditor editor = CreateInstance<ThreadListEditor>();
                editor.group = group;
                editor.platform = platform;
                editor.parent = parent;

                Rect rect = new Rect(GUIUtility.GUIToScreenPoint(buttonRect.position), buttonRect.size);

                editor.ShowAsDropDown(rect, CalculateSize());
            }

            private static GUIStyle FrameStyle { get { return GUI.skin.box; } }
            private static GUIStyle ThreadStyle { get { return EditorStyles.toggle; } }

            private static Vector2 CalculateSize()
            {
                Vector2 result = Vector2.zero;

                Array enumValues = Enum.GetValues(typeof(ThreadType));

                foreach (ThreadType thread in enumValues)
                {
                    Vector2 size = ThreadStyle.CalcSize(new GUIContent(thread.DisplayName()));
                    result.x = Mathf.Max(result.x, size.x);
                }

                result.y = enumValues.Length * EditorGUIUtility.singleLineHeight
                    + (enumValues.Length - 1) * EditorGUIUtility.standardVerticalSpacing;

                result.x += FrameStyle.padding.horizontal;
                result.y += FrameStyle.padding.vertical;

                return result;
            }

            private void OnGUI()
            {
                Rect frameRect = new Rect(0, 0, position.width, position.height);

                GUI.Box(frameRect, string.Empty, FrameStyle);

                Rect threadRect = FrameStyle.padding.Remove(frameRect);
                threadRect.height = EditorGUIUtility.singleLineHeight;

                foreach (ThreadType thread in Enum.GetValues(typeof(ThreadType)))
                {
                    EditorGUI.BeginChangeCheck();

                    bool include = EditorGUI.ToggleLeft(threadRect, thread.DisplayName(), group.threads.Contains(thread));

                    if (EditorGUI.EndChangeCheck())
                    {
                        Undo.RecordObject(platform, EditPlatformUndoMessage);

                        if (include)
                        {
                            // Make sure each thread is only in one group
                            foreach (ThreadAffinityGroup other in platform.ThreadAffinities)
                            {
                                other.threads.Remove(thread);
                            }

                            group.threads.Add(thread);
                            group.threads.Sort();
                        }
                        else
                        {
                            group.threads.Remove(thread);
                        }

                        parent.Repaint();
                    }

                    threadRect.y = threadRect.yMax + EditorGUIUtility.standardVerticalSpacing;
                }
            }
        }

        void DisplaySampleRate(string label, Platform platform)
        {
            Platform.PropertyAccessor<int> property = Platform.PropertyAccessors.SampleRate;

            Rect rect = DrawPlatformPropertyLabel(label, platform, property);

            EditorGUI.BeginChangeCheck();

            int currentIndex = Math.Max(0, Array.IndexOf(FrequencyValues, platform.SampleRate));
            int nextIndex = DrawPopup(rect, currentIndex, FrequencyDisplay);

            if (EditorGUI.EndChangeCheck())
            {
                property.Set(platform, FrequencyValues[nextIndex]);
            }
        }

        void DisplayProjectPlatform(string label, Platform platform)
        {
            Rect rect = DrawPlatformPropertyLabel(label, platform,
                Platform.PropertyAccessors.BuildDirectory, Platform.PropertyAccessors.SpeakerMode);

            int speakerModeIndex = Math.Max(0, Array.IndexOf(SpeakerModeValues, platform.SpeakerMode));
            string speakerModeName = SpeakerModeDisplay[speakerModeIndex];

            if (GUI.Button(rect, string.Format("{0} ({1})", platform.BuildDirectory, speakerModeName)))
            {
                PopupWindow.Show(rect, new ProjectPlatformSelector(platform, this));
            }
        }

        class ProjectPlatformSelector : PopupWindowContent
        {
            private Platform platform;
            private SettingsEditor settingsEditor;
            private string[] outputSubdirectories;

            private GUIStyle headerStyle;
            private GUIStyle toggleStyle;

            private GUIContent subdirectoryHeader = new GUIContent("Output sub-directory:");
            private GUIContent speakerModeHeader = new GUIContent("Surround speaker mode:");

            const string HelpText = "Select the output sub-directory and speaker mode that match the project " +
                "platform settings in the FMOD Studio build preferences.";
            const string UndoText = "Edit FMOD Platform Settings";

            const float InterColumnSpace = 25;

            Vector2 subdirectorySize;
            Vector2 speakerModeSize;
            Vector2 helpButtonSize;

            Vector2 windowSize;

            public ProjectPlatformSelector(Platform platform, SettingsEditor settingsEditor)
            {
                this.platform = platform;
                this.settingsEditor = settingsEditor;

                headerStyle = GUI.skin.label;

                toggleStyle = new GUIStyle(EditorStyles.radioButton);
                toggleStyle.margin.left = headerStyle.margin.left + 10;

                outputSubdirectories = EditorUtils.GetBankPlatforms();

                Vector2 subdirectoryHeaderSize = headerStyle.CalcSize(subdirectoryHeader);

                subdirectorySize = ToggleGroupSize(outputSubdirectories);
                subdirectorySize.x = Math.Max(subdirectoryHeaderSize.x, subdirectorySize.x);
                subdirectorySize.y += subdirectoryHeaderSize.y + headerStyle.margin.bottom;

                Vector2 speakerModeHeaderSize = headerStyle.CalcSize(speakerModeHeader);

                speakerModeSize = ToggleGroupSize(SpeakerModeDisplay);
                speakerModeSize.x = Math.Max(speakerModeHeaderSize.x, speakerModeSize.x);
                speakerModeSize.y += speakerModeHeaderSize.y + headerStyle.margin.bottom;

                helpButtonSize = EditorUtils.GetHelpButtonSize();

                float width = headerStyle.margin.left + subdirectorySize.x + InterColumnSpace + speakerModeSize.x
                    + helpButtonSize.x;
                float height = Math.Max(subdirectorySize.y, speakerModeSize.y);

                windowSize = new Vector2(width, height);
            }

            private Vector2 ToggleGroupSize(IEnumerable<string> labels)
            {
                Vector2 totalSize = Vector2.zero;

                foreach (string label in labels)
                {
                    Vector2 size = toggleStyle.CalcSize(new GUIContent(label));

                    totalSize.x = Math.Max(totalSize.x, size.x);
                    totalSize.y += size.y + toggleStyle.margin.top;
                }

                totalSize.y += toggleStyle.margin.bottom;

                return totalSize;
            }

            public override Vector2 GetWindowSize()
            {
                return windowSize;
            }

            public override void OnGUI(Rect rect)
            {
                float y = rect.y + headerStyle.margin.top;

                Rect subdirectoryRect = new Rect(rect.x + headerStyle.margin.left, y, subdirectorySize.x, rect.height);

                using (new GUILayout.AreaScope(subdirectoryRect))
                {
                    GUILayout.Label(subdirectoryHeader, headerStyle);

                    foreach (string buildDirectory in outputSubdirectories)
                    {
                        bool selected = (platform.BuildDirectory == buildDirectory);

                        EditorGUI.BeginChangeCheck();

                        selected = GUILayout.Toggle(selected, buildDirectory, toggleStyle);

                        if (EditorGUI.EndChangeCheck() && selected)
                        {
                            Undo.RecordObject(platform, UndoText);

                            Platform.PropertyAccessors.BuildDirectory.Set(platform, buildDirectory);

                            // Ensure SpeakerMode is also overridden
                            Platform.PropertyAccessors.SpeakerMode.Set(platform, platform.SpeakerMode);

                            settingsEditor.Repaint();
                        }
                    }
                }

                Rect speakerModeRect = new Rect(subdirectoryRect.xMax + InterColumnSpace, y, speakerModeSize.x, rect.height);

                using (new GUILayout.AreaScope(speakerModeRect))
                {
                    GUILayout.Label(speakerModeHeader, headerStyle);

                    for (int i = 0; i < SpeakerModeValues.Length; ++i)
                    {
                        bool selected = (platform.SpeakerMode == SpeakerModeValues[i]);

                        EditorGUI.BeginChangeCheck();

                        selected = GUILayout.Toggle(selected, SpeakerModeDisplay[i], toggleStyle);

                        if (EditorGUI.EndChangeCheck() && selected)
                        {
                            Undo.RecordObject(platform, UndoText);

                            Platform.PropertyAccessors.SpeakerMode.Set(platform, SpeakerModeValues[i]);

                            // Ensure BuildDirectory is also overridden
                            Platform.PropertyAccessors.BuildDirectory.Set(platform, platform.BuildDirectory);

                            settingsEditor.Repaint();
                        }
                    }
                }

                Rect helpButtonRect = new Rect(speakerModeRect.xMax, y, helpButtonSize.x, helpButtonSize.y);
                EditorUtils.DrawHelpButton(helpButtonRect, () => new SimpleHelp(HelpText));
            }
        }

        void DisplaySpeakerMode(string label, Platform platform)
        {
            const string HelpText = "Select the speaker mode that matches the project " +
                "platform settings in the FMOD Studio build preferences.";

            Rect rect = EditorUtils.DrawHelpButtonLayout(() => new SimpleHelp(HelpText));

            Rect labelRect = LabelRect(rect);

            GUI.Label(labelRect, label);

            Rect speakerModeRect = rect;
            speakerModeRect.xMin = labelRect.xMax;

            int currentIndex = Math.Max(0, Array.IndexOf(SpeakerModeValues, platform.SpeakerMode));

            int next = DrawPopup(speakerModeRect, currentIndex, SpeakerModeDisplay);

            Platform.PropertyAccessors.SpeakerMode.Set(platform, SpeakerModeValues[next]);
        }

        void DisplayCallbackHandler(string label, Platform platform)
        {
            Platform.PropertyAccessor<PlatformCallbackHandler> property = Platform.PropertyAccessors.CallbackHandler;

            Rect rect = DrawPlatformPropertyLabel(label, platform, property);

            using (new NoIndentScope())
            {
                EditorGUI.BeginChangeCheck();

                PlatformCallbackHandler next = EditorGUI.ObjectField(rect, property.Get(platform),
                    typeof(PlatformCallbackHandler), false) as PlatformCallbackHandler;

                if (EditorGUI.EndChangeCheck())
                {
                    property.Set(platform, next);
                }
            }
        }

        void DisplayInt(string label, Platform platform, Platform.PropertyAccessor<int> property, int min, int max)
        {
            int currentValue = property.Get(platform);

            Rect rect = DrawPlatformPropertyLabel(label, platform, property);

            using (new NoIndentScope())
            {
                EditorGUI.BeginChangeCheck();

                int next = EditorGUI.IntSlider(rect, currentValue, min, max);

                if (EditorGUI.EndChangeCheck())
                {
                    property.Set(platform, next);
                }
            }
        }

        void DisplayLiveUpdatePort(string label, Platform platform, Platform.PropertyAccessor<int> property)
        {
            Rect rect = DrawPlatformPropertyLabel(label, platform, property);

            GUIContent resetContent = new GUIContent("Reset");

            Rect resetRect = rect;
            resetRect.xMin = resetRect.xMax - GUI.skin.button.CalcSize(resetContent).x;

            Rect textRect = rect;
            textRect.xMax = resetRect.xMin;

            using (new NoIndentScope())
            {
                EditorGUI.BeginChangeCheck();

                int next = EditorGUI.IntField(textRect, property.Get(platform));

                if (GUI.Button(resetRect, resetContent))
                {
                    next = 9264;
                }

                if (EditorGUI.EndChangeCheck())
                {
                    property.Set(platform, next);
                }
            }
        }

        private void DisplayPlatform(Platform platform)
        {
            if (!platform.Active)
            {
                return;
            }

            DisplayPlatformHeader(platform);

            Undo.RecordObject(platform, EditPlatformUndoMessage);

            Settings settings = target as Settings;

            using (new EditorGUI.IndentLevelScope())
            {
                EditorGUI.BeginChangeCheck();
                DisplayTriStateBool("Live Update", platform, Platform.PropertyAccessors.LiveUpdate);

                if (platform.IsLiveUpdateEnabled)
                {
                    DisplayLiveUpdatePort("Live Update Port", platform, Platform.PropertyAccessors.LiveUpdatePort);
                }

                DisplayTriStateBool("Debug Overlay", platform, Platform.PropertyAccessors.Overlay);
                DisplayOutputMode("Output Mode", platform);
                DisplaySampleRate("Sample Rate", platform);

                if (settings.HasPlatforms)
                {
                    DisplayProjectPlatform("Project Platform", platform);
                }
                else if (platform is PlatformDefault)
                {
                    DisplaySpeakerMode("Speaker Mode", platform);
                }

                DisplayCallbackHandler("Callback Handler", platform);

                if (!(platform is PlatformPlayInEditor))
                {
                    DisplayInt("Virtual Channel Count", platform, Platform.PropertyAccessors.VirtualChannelCount, 1, 2048);
                    DisplayInt("Real Channel Count", platform, Platform.PropertyAccessors.RealChannelCount, 1, 256);
                    DisplayDSPBufferSettings(platform);

                    string warning = null;

                    BuildTargetGroup buildTargetGroup =
                        BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
                    ScriptingImplementation scriptingBackend = PlayerSettings.GetScriptingBackend(buildTargetGroup);

                    if (scriptingBackend != ScriptingImplementation.IL2CPP)
                    {
                        warning = "Only supported on the IL2CPP scripting backend";
                    }

                    DisplayPlugins("Static Plugins", staticPluginsView, platform, ref expandStaticPlugins, warning);
                }

                DisplayPlugins("Dynamic Plugins", dynamicPluginsView, platform, ref expandDynamicPlugins);

                DisplayThreadAffinity("Thread Affinity", platform);

                if (EditorGUI.EndChangeCheck())
                {
                    EditorUtility.SetDirty(target);
                }
            }
        }

        private void DisplayPlatformHeader(Platform platform)
        {
            string type;

            if (platform is PlatformGroup)
            {
                type = "platform group";
            }
            else if (platform.IsIntrinsic)
            {
                type = "built-in platform";
            }
            else
            {
                type = "platform";
            }

            if (platform.Parent != null || platform is PlatformPlayInEditor)
            {
                Platform parent;
                GUIContent labelContent;

                if (platform is PlatformPlayInEditor)
                {
                    labelContent = new GUIContent(string.Format("<b>{0}</b>: {1} inheriting from Unity build target: ",
                        platform.DisplayName, type));
                    parent = EditorSettings.Instance.CurrentEditorPlatform;

                    while (!parent.Active)
                    {
                        parent = parent.Parent;
                    }
                }
                else
                {
                    labelContent = new GUIContent(string.Format("<b>{0}</b>: {1} inheriting from", platform.DisplayName, type));
                    parent = platform.Parent;
                }

                Rect rect = EditorGUILayout.GetControlRect();

                GUIContent buttonContent = new GUIContent(string.Format("<b>{0}</b>", parent.DisplayName));
                GUIContent iconContent = EditorGUIUtility.IconContent("UnityEditor.FindDependencies");

                Rect labelRect = LabelRect(rect);
                labelRect.width = platformHeaderStyle.CalcSize(labelContent).x;

                Rect buttonRect = rect;
                buttonRect.x = labelRect.xMax;
                buttonRect.width = platformHeaderStyle.CalcSize(buttonContent).x;

                Rect iconRect = rect;
                iconRect.x = buttonRect.xMax;
                iconRect.width = iconContent.image.width;
                iconRect.height = iconContent.image.height;
                iconRect.y += (rect.height - iconRect.height) / 2;

                buttonRect.width += iconRect.width;

                GUI.Label(labelRect, labelContent, platformHeaderStyle);

                if (GUI.Button(buttonRect, buttonContent, platformHeaderStyle))
                {
                    platformsView.SelectAndFramePlatform(parent);
                }

                if (Event.current.type == EventType.Repaint)
                {
                    GUI.DrawTexture(iconRect, iconContent.image);
                }

                EditorGUIUtility.AddCursorRect(buttonRect, MouseCursor.Link);
            }
            else
            {
                string text = string.Format("<b>{0}</b>: {1}", platform.DisplayName, type);
                EditorGUILayout.LabelField(text, platformHeaderStyle);
            }
        }

        private void DisplayDSPBufferSettings(Platform platform)
        {
            Rect rect = DrawPlatformPropertyLabel("DSP Buffer Settings", platform,
                Platform.PropertyAccessors.DSPBufferLength, Platform.PropertyAccessors.DSPBufferCount);

            bool useAutoDSPBufferSettings = DisplayAutoDSPBufferSettings(rect, platform);

            if (!useAutoDSPBufferSettings)
            {
                DisplayDSPBufferFields(platform);
            }
        }

        private bool DisplayAutoDSPBufferSettings(Rect rect, Platform platform)
        {
            Platform.PropertyAccessor<int> lengthProperty = Platform.PropertyAccessors.DSPBufferLength;
            Platform.PropertyAccessor<int> countProperty = Platform.PropertyAccessors.DSPBufferCount;

            GUIStyle style = GUI.skin.toggle;

            GUIContent content = new GUIContent("Auto");
            rect.width = style.CalcSize(content).x;

            bool useAutoDSPBufferSettings = lengthProperty.Get(platform) == 0 && countProperty.Get(platform) == 0;

            EditorGUI.BeginChangeCheck();

            useAutoDSPBufferSettings = GUI.Toggle(rect, useAutoDSPBufferSettings, content, style);

            if (EditorGUI.EndChangeCheck())
            {
                if (useAutoDSPBufferSettings)
                {
                    lengthProperty.Set(platform, 0);
                    countProperty.Set(platform, 0);

                }
                else
                {
                    // set a helpful default value (real default is 0 for auto behaviour)
                    lengthProperty.Set(platform, 512);
                    countProperty.Set(platform, 4);
                }
            }

            return useAutoDSPBufferSettings;
        }

        private void DisplayDSPBufferFields(Platform platform)
        {
            Platform.PropertyAccessor<int> lengthProperty = Platform.PropertyAccessors.DSPBufferLength;
            Platform.PropertyAccessor<int> countProperty = Platform.PropertyAccessors.DSPBufferCount;

            using (new EditorGUI.IndentLevelScope())
            {
                EditorGUI.BeginChangeCheck();

                int nextLength = Mathf.Max(EditorGUILayout.IntField("DSP Buffer Length", lengthProperty.Get(platform)), 8);
                int nextCount = Mathf.Max(EditorGUILayout.IntField("DSP Buffer Count", countProperty.Get(platform)), 2);

                if (EditorGUI.EndChangeCheck())
                {
                    lengthProperty.Set(platform, nextLength);
                    countProperty.Set(platform, nextCount);
                }
            }
        }

        private void DisplayPlugins(string title, PlatformPropertyStringListView view, Platform platform,
            ref bool expand, string warning = null)
        {
            List<string> plugins = view.property.Get(platform);

            string fullTitle = string.Format("{0}: {1}", title, plugins.Count);

            DrawPlatformPropertyFoldout(fullTitle, ref expand, platform, view.property);

            if (expand)
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    if (warning != null)
                    {
                        EditorGUILayout.HelpBox(warning, MessageType.Warning);
                    }

                    view.platform = platform;
                    view.DrawLayout();
                }
            }
        }

        protected override void OnHeaderGUI()
        {
            AffirmResources();

            GUIContent text = new GUIContent("FMOD Settings");

            Vector2 textSize = mainHeaderStyle.CalcSize(text);
            Vector2 iconSize = GUI.skin.label.CalcSize(mainHeaderIcon);

            Rect rect = EditorGUILayout.GetControlRect(false, Math.Max(textSize.y, iconSize.y));

            Rect iconRect = rect;
            iconRect.width = iconSize.x;
            iconRect.height = iconSize.y;
            iconRect.y += (rect.height - iconRect.height) / 2;

            Rect textRect = rect;
            textRect.xMin = iconRect.xMax;
            textRect.height = textSize.y;
            textRect.y += (rect.height - textRect.height) / 2;

            GUI.Label(iconRect, mainHeaderIcon);
            GUI.Label(textRect, text, mainHeaderStyle);
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            string invalidSourceMessage = CheckValidSource();

            DrawImportSection(invalidSourceMessage);

            if (invalidSourceMessage != null)
            {
                serializedObject.ApplyModifiedProperties();
                return;
            }

            EditorGUILayout.Separator();

            DrawInitializationSection();

            EditorGUILayout.Separator();

            DrawBehaviorSection();

            EditorGUILayout.Separator();

            DrawUserInterfaceSection();

            EditorGUILayout.Separator();

            DrawPlatforms();

            serializedObject.ApplyModifiedProperties();

            ApplyPendingActions();
        }

        private bool DrawSectionHeaderLayout(Section section, string title)
        {
            Rect rect = EditorGUILayout.GetControlRect();

            return DrawSectionHeader(rect, section, title);
        }

        private bool DrawSectionHeader(Rect rect, Section section, string title)
        {
            AffirmResources();

            bool expanded = (section & sExpandedSections) == section;

            expanded = EditorGUI.Foldout(rect, expanded, title, true, sectionHeaderStyle);

            sExpandedSections = expanded ? (sExpandedSections | section) : (sExpandedSections & ~section);

            return expanded;
        }

        private void DrawImportSection(string invalidSourceMessage)
        {
            if (DrawSectionHeaderLayout(Section.BankImport, "Bank Import"))
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    DrawSourceSelection(invalidSourceMessage);

                    if (invalidSourceMessage != null)
                    {
                        return;
                    }

                    DrawTargetSelection();
                }
            }
        }

        private void DrawSourceSelection(string invalidSourceMessage)
        {
            Rect popupRect = EditorUtils.DrawHelpButtonLayout(() => new SourceSelectionHelp());

            hasBankSourceChanged = false;

            SourceType sourceType = hasSourceProject.boolValue
                ? SourceType.FMODStudioProject
                : (hasPlatforms.boolValue ? SourceType.MultiplePlatformBuild : SourceType.SinglePlatformBuild);

            sourceType = (SourceType)EditorGUI.EnumPopup(popupRect, "Source Type", sourceType);

            if (sourceType == SourceType.FMODStudioProject)
            {
                string oldPath = sourceProjectPath.stringValue;

                using (new EditorGUILayout.HorizontalScope())
                {
                    EditorGUI.BeginChangeCheck();
                    string newPath = EditorGUILayout.TextField("Studio Project Path", sourceProjectPath.stringValue);
                    if (EditorGUI.EndChangeCheck())
                    {
                        if (newPath.EndsWith(".fspro"))
                        {
                            sourceProjectPath.stringValue = newPath;
                        }
                    }

                    if (GUILayout.Button("Browse", GUILayout.ExpandWidth(false)))
                    {
                        GUI.FocusControl(null);
                        EditorApplication.delayCall += BrowseForSourceProjectPathAndRefresh;
                    }
                }

                // Cache in settings for runtime access in play-in-editor mode
                sourceBankPath.stringValue = GetBankDirectory(serializedObject);
                hasPlatforms.boolValue = true;
                hasSourceProject.boolValue = true;

                // First time project path is set or changes, copy to streaming assets
                if (sourceProjectPath.stringValue != oldPath)
                {
                    hasBankSourceChanged = true;
                }
            }
            else if (sourceType == SourceType.SinglePlatformBuild || sourceType == SourceType.MultiplePlatformBuild)
            {
                string oldPath = sourceBankPath.stringValue;

                using (new EditorGUILayout.HorizontalScope())
                {
                    EditorGUILayout.PropertyField(sourceBankPath, new GUIContent("Build Path"));

                    if (GUILayout.Button("Browse", GUILayout.ExpandWidth(false)))
                    {
                        GUI.FocusControl(null);
                        EditorApplication.delayCall += BrowseForSourceBankPathAndRefresh;
                    }
                }

                hasPlatforms.boolValue = (sourceType == SourceType.MultiplePlatformBuild);
                hasSourceProject.boolValue = false;

                // First time project path is set or changes, copy to streaming assets
                if (sourceBankPath.stringValue != oldPath)
                {
                    hasBankSourceChanged = true;
                }
            }

            if (invalidSourceMessage != null)
            {
                EditorGUILayout.HelpBox(invalidSourceMessage, MessageType.Error, true);
            }
        }

        private void BrowseForSourceProjectPathAndRefresh()
        {
            if (BrowseForSourceProjectPath(serializedObject))
            {
                Repaint();
            }
        }

        private void BrowseForSourceBankPathAndRefresh()
        {
            if (BrowseForSourceBankPath(serializedObject))
            {
                Repaint();
            }
        }

        private string CheckValidSource()
        {
            bool validSource;
            string invalidMessage;
            EditorUtils.ValidateSource(out validSource, out invalidMessage);

            if (validSource)
            {
                return null;
            }
            else
            {
                sExpandedSections |= Section.BankImport;

                return invalidMessage + "\n\nFor detailed setup instructions, please see the FMOD/Help/Getting Started menu item.";
            }
        }

        class SourceSelectionHelp : HelpContent
        {
            private GUIStyle style;

            readonly GUIContent introduction = new GUIContent("Choose how to access your FMOD Studio content:");

            private readonly ListEntry[] listEntries = {
                new ListEntry("FMOD Studio Project",
                    "If you have the complete FMOD Studio project."
                ),
                new ListEntry("Single Platform Build",
                    "If you have the contents of the <b>Build</b> folder for a single platform."
                ),
                new ListEntry("Multiple Platform Build",
                    "If you have the contents of the <b>Build</b> folder for multiple platforms, " +
                    "with each platform in its own subdirectory."
                ),
            };

            protected override void Prepare()
            {
                style = new GUIStyle(GUI.skin.label) {
                    richText = true,
                    wordWrap = true,
                };
            }

            struct ListEntry
            {
                public ListEntry(string label, string description)
                {
                    this.label = new GUIContent(label);
                    this.description = new GUIContent(description);
                }

                public GUIContent label;
                public GUIContent description;
            }

            protected override Vector2 GetContentSize()
            {
                Vector2 size = new Vector2(440, 0);

                size.y += style.margin.top;
                size.y += style.CalcHeight(introduction, size.x);

                foreach (ListEntry entry in listEntries)
                {
                    size.y += style.margin.top;
                    size.y += style.CalcHeight(entry.description, size.x - EditorGUIUtility.labelWidth);
                }

                size.y += style.margin.bottom;

                return size;
            }

            protected override void DrawContent()
            {
                EditorGUILayout.LabelField(introduction, style);

                using (new EditorGUI.IndentLevelScope())
                {
                    foreach (ListEntry entry in listEntries)
                    {
                        EditorGUILayout.LabelField(entry.label, entry.description, style);
                    }
                }
            }
        }

        private static string GetBankDirectory(SerializedObject serializedObject)
        {
            var sourceProjectPath = serializedObject.FindProperty("sourceProjectPath");
            var sourceBankPath = serializedObject.FindProperty("sourceBankPath");
            var hasSourceProject = serializedObject.FindProperty("HasSourceProject");

            if (hasSourceProject.boolValue && !string.IsNullOrEmpty(sourceProjectPath.stringValue))
            {
                string projectFolder = Path.GetDirectoryName(sourceProjectPath.stringValue);
                return RuntimeUtils.GetCommonPlatformPath(Path.Combine(projectFolder, EditorUtils.BuildFolder));
            }
            else if (!string.IsNullOrEmpty(sourceBankPath.stringValue))
            {
                return RuntimeUtils.GetCommonPlatformPath(Path.GetFullPath(sourceBankPath.stringValue));
            }
            else
            {
                return null;
            }
        }

        private void DrawTargetSelection()
        {
            Settings settings = target as Settings;

            hasBankTargetChanged = false;

            string[] importTypeNames = importType.enumDisplayNames;
            int importTypeIndex = importType.enumValueIndex;

            int newImportTypeIndex = EditorGUILayout.Popup("Import Type", importTypeIndex, importTypeNames);

            if (newImportTypeIndex != importType.enumValueIndex)
            {
                bool deleteBanks = EditorUtility.DisplayDialog(
                    "FMOD Bank Import Type Changed",
                    "Do you want to delete the " + importTypeNames[importTypeIndex] + " banks in " + settings.TargetPath,
                    "Yes", "No");

                if (deleteBanks)
                {
                    // Delete the old banks
                    EventManager.RemoveBanks(settings.TargetPath);
                }

                hasBankTargetChanged = true;
                importType.enumValueIndex = newImportTypeIndex;
            }

            // ----- Asset Sub Directory -------------
            SerializedProperty targetSubFolder;
            string label;

            if (importType.intValue == (int)ImportType.AssetBundle)
            {
                targetSubFolder = targetAssetPath;
                label = "FMOD Asset Sub Folder";
            }
            else
            {
                targetSubFolder = targetBankFolder;
                label = "FMOD Bank Sub Folder";
            }

            string newSubFolder = EditorGUILayout.DelayedTextField(label, targetSubFolder.stringValue);

            if (newSubFolder != targetSubFolder.stringValue)
            {
                EventManager.RemoveBanks(settings.TargetPath);
                targetSubFolder.stringValue = newSubFolder;
                hasBankTargetChanged = true;
            }

            DisplayBankRefreshSettings(bankRefreshCooldown, showBankRefreshWindow, true);

            EditorGUILayout.PropertyField(eventLinkage);
        }

        private void DrawBehaviorSection()
        {
            if (DrawSectionHeaderLayout(Section.Behavior, "Behavior"))
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    EditorGUILayout.PropertyField(stopEventsOutsideMaxDistance,
                        new GUIContent("Stop Events Outside Max Distance"));
                }
            }
        }

        private void DrawUserInterfaceSection()
        {
            if (DrawSectionHeaderLayout(Section.UserInterface, "User Interface"))
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    EditorGUI.BeginChangeCheck();

                    EditorGUILayout.PropertyField(meterChannelOrdering, new GUIContent("Meter Channel Ordering"));

                    if (EditorGUI.EndChangeCheck() && EventBrowser.IsOpen)
                    {
                        EditorWindow.GetWindow<EventBrowser>("FMOD Events", false).Repaint();
                    }
                }
            }
        }

        private void DrawInitializationSection()
        {
            if (DrawSectionHeaderLayout(Section.Initialization, "Initialization"))
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    loggingLevel.intValue = EditorGUILayout.IntPopup("Logging Level",
                        loggingLevel.intValue, LoggingDisplay, LoggingValues);

                    EditorGUILayout.PropertyField(enableErrorCallback,
                        new GUIContent("Enable API Error Logging"));

                    EditorGUILayout.PropertyField(enableMemoryTracking, new GUIContent("Enable Memory Tracking"));

                    using (new EditorGUI.DisabledScope(importType.intValue == (int)ImportType.AssetBundle))
                    {
                        EditorGUILayout.PropertyField(bankLoadType, new GUIContent("Load Banks"));

                        switch ((BankLoadType)bankLoadType.intValue)
                        {
                            case BankLoadType.All:
                                break;
                            case BankLoadType.Specified:
                                automaticEventLoading.boolValue = false;
                                DisplayBanksToLoad();
                                break;
                            case BankLoadType.None:
                                automaticEventLoading.boolValue = false;
                                break;
                            default:
                                break;
                        }

                        using (new EditorGUI.DisabledScope(bankLoadType.intValue == (int)BankLoadType.None))
                        {
                            EditorGUILayout.PropertyField(automaticSampleLoading, new GUIContent("Load Bank Sample Data"));
                        }

                        EditorGUILayout.DelayedTextField(encryptionKey, new GUIContent("Bank Encryption Key"));
                    }
                }
            }
        }

        private void DisplayBanksToLoad()
        {
            banksToLoad.isExpanded = EditorGUILayout.Foldout(banksToLoad.isExpanded, "Specified Banks", true);

            if (banksToLoad.isExpanded)
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    banksToLoadView.DrawLayout();
                }
            }
        }

        private void BrowseForBankToLoad()
        {
            string bankDirectory = CurrentBankDirectory();
            string path = EditorUtility.OpenFilePanel("Locate Bank", bankDirectory, "bank");

            if (!string.IsNullOrEmpty(path))
            {
                serializedObject.Update();

                path = RuntimeUtils.GetCommonPlatformPath(path);
                path = path.Replace(bankDirectory, "");
                path = Regex.Replace(path, "\\.bank$", "");

                banksToLoad.ArrayAdd(p => p.stringValue = path);

                serializedObject.ApplyModifiedProperties();

                Repaint();
            }
        }

        private void AddAllBanksToLoad()
        {
            string sourceDir = CurrentBankDirectory();
            string[] banksFound = Directory.GetFiles(sourceDir, "*.bank", SearchOption.AllDirectories);

            serializedObject.Update();

            for (int i = 0; i < banksFound.Length; i++)
            {
                string bankLongName = RuntimeUtils.GetCommonPlatformPath(Path.GetFullPath(banksFound[i]));
                string bankShortName = bankLongName.Replace(sourceDir, "");
                bankShortName = Regex.Replace(bankShortName, "\\.bank$", "");

                if (!banksToLoad.ArrayContains(p => p.stringValue == bankShortName))
                {
                    banksToLoad.ArrayAdd(p => p.stringValue = bankShortName);
                }
            }

            serializedObject.ApplyModifiedProperties();

            Repaint();
        }

        private string CurrentBankDirectory()
        {
            Settings settings = target as Settings;

            string bankDirectory;

            if (settings.HasPlatforms)
            {
                bankDirectory = string.Format("{0}/{1}/",
                    settings.SourceBankPath, EditorSettings.Instance.CurrentEditorPlatform.BuildDirectory);
            }
            else
            {
                bankDirectory = settings.SourceBankPath + "/";
            }

            return RuntimeUtils.GetCommonPlatformPath(Path.GetFullPath(bankDirectory));
        }

        private void DrawPlatforms()
        {
            platformsView.ReloadIfNecessary();

            if (DrawSectionHeaderLayout(Section.PlatformSpecific, "Platform Specific"))
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    platformsView.DrawLayout();

                    Platform selectedPlatform = platformsView.SelectedPlatform;

                    if (selectedPlatform != null)
                    {
                        DisplayPlatform(selectedPlatform);
                    }
                }
            }
        }

        private class PlatformsView : TreeView
        {
            const float RowPadding = 2;

            private Settings settings;

            static UnityEditorInternal.ReorderableList.Defaults s_Defaults;

            const float HeaderHeight = 3;
            const float BodyHeight = 150;
            const float FooterHeight = 13;
            const float TotalHeight = HeaderHeight + BodyHeight + FooterHeight;

            const float ButtonWidth = 25;
            const float ButtonHeight = 16;
            const float ButtonMarginTop = 0;

            const float FooterMarginRight = 10;

            static readonly RectOffset BodyPadding = new RectOffset(1, 2, 1, 4);
            static readonly RectOffset FooterPadding = new RectOffset(4, 4, 0, 0);

            static readonly Vector2 DragHandleSize = new Vector2(10, 7);
            static readonly Vector2 DragHandlePadding = new Vector2(5, 6);

            public PlatformsView(Settings settings, TreeViewState state) : base(state)
            {
                this.settings = settings;
                rowHeight = EditorGUIUtility.singleLineHeight + RowPadding;
            }

            public Platform SelectedPlatform
            {
                get
                {
                    IList<int> selection = GetSelection();

                    if (selection.Count != 1)
                    {
                        return null;
                    }

                    PlatformItem selectedItem = FindItem(selection[0], rootItem) as PlatformItem;

                    if (selectedItem == null)
                    {
                        return null;
                    }

                    return selectedItem.platform;
                }
            }

            static UnityEditorInternal.ReorderableList.Defaults defaultBehaviours
            {
                get
                {
                    if (s_Defaults == null)
                    {
                        s_Defaults = new UnityEditorInternal.ReorderableList.Defaults();
                    }

                    return s_Defaults;
                }
            }

            public void DrawLayout()
            {
                Rect rect = EditorGUILayout.GetControlRect(false, TotalHeight);
                rect = EditorGUI.IndentedRect(rect);

                Rect headerRect = rect;
                headerRect.height = HeaderHeight;

                Rect bodyRect = rect;
                bodyRect.y = headerRect.yMax;
                bodyRect.height = BodyHeight;

                Rect footerRect = rect;
                footerRect.xMax -= FooterMarginRight;
                footerRect.y = bodyRect.yMax;
                footerRect.height = FooterHeight;

                Rect removeRect = footerRect;
                removeRect.x = footerRect.xMax - FooterPadding.right - ButtonWidth;
                removeRect.y += ButtonMarginTop;
                removeRect.width = ButtonWidth;
                removeRect.height = ButtonHeight;

                Rect addRect = footerRect;
                addRect.x = removeRect.x - ButtonWidth;
                addRect.y += ButtonMarginTop;
                addRect.width = ButtonWidth;
                addRect.height = ButtonHeight;

                footerRect.xMin = addRect.xMin - FooterPadding.left;
                footerRect.xMax = removeRect.xMax + FooterPadding.right;

                defaultBehaviours.DrawHeaderBackground(headerRect);

                if (Event.current.type == EventType.Repaint)
                {
                    defaultBehaviours.boxBackground.Draw(bodyRect, false, false, false, false);
                }

                Rect contentRect = BodyPadding.Remove(bodyRect);

                using (new NoIndentScope())
                {
                    base.OnGUI(contentRect);
                }

                if (Event.current.type == EventType.Repaint)
                {
                    defaultBehaviours.footerBackground.Draw(footerRect, false, false, false, false);
                }

                if (GUI.Button(addRect, defaultBehaviours.iconToolbarPlusMore, defaultBehaviours.preButton))
                {
                    DoAddMenu(addRect);
                }

                using (new EditorGUI.DisabledScope(SelectedPlatform == null))
                {
                    if (GUI.Button(removeRect, defaultBehaviours.iconToolbarMinus, defaultBehaviours.preButton))
                    {
                        DeleteSelectedPlatform();
                    }
                }
            }

            private void DoAddMenu(Rect rect)
            {
                GenericMenu menu = new GenericMenu();

                menu.AddItem(new GUIContent("New Group"), false, AddGroup);

                menu.AddSeparator(string.Empty);

                IEnumerable<Platform> missingPlatforms = settings.EnumeratePlatforms()
                    .Where(p => !p.Active)
                    .OrderBy(p => p.DisplayName, new NaturalComparer());

                foreach (Platform platform in missingPlatforms)
                {
                    menu.AddItem(new GUIContent(platform.DisplayName), false, AddPlatform, platform.Identifier);
                }

                menu.DropDown(rect);
            }

            private void AddPlatform(object data)
            {
                string identifier = data as string;

                Platform platform = settings.FindPlatform(identifier);

                const string UndoMessage = "Add FMOD Platform";

                Undo.RecordObjects(new UnityEngine.Object[] { settings, platform, platform.Parent }, UndoMessage);

                platform.DisplaySortOrder = UpdateSortOrderForChildren(platform.Parent, platform, UndoMessage);

                settings.AddPlatformProperties(platform);

                ForceReload();

                SelectAndFramePlatform(platform);
            }

            private void AddGroup()
            {
                const string UndoMessage = "Add FMOD Platform Group";

                Undo.RecordObjects(new UnityEngine.Object[] { settings, settings.DefaultPlatform }, UndoMessage);

                int sortOrder = UpdateSortOrderForChildren(settings.DefaultPlatform, null, UndoMessage);

                PlatformGroup group = EditorSettings.Instance.AddPlatformGroup("New Group", sortOrder);

                Undo.RegisterCreatedObjectUndo(group, UndoMessage);

                ForceReload();

                SelectAndFramePlatform(group);

                // Bring up the rename UI
                DoubleClickedItem(group.Identifier.GetHashCode());
            }

            private int UpdateSortOrderForChildren(Platform platform, Platform skipChild, string undoMessage)
            {
                int sortOrder = 0;

                foreach (string childID in platform.ChildIdentifiers)
                {
                    Platform child = settings.FindPlatform(childID);

                    if (child.Active && child != skipChild)
                    {
                        Undo.RecordObject(child, undoMessage);

                        child.DisplaySortOrder = sortOrder;
                        ++sortOrder;
                    }
                }

                return sortOrder;
            }

            // Removes a platform from the inheritance hierarchy and clears its properties, thus hiding
            // it in the UI. Also destroys the platform if it is a group.
            private void DeleteSelectedPlatform()
            {
                Platform platform = SelectedPlatform;

                if (platform == null)
                {
                    return;
                }

                const string UndoMessage = "Delete FMOD Platform";

                Undo.RecordObjects(new UnityEngine.Object[] { platform, platform.Parent, settings }, UndoMessage);

                while (platform.ChildIdentifiers.Count > 0)
                {
                    Platform child = settings.FindPlatform(platform.ChildIdentifiers[platform.ChildIdentifiers.Count - 1]);

                    SetPlatformParent(UndoMessage, settings, child, platform.Parent, (int)platform.DisplaySortOrder + 1);
                }

                if (platform is PlatformGroup)
                {
                    PlatformGroup group = platform as PlatformGroup;

                    settings.SetPlatformParent(group, null);
                    settings.RemovePlatform(group.Identifier);

                    Undo.DestroyObjectImmediate(group);
                }
                else
                {
                    platform.ClearProperties();

                    Undo.RecordObject(settings.DefaultPlatform, UndoMessage);

                    settings.SetPlatformParent(platform, settings.DefaultPlatform);
                }

                ForceReload();
            }

            public void SelectAndFramePlatform(Platform platform)
            {
                SetSelection(new List<int>() { platform.Identifier.GetHashCode() },
                    TreeViewSelectionOptions.RevealAndFrame);
            }

            protected override void RowGUI(RowGUIArgs args)
            {
                if (Event.current.type == EventType.Repaint)
                {
                    defaultBehaviours.elementBackground.Draw(args.rowRect, false, args.selected, args.selected, args.focused);

                    if (IsItemDraggable(args.item))
                    {
                        Rect dragRect = new Rect(args.rowRect.position + DragHandlePadding, DragHandleSize);

                        defaultBehaviours.draggingHandle.Draw(dragRect, false, false, false, false);
                    }

                    GUIContent labelContent = new GUIContent(args.label);

                    GUIStyle labelStyle = GUI.skin.label;

                    Rect labelRect = args.rowRect;
                    CenterRectUsingSingleLineHeight(ref labelRect);

                    labelRect.x = GetContentIndent(args.item);
                    labelRect.width = GUI.skin.label.CalcSize(labelContent).x;

                    Texture renameIcon = EditorGUIUtility.IconContent("SettingsIcon").image;

                    bool canRename = CanRename(args.item);

                    if (canRename)
                    {
                        labelContent.tooltip = "Double-click to rename";
                        labelRect.width += renameIcon.width;
                    }

                    GUI.Label(labelRect, labelContent);

                    if (canRename && Event.current.type == EventType.Repaint)
                    {
                        Rect iconRect = new Rect() {
                            x = labelRect.xMax - renameIcon.width,
                            y = labelRect.yMax - labelStyle.padding.bottom - renameIcon.height,
                            width = renameIcon.width,
                            height = renameIcon.height,
                        };

                        GUI.DrawTexture(iconRect, renameIcon, ScaleMode.ScaleToFit,
                            true, 0, labelStyle.normal.textColor, 0, 0);
                    }
                }
            }

            public void ForceReload()
            {
                Reload();
                ExpandAll();
            }

            public void ReloadIfNecessary()
            {
                if (!isInitialized)
                {
                    ForceReload();
                }
            }

            protected override bool CanMultiSelect(TreeViewItem item)
            {
                return false;
            }

            protected override bool CanChangeExpandedState(TreeViewItem item)
            {
                return false;
            }

            protected override TreeViewItem BuildRoot()
            {
                TreeViewItem root = new TreeViewItem(-1, -1);

                root.AddChild(CreateItem(settings.PlayInEditorPlatform));

                TreeViewItem defaultItem = CreateItem(settings.DefaultPlatform);
                root.AddChild(defaultItem);

                CreateItems(defaultItem, settings.DefaultPlatform.ChildIdentifiers);

                SetupDepthsFromParentsAndChildren(root);

                return root;
            }

            private class PlatformItem : TreeViewItem
            {
                public Platform platform;

                public PlatformItem(Platform platform)
                    : base(platform.Identifier.GetHashCode(), 0, platform.DisplayName)
                {
                    this.platform = platform;
                }
            }

            private void CreateItems(TreeViewItem parent, IEnumerable<string> platformIdentifiers)
            {
                foreach (string identifier in platformIdentifiers)
                {
                    Platform platform = settings.FindPlatform(identifier);

                    if (platform.Active)
                    {
                        TreeViewItem item = CreateItem(platform);
                        parent.AddChild(item);

                        CreateItems(item, platform.ChildIdentifiers);
                    }
                }
            }

            private static TreeViewItem CreateItem(Platform platform)
            {
                return new PlatformItem(platform);
            }

            protected override void DoubleClickedItem(int id)
            {
                TreeViewItem item = FindItem(id, rootItem);

                if (CanRename(item))
                {
                    BeginRename(item);
                }
            }

            protected override bool CanRename(TreeViewItem item)
            {
                PlatformItem platformItem = item as PlatformItem;
                return (platformItem != null) && (platformItem.platform is PlatformGroup);
            }

            protected override void RenameEnded(RenameEndedArgs args)
            {
                if (!args.acceptedRename || string.IsNullOrEmpty(args.newName))
                {
                    return;
                }

                PlatformItem item = FindItem(args.itemID, rootItem) as PlatformItem;

                if (item == null)
                {
                    return;
                }

                PlatformGroup group = item.platform as PlatformGroup;

                if (group == null)
                {
                    return;
                }

                // Undo.RecordObject doesn't capture PlatformGroup.displayName, maybe due to inheritance?
                // This means we need to use the SerializedObject interface instead.
                SerializedObject serializedGroup = new SerializedObject(group);
                SerializedProperty displayName = serializedGroup.FindProperty("displayName");

                displayName.stringValue = args.newName;

                serializedGroup.ApplyModifiedProperties();

                item.displayName = args.newName;
            }

            protected override bool CanStartDrag(CanStartDragArgs args)
            {
                return IsItemDraggable(args.draggedItem);
            }

            private bool IsItemDraggable(TreeViewItem draggedItem)
            {
                PlatformItem item = draggedItem as PlatformItem;

                return (item != null) && !item.platform.IsIntrinsic;
            }

            protected override void SetupDragAndDrop(SetupDragAndDropArgs args)
            {
                PlatformItem item = FindItem(args.draggedItemIDs[0], rootItem) as PlatformItem;

                if (item != null)
                {
                    DragAndDrop.PrepareStartDrag();
                    DragAndDrop.objectReferences = new UnityEngine.Object[] { item.platform };
                    DragAndDrop.StartDrag("Change FMOD Platform Inheritance");
                }
            }

            protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args)
            {
                if (DragAndDrop.objectReferences.Length != 1)
                {
                    return DragAndDropVisualMode.None;
                }

                Platform draggedPlatform = DragAndDrop.objectReferences[0] as Platform;

                if (draggedPlatform == null)
                {
                    return DragAndDropVisualMode.None;
                }

                PlatformItem parentItem = args.parentItem as PlatformItem;

                if (parentItem == null)
                {
                    return DragAndDropVisualMode.None;
                }

                Platform parent = parentItem.platform;

                if (parent is PlatformPlayInEditor)
                {
                    return DragAndDropVisualMode.None;
                }

                switch (args.dragAndDropPosition)
                {
                    case DragAndDropPosition.UponItem:
                        return HandleDragOverPlatform(draggedPlatform, parent, args.performDrop);
                    case DragAndDropPosition.BetweenItems:
                        return HandleDragBetweenChildren(draggedPlatform, parent, args.insertAtIndex, args.performDrop);
                    case DragAndDropPosition.OutsideItems:
                        return DragAndDropVisualMode.Move;
                    default:
                        return DragAndDropVisualMode.None;
                }
            }

            private DragAndDropVisualMode HandleDragOverPlatform(Platform draggedPlatform, Platform parent, bool performDrop)
            {
                if (parent == draggedPlatform)
                {
                    return DragAndDropVisualMode.Move;
                }

                if (parent.InheritsFrom(draggedPlatform))
                {
                    return DragAndDropVisualMode.None;
                }

                if (performDrop)
                {
                    SetPlatformParent("Set FMOD Platform Inheritance", settings, draggedPlatform, parent);
                    ForceReload();
                }

                return DragAndDropVisualMode.Link;
            }

            private DragAndDropVisualMode HandleDragBetweenChildren(Platform draggedPlatform, Platform parent,
                int insertAtIndex, bool performDrop)
            {
                if (parent.InheritsFrom(draggedPlatform))
                {
                    return DragAndDropVisualMode.None;
                }

                if (performDrop)
                {
                    SetPlatformParent("Set FMOD Platform Inheritance", settings, draggedPlatform, parent, insertAtIndex);
                    ForceReload();
                }

                return DragAndDropVisualMode.Move;
            }
        }

        private class ReorderableList : UnityEditorInternal.ReorderableList
        {
            const float ElementPadding = 2;

            public ReorderableList(SerializedProperty property)
                : base(property.serializedObject, property, true, false, true, true)
            {
                headerHeight = 3;
                elementHeight = EditorGUIUtility.singleLineHeight + ElementPadding;
                drawElementCallback = DrawElement;
            }

            public void DrawLayout()
            {
                Rect rect = EditorGUILayout.GetControlRect(false, GetHeight());
                rect = EditorGUI.IndentedRect(rect);

                DoList(rect);
            }

            void DrawElement(Rect rect, int index, bool active, bool focused)
            {
                using (new NoIndentScope())
                {
                    rect.height -= ElementPadding;

                    EditorGUI.PropertyField(rect, serializedProperty.GetArrayElementAtIndex(index), GUIContent.none);
                }
            }
        }

        private class PlatformPropertyStringListView : UnityEditorInternal.ReorderableList
        {
            const float ElementPadding = 2;

            public Platform platform;

            List<string> displayList;

            public PlatformPropertyStringListView(Platform.PropertyAccessor<List<string>> property)
                : base(null, typeof(string), true, false, true, true)
            {
                this.property = property;

                displayList = new List<string>();
                list = displayList;

                headerHeight = 3;
                elementHeight = EditorGUIUtility.singleLineHeight + ElementPadding;

                drawElementCallback = DrawElement;
                onAddCallback = AddElement;
                onRemoveCallback = RemoveElement;
                onReorderCallback = OnReorder;
            }

            public Platform.PropertyAccessor<List<string>> property { get; private set; }

            // We need this because ReorderableList modifies the list before calling
            // onReorderCallback, meaning we can't call AffirmOverriddenList
            // soon enough.
            public void DrawLayout()
            {
                if (IsReloadNeeded())
                {
                    displayList.Clear();
                    displayList.AddRange(property.Get(platform));
                }

                Rect rect = EditorGUILayout.GetControlRect(false, GetHeight());
                rect = EditorGUI.IndentedRect(rect);

                DoList(rect);
            }

            public bool IsReloadNeeded()
            {
                List<string> propertyList = property.Get(platform);

                if (displayList.Count != propertyList.Count)
                {
                    return true;
                }

                for (int i = 0; i < displayList.Count; ++i)
                {
                    if (displayList[i] != propertyList[i])
                    {
                        return true;
                    }
                }

                return false;
            }

            void DrawElement(Rect rect, int index, bool active, bool focused)
            {
                using (new NoIndentScope())
                {
                    rect.height -= ElementPadding;

                    EditorGUI.BeginChangeCheck();

                    string newValue = EditorGUI.TextField(rect, list[index] as string);

                    if (EditorGUI.EndChangeCheck())
                    {
                        displayList[index] = newValue;
                        AffirmOverriddenList()[index] = newValue;
                    }
                }
            }

            void AddElement(UnityEditorInternal.ReorderableList list)
            {
                AffirmOverriddenList().Add(string.Empty);
            }

            void RemoveElement(UnityEditorInternal.ReorderableList list)
            {
                AffirmOverriddenList().RemoveAt(list.index);
            }

            void OnReorder(UnityEditorInternal.ReorderableList list)
            {
                List<string> propertyList = AffirmOverriddenList();

                propertyList.Clear();
                propertyList.AddRange(displayList);
            }

            List<string> AffirmOverriddenList()
            {
                if (!property.HasValue(platform))
                {
                    List<string> newList = new List<string>(property.Get(platform));

                    property.Set(platform, newList);
                }

                return property.Get(platform);
            }
        }

        // If insertAtIndex == -1, insert at the end
        private static void SetPlatformParent(string undoMessage, Settings settings, Platform child, Platform parent, int insertAtIndex = -1)
        {
            if (parent == child.Parent)
            {
                if (insertAtIndex > child.DisplaySortOrder)
                {
                    --insertAtIndex;
                }

                if (insertAtIndex == child.DisplaySortOrder)
                {
                    return;
                }
            }

            Undo.RecordObjects(new[] { child, child.Parent, parent }, undoMessage);

            int index = 0;

            for (int i = 0; i < parent.ChildIdentifiers.Count; ++i)
            {
                Platform sibling = settings.FindPlatform(parent.ChildIdentifiers[i]);

                if (sibling.Active && sibling != child)
                {
                    if (index == insertAtIndex)
                    {
                        ++index;
                    }

                    Undo.RecordObject(sibling, undoMessage);

                    sibling.DisplaySortOrder = index;
                    ++index;
                }
            }

            if (insertAtIndex == -1)
            {
                insertAtIndex = index;
            }

            child.DisplaySortOrder = insertAtIndex;

            settings.SetPlatformParent(child, parent);
        }

        private void ApplyPendingActions()
        {
            if (hasBankSourceChanged || hasBankTargetChanged)
            {
                RefreshBanks();
            }
        }

        private void RefreshBanks()
        {
            Settings settings = target as Settings;

            if (lastSourceBankPath != settings.SourceBankPath)
            {
                lastSourceBankPath = settings.SourceBankPath;
                EventManager.RefreshBanks();
            }
        }

        public static void DisplayBankRefreshSettings(SerializedProperty cooldown, SerializedProperty showWindow,
            bool inInspector)
        {
            Rect controlRect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight);

            Rect labelRect;

            if (inInspector)
            {
                labelRect = LabelRect(controlRect);
            }
            else
            {
                labelRect = EditorGUI.IndentedRect(controlRect);
                labelRect.width = GUI.skin.label.CalcSize(BankRefreshLabel).x;
            }


            Rect popupRect = controlRect;
            popupRect.x = labelRect.xMax;
            popupRect.width = BankRefreshCooldownLabels.Max(l => EditorStyles.popup.CalcSize(l).x);

            using (new NoIndentScope())
            {
                GUI.Label(labelRect, BankRefreshLabel);

                cooldown.intValue = EditorGUI.IntPopup(popupRect, cooldown.intValue,
                    BankRefreshCooldownLabels, BankRefreshCooldownValues);

                if (cooldown.intValue >= 0)
                {
                    Rect toggleRect = controlRect;
                    toggleRect.xMin = popupRect.xMax + GUI.skin.toggle.margin.left;

                    showWindow.boolValue = EditorGUI.ToggleLeft(toggleRect, "Show Status Window", showWindow.boolValue);
                }
            }
        }

        static Rect LabelRect(Rect controlRect)
        {
            Rect result = controlRect;
            result.width = EditorGUIUtility.labelWidth;
            result = EditorGUI.IndentedRect(result);

            return result;
        }

        public static bool BrowseForSourceProjectPath(SerializedObject serializedObject)
        {
            serializedObject.Update();
            var sourceProjectPath = serializedObject.FindProperty("sourceProjectPath");
            var sourceBankPath = serializedObject.FindProperty("sourceBankPath");
            var hasSourceProject = serializedObject.FindProperty("HasSourceProject");
            var hasPlatforms = serializedObject.FindProperty("HasPlatforms");

            string newPath = EditorUtility.OpenFilePanel("Locate Studio Project", sourceProjectPath.stringValue, "fspro");

            if (string.IsNullOrEmpty(newPath))
            {
                return false;
            }
            else
            {
                hasSourceProject.boolValue = true;
                hasPlatforms.boolValue = true;
                newPath = MakePathRelative(newPath);
                sourceProjectPath.stringValue = newPath;
                sourceBankPath.stringValue = GetBankDirectory(serializedObject);
                serializedObject.ApplyModifiedProperties();
                EventManager.RefreshBanks();
                return true;
            }
        }

        public static bool BrowseForSourceBankPath(SerializedObject serializedObject, bool multiPlatform = false)
        {
            serializedObject.Update();
            var sourceBankPath = serializedObject.FindProperty("sourceBankPath");
            var hasSourceProject = serializedObject.FindProperty("HasSourceProject");
            var hasPlatforms = serializedObject.FindProperty("HasPlatforms");

            string newPath = EditorUtility.OpenFolderPanel("Locate Build Folder", sourceBankPath.stringValue, null);

            if (string.IsNullOrEmpty(newPath))
            {
                return false;
            }
            else
            {
                hasSourceProject.boolValue = false;
                hasPlatforms.boolValue = multiPlatform;
                newPath = MakePathRelative(newPath);
                sourceBankPath.stringValue = newPath;
                serializedObject.ApplyModifiedProperties();
                EventManager.RefreshBanks();
                return true;
            }
        }

        private static string MakePathRelative(string path)
        {
            if (string.IsNullOrEmpty(path))
                return "";
            string fullPath = Path.GetFullPath(path);
            string fullProjectPath = Path.GetFullPath(Environment.CurrentDirectory + Path.DirectorySeparatorChar);

            // If the path contains the Unity project path remove it and return the result
            if (fullPath.Contains(fullProjectPath))
            {
                fullPath = fullPath.Replace(fullProjectPath, "");
            }
            // If not, attempt to find a relative path on the same drive
            else if (Path.GetPathRoot(fullPath) == Path.GetPathRoot(fullProjectPath))
            {
                // Remove trailing slash from project path for split count simplicity
                if (fullProjectPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.CurrentCulture)) fullProjectPath = fullProjectPath.Substring(0, fullProjectPath.Length - 1);

                string[] fullPathSplit = fullPath.Split(Path.DirectorySeparatorChar);
                string[] projectPathSplit = fullProjectPath.Split(Path.DirectorySeparatorChar);
                int minNumSplits = Mathf.Min(fullPathSplit.Length, projectPathSplit.Length);
                int numCommonElements = 0;
                for (int i = 0; i < minNumSplits; i++)
                {
                    if (fullPathSplit[i] == projectPathSplit[i])
                    {
                        numCommonElements++;
                    }
                    else
                    {
                        break;
                    }
                }
                string result = "";
                int fullPathSplitLength = fullPathSplit.Length;
                for (int i = numCommonElements; i < fullPathSplitLength; i++)
                {
                    result += fullPathSplit[i];
                    if (i < fullPathSplitLength - 1)
                    {
                        result += '/';
                    }
                }

                int numAdditionalElementsInProjectPath = projectPathSplit.Length - numCommonElements;
                for (int i = 0; i < numAdditionalElementsInProjectPath; i++)
                {
                    result = "../" + result;
                }

                fullPath = result;
            }

            return fullPath.Replace(Path.DirectorySeparatorChar, '/');
        }
    }
}
