using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace FMOD
{
    public partial class VERSION
    {
#if DEVELOPMENT_BUILD
        public const string dllSuffix = "L";
#else
        public const string dllSuffix = "";
#endif
    }
}

namespace FMOD.Studio
{
    public partial class STUDIO_VERSION
    {
#if DEVELOPMENT_BUILD
        public const string dllSuffix = "L";
#else
        public const string dllSuffix = "";
#endif
    }
}

namespace FMODUnity
{
    public class PlatformCallbackHandler : ScriptableObject
    {
        // A hook for custom initialization logic. RuntimeManager.Initialize calls this
        // just before calling system.Initialize.
        // Call reportResult() with the result of each FMOD call to use FMOD's error handling logic.
        public virtual void PreInitialize(FMOD.Studio.System system, Action<FMOD.RESULT, string> reportResult)
        {
        }
    }

    // This class holds per-platform settings and provides hooks for platform-specific behaviour.
    // Each platform has a parent platform, forming a hierarchy that is rooted at PlatformDefault.
    // By default a platform inherits all of its properties from its parent platform; this behaviour
    // can be overridden for each property.
    //
    // There is at least one concrete derived class for each supported platform; these classes use
    // [InitializeOnLoad] and a static constructor to register themselves as supported platforms by
    // calling Settings.AddPlatformTemplate. The user can also create instances of the PlatformGroup
    // class and use them to group platforms that have settings in common.
    public abstract class Platform : ScriptableObject, IComparable<Platform>
    {
        // This is a persistent identifier. It is used:
        // * To link platforms together at load time
        // * To avoid creating duplicate platforms from templates (in Settings.OnEnable)
        // * As a key for SettingsEditor UI state
        // It should be kept stable for concrete platforms (like PlatformWindows) to support
        // settings migration in the future.
        [SerializeField]
        private string identifier;

        public string Identifier
        {
            get
            {
                return identifier;
            }

            set
            {
                identifier = value;
            }
        }

        // The display name to show for this platform in the UI.
        public abstract string DisplayName { get; }

        // Declares the Unity RuntimePlatforms and BuildTargets this platform implements.
        public abstract void DeclareUnityMappings(Settings settings);

#if UNITY_EDITOR
        // The old FMOD platform identifier that this platform corresponds to, for settings migration.
        public abstract Legacy.Platform LegacyIdentifier { get; }
#endif

        public const float DefaultPriority = 0;

        // The priority to use when finding a platform to support the current Unity runtime
        // platform (higher priorities are tried first).
        public virtual float Priority { get { return DefaultPriority; } }

        // Determines whether this platform matches the current environment. When more than one
        // platform implements the current Unity runtime platform, FMOD for Unity will use the
        // highest-priority platform that returns true from MatchesCurrentEnvironment.
        public virtual bool MatchesCurrentEnvironment { get { return true; } }

        // Whether this platform is a fixed part of the FMOD for Unity settings, or can be
        // added/removed by the user.
        public virtual bool IsIntrinsic { get { return false; } }

        // A hook for platform-specific initialization logic. RuntimeManager.Initialize calls this
        // before calling FMOD.Studio.System.create.
        public virtual void PreSystemCreate(Action<FMOD.RESULT, string> reportResult)
        {
        }

        // A hook for platform-specific initialization logic. RuntimeManager.Initialize calls this
        // just before calling studioSystem.Initialize.
        public virtual void PreInitialize(FMOD.Studio.System studioSystem)
        {
        }

        // The folder in which FMOD .bank files are stored. Used when loading banks.
        public virtual string GetBankFolder()
        {
            return Application.streamingAssetsPath;
        }

#if UNITY_EDITOR
        [Flags]
        public enum BinaryType
        {
            Release = 1,
            Logging = 2,
            Optional = 4,
            AllVariants = 8,
            All = Release | Logging | Optional | AllVariants
        }

        protected virtual IEnumerable<string> GetBinaryPaths(BuildTarget buildTarget, BinaryType binaryType, string prefix)
        {
            string assetBasePath = GetBinaryAssetBasePath();
            bool allVariants = (binaryType & BinaryType.AllVariants) == BinaryType.AllVariants;

            if ((binaryType & BinaryType.Release) == BinaryType.Release)
            {
                foreach (string path in GetRelativeBinaryPaths(buildTarget, allVariants, ""))
                {
                    yield return string.Format("{0}/{1}/{2}", prefix, assetBasePath, path);
                }
            }

            if ((binaryType & BinaryType.Logging) == BinaryType.Logging)
            {
                foreach (string path in GetRelativeBinaryPaths(buildTarget, allVariants, "L"))
                {
                    yield return string.Format("{0}/{1}/{2}", prefix, assetBasePath, path);
                }
            }

            if ((binaryType & BinaryType.Optional) == BinaryType.Optional)
            {
                foreach (string path in GetRelativeOptionalBinaryPaths(buildTarget, allVariants))
                {
                    yield return string.Format("{0}/{1}/{2}", prefix, assetBasePath, path);
                }
            }
        }

        // Called by Settings.CanBuildTarget to get the required binaries for the current
        // build target and logging state.
        public virtual IEnumerable<string> GetBinaryFilePaths(BuildTarget buildTarget, BinaryType binaryType)
        {
            return GetBinaryPaths(buildTarget, binaryType, Application.dataPath);
        }

        // Called by Settings.SelectBinaries to get:
        // * The required and optional binaries for the current build target and logging state;
        //   these get enabled.
        // * All binaries; any that weren't enabled in the previous step get disabled.
        public virtual IEnumerable<string> GetBinaryAssetPaths(BuildTarget buildTarget, BinaryType binaryType)
        {
            return GetBinaryPaths(buildTarget, binaryType, "Assets");
        }

        protected virtual string GetBinaryAssetBasePath()
        {
            return "Plugins/FMOD/lib";
        }

        protected abstract IEnumerable<string> GetRelativeBinaryPaths(BuildTarget buildTarget, bool allVariants, string suffix);

        protected virtual IEnumerable<string> GetRelativeOptionalBinaryPaths(BuildTarget buildTarget, bool allVariants)
        {
            yield break;
        }

        public virtual bool IsFMODStaticallyLinked { get { return false; } }

        public virtual bool SupportsAdditionalCPP(BuildTarget target)
        {
            return true;
        }
#endif

        // The base path for FMOD plugins when in a standalone player.
        protected virtual string GetPluginBasePath()
        {
            return string.Format("{0}/Plugins", Application.dataPath);
        }

        // The base path for FMOD plugins when in the Unity editor.
        protected virtual string GetEditorPluginBasePath()
        {
            return string.Format("{0}/FMOD/lib", GetPluginBasePath());
        }

        // Returns the full path for an FMOD plugin.
        public virtual string GetPluginPath(string pluginName)
        {
            throw new NotImplementedException(string.Format("Plugins are not implemented on platform {0}", Identifier));
        }

        // Loads static and dynamic FMOD plugins for this platform.
        public virtual void LoadPlugins(FMOD.System coreSystem, Action<FMOD.RESULT, string> reportResult)
        {
            LoadDynamicPlugins(coreSystem, reportResult);
            LoadStaticPlugins(coreSystem, reportResult);
        }

        // Loads dynamic FMOD plugins for this platform.
        public virtual void LoadDynamicPlugins(FMOD.System coreSystem, Action<FMOD.RESULT, string> reportResult)
        {
            List<string> pluginNames = Plugins;

            if (pluginNames == null)
            {
                return;
            }

            foreach (string pluginName in pluginNames)
            {
                if (string.IsNullOrEmpty(pluginName))
                {
                    continue;
                }

                string pluginPath = GetPluginPath(pluginName);
                uint handle;

                FMOD.RESULT result = coreSystem.loadPlugin(pluginPath, out handle);

#if UNITY_64 || UNITY_EDITOR_64
                // Add a "64" suffix and try again
                if (result == FMOD.RESULT.ERR_FILE_BAD || result == FMOD.RESULT.ERR_FILE_NOTFOUND)
                {
                    string pluginPath64 = GetPluginPath(pluginName + "64");
                    result = coreSystem.loadPlugin(pluginPath64, out handle);
                }
#endif

                reportResult(result, string.Format("Loading plugin '{0}' from '{1}'", pluginName, pluginPath));
            }
        }

        // Loads static FMOD plugins for this platform.
#if ENABLE_IL2CPP
        public virtual void LoadStaticPlugins(FMOD.System coreSystem, Action<FMOD.RESULT, string> reportResult)
        {
            if (StaticPlugins.Count > 0)
            {
                FMOD.RESULT result = FMOD_Unity_RegisterStaticPlugins(FMOD.VERSION.dll, coreSystem.handle);
                reportResult(result, "Registering static plugins");
            }
        }

        // This function's name needs to match the contents of RegisterStaticPluginsFunctionName below
        [DllImport("__Internal")]
        private static extern FMOD.RESULT FMOD_Unity_RegisterStaticPlugins(string coreLibraryName, IntPtr system);
#else
        public virtual void LoadStaticPlugins(FMOD.System coreSystem, Action<FMOD.RESULT, string> reportResult)
        {
            if (StaticPlugins.Count > 0)
            {
                Debug.LogWarningFormat(
                    "{0} static plugins specified, but static plugins are only supported on the IL2CPP scripting backend",
                    StaticPlugins.Count);
            }
        }
#endif

        // This needs to match the DllImport function called by LoadStaticPlugins above
        public const string RegisterStaticPluginsFunctionName = "FMOD_Unity_RegisterStaticPlugins";

        // Ensures that this platform has properties.
        public void AffirmProperties()
        {
            if (!active)
            {
                Properties = new PropertyStorage();
                InitializeProperties();
                active = true;
            }
        }

        // Clears this platform's properties.
        public void ClearProperties()
        {
            if (active)
            {
                Properties = new PropertyStorage();
                active = false;
            }
        }

        // Initializes this platform's properties to their default values.
        public virtual void InitializeProperties()
        {
            if (!IsIntrinsic)
            {
                ParentIdentifier = PlatformDefault.ConstIdentifier;
            }
        }

        // Ensures that this platform's properties are valid after loading from file.
        public virtual void EnsurePropertiesAreValid()
        {
            if (!IsIntrinsic && string.IsNullOrEmpty(ParentIdentifier))
            {
                ParentIdentifier = PlatformDefault.ConstIdentifier;
            }
        }

        [SerializeField]
        private string parentIdentifier;

        public string ParentIdentifier
        {
            get
            {
                return parentIdentifier;
            }

            set
            {
                parentIdentifier = value;
            }
        }

        [SerializeField]
        private float displaySortOrder;

        public float DisplaySortOrder
        {
            get
            {
                return displaySortOrder;
            }

            set
            {
                if (displaySortOrder != value)
                {
                    displaySortOrder = value;

                    if (Parent != null)
                    {
                        Parent.children.Sort();
                    }
                }
            }
        }

        public bool IsLiveUpdateEnabled
        {
            get
            {
#if DEVELOPMENT_BUILD || UNITY_EDITOR
                return LiveUpdate != TriStateBool.Disabled;
#else
                return LiveUpdate == TriStateBool.Enabled;
#endif
            }
        }

        public bool IsOverlayEnabled
        {
            get
            {
#if DEVELOPMENT_BUILD || UNITY_EDITOR
                return Overlay != TriStateBool.Disabled;
#else
                return Overlay == TriStateBool.Enabled;
#endif
            }
        }

        public int CompareTo(Platform other)
        {
            if (other == null)
            {
                return 1;
            }
            else
            {
                return DisplaySortOrder.CompareTo(other.DisplaySortOrder);
            }
        }

        // A property value that can be inherited from the parent or overridden.
        public class Property<T>
        {
            public T Value;
            public bool HasValue;
        }

        // These stub classes are needed because Unity can't serialize generic classes
        [Serializable]
        public class PropertyBool : Property<TriStateBool>
        {
        }

        [Serializable]
        public class PropertyInt : Property<int>
        {
        }

        [Serializable]
        public class PropertySpeakerMode : Property<FMOD.SPEAKERMODE>
        {
        }

        [Serializable]
        public class PropertyString : Property<string>
        {
        }

        [Serializable]
        public class PropertyStringList : Property<List<string>>
        {
        }

        [Serializable]
        public class PropertyCallbackHandler : Property<PlatformCallbackHandler>
        {
        }

        // This class provides access to a specific property on any Platform object; the property to
        // operate on is determined by the Getter function. This allows client code to operate on
        // platform properties in a generic manner.
        public struct PropertyAccessor<T>
        {
            private readonly Func<PropertyStorage, Property<T>> Getter;
            private readonly T DefaultValue;

            public PropertyAccessor(Func<PropertyStorage, Property<T>> getter, T defaultValue)
            {
                Getter = getter;
                DefaultValue = defaultValue;
            }

            // Determine whether the property has a value in the given platform, or is inherited
            // from the parent.
            public bool HasValue(Platform platform)
            {
                return platform.Active && Getter(platform.Properties).HasValue;
            }

            // Get the (possibly inherited) value of the property for the given platform.
            public T Get(Platform platform)
            {
                for (Platform current = platform; current != null; current = current.Parent)
                {
                    if (current.Active)
                    {
                        Property<T> property = Getter(current.Properties);

                        if (property.HasValue)
                        {
                            return property.Value;
                        }
                    }
                }

#if UNITY_EDITOR
                if (platform is PlatformPlayInEditor)
                {
                    return Get(Settings.Instance.CurrentEditorPlatform);
                }
#endif

                return DefaultValue;
            }

            // Set the value of the property in the given platform, so it is not inherited from the
            // platform's parent.
            public void Set(Platform platform, T value)
            {
                Property<T> property = Getter(platform.Properties);

                property.Value = value;
                property.HasValue = true;
            }

            // Clear the value of the property in the given platform, so it is inherited from the
            // platform's parent.
            public void Clear(Platform platform)
            {
                Getter(platform.Properties).HasValue = false;
            }
        }

        // This class stores all of the inheritable properties for a platform.
        [Serializable]
        public class PropertyStorage
        {
            public PropertyBool LiveUpdate = new PropertyBool();
            public PropertyInt LiveUpdatePort = new PropertyInt();
            public PropertyBool Overlay = new PropertyBool();
            public PropertyBool Logging = new PropertyBool();
            public PropertyInt SampleRate = new PropertyInt();
            public PropertyString BuildDirectory = new PropertyString();
            public PropertySpeakerMode SpeakerMode = new PropertySpeakerMode();
            public PropertyInt VirtualChannelCount = new PropertyInt();
            public PropertyInt RealChannelCount = new PropertyInt();
            public PropertyInt DSPBufferLength = new PropertyInt();
            public PropertyInt DSPBufferCount = new PropertyInt();
            public PropertyStringList Plugins = new PropertyStringList();
            public PropertyStringList StaticPlugins = new PropertyStringList();
            public PropertyCallbackHandler CallbackHandler = new PropertyCallbackHandler();
        }

        [SerializeField]
        private bool active = false;

        // Whether this platform is active in the settings UI.
        public bool Active { get { return active; } }

        // Whether this platform has any properties that are not inherited from the parent.
        public bool HasAnyOverriddenProperties
        {
            get
            {
                return active &&
                    (
                        Properties.LiveUpdate.HasValue
                        || Properties.LiveUpdatePort.HasValue
                        || Properties.Overlay.HasValue
                        || Properties.Logging.HasValue
                        || Properties.SampleRate.HasValue
                        || Properties.BuildDirectory.HasValue
                        || Properties.SpeakerMode.HasValue
                        || Properties.VirtualChannelCount.HasValue
                        || Properties.RealChannelCount.HasValue
                        || Properties.DSPBufferLength.HasValue
                        || Properties.DSPBufferCount.HasValue
                        || Properties.Plugins.HasValue
                        || Properties.StaticPlugins.HasValue
                    );
            }
        }

        [SerializeField]
        protected PropertyStorage Properties = new PropertyStorage();

        // These accessors provide (possibly inherited) property values.
        public TriStateBool LiveUpdate { get { return PropertyAccessors.LiveUpdate.Get(this); } }
        public int LiveUpdatePort { get { return PropertyAccessors.LiveUpdatePort.Get(this); } }
        public TriStateBool Overlay { get { return PropertyAccessors.Overlay.Get(this); } }
        public TriStateBool Logging { get { return PropertyAccessors.Logging.Get(this); } }
        public int SampleRate { get { return PropertyAccessors.SampleRate.Get(this); } }
        public string BuildDirectory { get { return PropertyAccessors.BuildDirectory.Get(this); } }
        public FMOD.SPEAKERMODE SpeakerMode { get { return PropertyAccessors.SpeakerMode.Get(this); } }
        public int VirtualChannelCount { get { return PropertyAccessors.VirtualChannelCount.Get(this); } }
        public int RealChannelCount { get { return PropertyAccessors.RealChannelCount.Get(this); } }
        public int DSPBufferLength { get { return PropertyAccessors.DSPBufferLength.Get(this); } }
        public int DSPBufferCount { get { return PropertyAccessors.DSPBufferCount.Get(this); } }
        public List<string> Plugins { get { return PropertyAccessors.Plugins.Get(this); } }
        public List<string> StaticPlugins { get { return PropertyAccessors.StaticPlugins.Get(this); } }
        public PlatformCallbackHandler CallbackHandler { get { return PropertyAccessors.CallbackHandler.Get(this); } }

        // These accessors provide full access to properties.
        public static class PropertyAccessors
        {
            public static readonly PropertyAccessor<TriStateBool> LiveUpdate
                    = new PropertyAccessor<TriStateBool>(properties => properties.LiveUpdate, TriStateBool.Disabled);

            public static readonly PropertyAccessor<int> LiveUpdatePort
                    = new PropertyAccessor<int>(properties => properties.LiveUpdatePort, 9264);

            public static readonly PropertyAccessor<TriStateBool> Overlay
                    = new PropertyAccessor<TriStateBool>(properties => properties.Overlay, TriStateBool.Disabled);

            public static readonly PropertyAccessor<TriStateBool> Logging
                    = new PropertyAccessor<TriStateBool>(properties => properties.Logging, TriStateBool.Disabled);

            public static readonly PropertyAccessor<int> SampleRate
                    = new PropertyAccessor<int>(properties => properties.SampleRate, 0);

            public static readonly PropertyAccessor<string> BuildDirectory
                    = new PropertyAccessor<string>(properties => properties.BuildDirectory, "Desktop");

            public static readonly PropertyAccessor<FMOD.SPEAKERMODE> SpeakerMode
                    = new PropertyAccessor<FMOD.SPEAKERMODE>(properties => properties.SpeakerMode, FMOD.SPEAKERMODE.STEREO);

            public static readonly PropertyAccessor<int> VirtualChannelCount
                    = new PropertyAccessor<int>(properties => properties.VirtualChannelCount, 128);

            public static readonly PropertyAccessor<int> RealChannelCount
                    = new PropertyAccessor<int>(properties => properties.RealChannelCount, 32);

            public static readonly PropertyAccessor<int> DSPBufferLength
                    = new PropertyAccessor<int>(properties => properties.DSPBufferLength, 0);

            public static readonly PropertyAccessor<int> DSPBufferCount
                    = new PropertyAccessor<int>(properties => properties.DSPBufferCount, 0);

            public static readonly PropertyAccessor<List<string>> Plugins
                    = new PropertyAccessor<List<string>>(properties => properties.Plugins, null);

            public static readonly PropertyAccessor<List<string>> StaticPlugins
                    = new PropertyAccessor<List<string>>(properties => properties.StaticPlugins, null);

            public static readonly PropertyAccessor<PlatformCallbackHandler> CallbackHandler
                    = new PropertyAccessor<PlatformCallbackHandler>(properties => properties.CallbackHandler, null);
        }

        [NonSerialized]
        private Platform parent;

        // The parent platform from which this platform inherits its property values.
        public Platform Parent
        {
            get { return parent; }

            set
            {
                if (value != parent)
                {
                    if (parent != null)
                    {
                        parent.children.Remove(this);
                    }

                    parent = value;

                    if (parent != null)
                    {
                        parent.children.Add(this);
                        parent.children.Sort();
                    }

                    ParentIdentifier = (parent != null) ? parent.Identifier : null;
                }
            }
        }

        [NonSerialized]
        private readonly List<Platform> children = new List<Platform>();

        // The platforms which inherit their property values from this platform.
        public List<Platform> Children { get { return children; } }

        // Checks whether this platform inherits from the given platform, so we can avoid creating
        // inheritance loops.
        public bool InheritsFrom(Platform platform)
        {
            if (platform == this)
            {
                return true;
            }
            else if (Parent != null)
            {
                return Parent.InheritsFrom(platform);
            }
            else
            {
                return false;
            }
        }

        [SerializeField]
        public string outputType;

        public FMOD.OUTPUTTYPE GetOutputType()
        {
            if (Enum.IsDefined(typeof(FMOD.OUTPUTTYPE), outputType))
            {
                return (FMOD.OUTPUTTYPE)Enum.Parse(typeof(FMOD.OUTPUTTYPE), outputType);
            }
            return FMOD.OUTPUTTYPE.AUTODETECT;
        }
#if UNITY_EDITOR
        public struct OutputType
        {
            public string displayName;
            public FMOD.OUTPUTTYPE outputType;
        }

        public abstract OutputType[] ValidOutputTypes { get; }

        public virtual int CoreCount { get { return 0; } }

        public const int MaximumCoreCount = 16;
#endif

        public virtual List<ThreadAffinityGroup> DefaultThreadAffinities { get { return StaticThreadAffinities; } }

        private static List<ThreadAffinityGroup> StaticThreadAffinities = new List<ThreadAffinityGroup>();

        [Serializable]
        public class PropertyThreadAffinityList : Property<List<ThreadAffinityGroup>>
        {
        }

        [SerializeField]
        private PropertyThreadAffinityList threadAffinities = new PropertyThreadAffinityList();

        public IEnumerable<ThreadAffinityGroup> ThreadAffinities
        {
            get
            {
                if (threadAffinities.HasValue)
                {
                    return threadAffinities.Value;
                }
                else
                {
                    return DefaultThreadAffinities;
                }
            }
        }

        public PropertyThreadAffinityList ThreadAffinitiesProperty { get { return threadAffinities; } }
    }
}
