// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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

namespace FMODUnityResonance
{
    /// This is the main Resonance Audio class that communicates with the FMOD Unity integration. Native
    /// functions of the system can only be called through this class to preserve the internal system
    /// functionality.
    public static class FmodResonanceAudio
    {
        /// Maximum allowed gain value in decibels.
        public const float maxGainDb = 24.0f;

        /// Minimum allowed gain value in decibels.
        public const float minGainDb = -24.0f;

        /// Maximum allowed reverb brightness modifier value.
        public const float maxReverbBrightness = 1.0f;

        /// Minimum allowed reverb brightness modifier value.
        public const float minReverbBrightness = -1.0f;

        /// Maximum allowed reverb time modifier value.
        public const float maxReverbTime = 3.0f;

        /// Maximum allowed reflectivity multiplier of a room surface material.
        public const float maxReflectivity = 2.0f;

        // Right-handed to left-handed matrix converter (and vice versa).
        private static readonly Matrix4x4 flipZ = Matrix4x4.Scale(new Vector3(1, 1, -1));

        // Get a handle to the Resonance Audio Listener FMOD Plugin.
        private static readonly string listenerPluginName = "Resonance Audio Listener";

        // Size of |RoomProperties| struct in bytes.
        private static readonly int roomPropertiesSize = FMOD.MarshalHelper.SizeOf(typeof(RoomProperties));

        // Plugin data parameter index for the room properties.
        private static readonly int roomPropertiesIndex = 1;

        // Boundaries instance to be used in room detection logic.
        private static Bounds bounds = new Bounds(Vector3.zero, Vector3.zero);

        // Container to store the currently active rooms in the scene.
        private static List<FmodResonanceAudioRoom> enabledRooms = new List<FmodResonanceAudioRoom>();

        // Current listener position.
        private static FMOD.VECTOR listenerPositionFmod = new FMOD.VECTOR();

        // FMOD Resonance Audio Listener Plugin.
        private static FMOD.DSP listenerPlugin;

        /// Updates the room effects of the environment with given |room| properties.
        /// @note This should only be called from the main Unity thread.
        public static void UpdateAudioRoom(FmodResonanceAudioRoom room, bool roomEnabled)
        {
            // Update the enabled rooms list.
            if (roomEnabled)
            {
                if (!enabledRooms.Contains(room))
                {
                    enabledRooms.Add(room);
                }
            }
            else
            {
                enabledRooms.Remove(room);
            }
            // Update the current room effects to be applied.
            if (enabledRooms.Count > 0)
            {
                FmodResonanceAudioRoom currentRoom = enabledRooms[enabledRooms.Count - 1];
                RoomProperties roomProperties = GetRoomProperties(currentRoom);
                // Pass the room properties into a pointer.
                IntPtr roomPropertiesPtr = Marshal.AllocHGlobal(roomPropertiesSize);
                Marshal.StructureToPtr(roomProperties, roomPropertiesPtr, false);
                ListenerPlugin.setParameterData(roomPropertiesIndex, GetBytes(roomPropertiesPtr,
                                                                               roomPropertiesSize));
                Marshal.FreeHGlobal(roomPropertiesPtr);
            }
            else
            {
                // Set the room properties to a null room, which will effectively disable the room effects.
                ListenerPlugin.setParameterData(roomPropertiesIndex, GetBytes(IntPtr.Zero, 0));
            }
        }

        /// Returns whether the listener is currently inside the given |room| boundaries.
        public static bool IsListenerInsideRoom(FmodResonanceAudioRoom room)
        {
            // Compute the room position relative to the listener.
            FMOD.VECTOR unused;
            RuntimeManager.CoreSystem.get3DListenerAttributes(0, out listenerPositionFmod, out unused,
                                                                  out unused, out unused);
            Vector3 listenerPosition = new Vector3(listenerPositionFmod.x, listenerPositionFmod.y,
                                                   listenerPositionFmod.z);
            Vector3 relativePosition = listenerPosition - room.transform.position;
            Quaternion rotationInverse = Quaternion.Inverse(room.transform.rotation);
            // Set the size of the room as the boundary and return whether the listener is inside.
            bounds.size = Vector3.Scale(room.transform.lossyScale, room.size);
            return bounds.Contains(rotationInverse * relativePosition);
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct RoomProperties
        {
            // Center position of the room in world space.
            public float positionX;
            public float positionY;
            public float positionZ;

            // Rotation (quaternion) of the room in world space.
            public float rotationX;
            public float rotationY;
            public float rotationZ;
            public float rotationW;

            // Size of the shoebox room in world space.
            public float dimensionsX;
            public float dimensionsY;
            public float dimensionsZ;

            // Material name of each surface of the shoebox room.
            public FmodResonanceAudioRoom.SurfaceMaterial materialLeft;
            public FmodResonanceAudioRoom.SurfaceMaterial materialRight;
            public FmodResonanceAudioRoom.SurfaceMaterial materialBottom;
            public FmodResonanceAudioRoom.SurfaceMaterial materialTop;
            public FmodResonanceAudioRoom.SurfaceMaterial materialFront;
            public FmodResonanceAudioRoom.SurfaceMaterial materialBack;

            // User defined uniform scaling factor for reflectivity. This parameter has no effect when set
            // to 1.0f.
            public float reflectionScalar;

            // User defined reverb tail gain multiplier. This parameter has no effect when set to 0.0f.
            public float reverbGain;

            // Adjusts the reverberation time across all frequency bands. RT60 values are multiplied by this
            // factor. Has no effect when set to 1.0f.
            public float reverbTime;

            // Controls the slope of a line from the lowest to the highest RT60 values (increases high
            // frequency RT60s when positive, decreases when negative). Has no effect when set to 0.0f.
            public float reverbBrightness;
        };

        // Returns the FMOD Resonance Audio Listener Plugin.
        private static FMOD.DSP ListenerPlugin
        {
            get
            {
                if (!listenerPlugin.hasHandle())
                {
                    listenerPlugin = Initialize();
                }
                return listenerPlugin;
            }
        }

        // Converts given |db| value to its amplitude equivalent where 'dB = 20 * log10(amplitude)'.
        private static float ConvertAmplitudeFromDb(float db)
        {
            return Mathf.Pow(10.0f, 0.05f * db);
        }

        // Converts given |position| and |rotation| from Unity space to audio space.
        private static void ConvertAudioTransformFromUnity(ref Vector3 position,
          ref Quaternion rotation)
        {
            // Compose the transformation matrix.
            Matrix4x4 transformMatrix = Matrix4x4.TRS(position, rotation, Vector3.one);
            // Convert the transformation matrix from left-handed to right-handed.
            transformMatrix = flipZ * transformMatrix * flipZ;
            // Update |position| and |rotation| respectively.
            position = transformMatrix.GetColumn(3);
            rotation = Quaternion.LookRotation(transformMatrix.GetColumn(2), transformMatrix.GetColumn(1));
        }

        // Returns a byte array of |length| created from |ptr|.
        private static byte[] GetBytes(IntPtr ptr, int length)
        {
            if (ptr != IntPtr.Zero)
            {
                byte[] byteArray = new byte[length];
                Marshal.Copy(ptr, byteArray, 0, length);
                return byteArray;
            }
            // Return an empty array if the pointer is null.
            return new byte[1];
        }

        // Returns room properties of the given |room|.
        private static RoomProperties GetRoomProperties(FmodResonanceAudioRoom room)
        {
            RoomProperties roomProperties;
            Vector3 position = room.transform.position;
            Quaternion rotation = room.transform.rotation;
            Vector3 scale = Vector3.Scale(room.transform.lossyScale, room.size);
            ConvertAudioTransformFromUnity(ref position, ref rotation);
            roomProperties.positionX = position.x;
            roomProperties.positionY = position.y;
            roomProperties.positionZ = position.z;
            roomProperties.rotationX = rotation.x;
            roomProperties.rotationY = rotation.y;
            roomProperties.rotationZ = rotation.z;
            roomProperties.rotationW = rotation.w;
            roomProperties.dimensionsX = scale.x;
            roomProperties.dimensionsY = scale.y;
            roomProperties.dimensionsZ = scale.z;
            roomProperties.materialLeft = room.leftWall;
            roomProperties.materialRight = room.rightWall;
            roomProperties.materialBottom = room.floor;
            roomProperties.materialTop = room.ceiling;
            roomProperties.materialFront = room.frontWall;
            roomProperties.materialBack = room.backWall;
            roomProperties.reverbGain = ConvertAmplitudeFromDb(room.reverbGainDb);
            roomProperties.reverbTime = room.reverbTime;
            roomProperties.reverbBrightness = room.reverbBrightness;
            roomProperties.reflectionScalar = room.reflectivity;
            return roomProperties;
        }

        // Initializes and returns the FMOD Resonance Audio Listener Plugin.
        private static FMOD.DSP Initialize()
        {
            // Search through all busses on in banks.
            int numBanks = 0;
            FMOD.DSP dsp = new FMOD.DSP();
            FMOD.Studio.Bank[] banks = null;
            RuntimeManager.StudioSystem.getBankCount(out numBanks);
            RuntimeManager.StudioSystem.getBankList(out banks);
            for (int currentBank = 0; currentBank < numBanks; ++currentBank)
            {
                int numBusses = 0;
                FMOD.Studio.Bus[] busses = null;
                banks[currentBank].getBusCount(out numBusses);
                banks[currentBank].getBusList(out busses);
                RuntimeManager.StudioSystem.flushCommands();
                for (int currentBus = 0; currentBus < numBusses; ++currentBus)
                {
                    // Make sure the channel group of the current bus is assigned properly.
                    string busPath = null;
                    busses[currentBus].getPath(out busPath);
                    RuntimeManager.StudioSystem.getBus(busPath, out busses[currentBus]);
                    RuntimeManager.StudioSystem.flushCommands();
                    FMOD.ChannelGroup channelGroup;
                    busses[currentBus].getChannelGroup(out channelGroup);
                    RuntimeManager.StudioSystem.flushCommands();
                    if (channelGroup.hasHandle())
                    {
                        int numDsps = 0;
                        channelGroup.getNumDSPs(out numDsps);
                        for (int currentDsp = 0; currentDsp < numDsps; ++currentDsp)
                        {
                            channelGroup.getDSP(currentDsp, out dsp);
                            string dspNameSb;
                            int unusedInt = 0;
                            uint unusedUint = 0;
                            dsp.getInfo(out dspNameSb, out unusedUint, out unusedInt, out unusedInt, out unusedInt);
                            if (dspNameSb.ToString().Equals(listenerPluginName) && dsp.hasHandle())
                            {
                                return dsp;
                            }
                        }
                    }
                }
            }
            RuntimeUtils.DebugLogError(listenerPluginName + " not found in the FMOD project.");
            return dsp;
        }
    }
}
