// Copyright 2016 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;

/// This is the main GVR 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 FmodGvrAudio {
  /// 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(FmodGvrAudioRoom 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) {
      FmodGvrAudioRoom 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(FmodGvrAudioRoom room) {
    // Compute the room position relative to the listener.
    FMOD.VECTOR unused;
    RuntimeManager.LowlevelSystem.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);
  }

  /// 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;

  [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 FmodGvrAudioRoom.SurfaceMaterial materialLeft;
    public FmodGvrAudioRoom.SurfaceMaterial materialRight;
    public FmodGvrAudioRoom.SurfaceMaterial materialBottom;
    public FmodGvrAudioRoom.SurfaceMaterial materialTop;
    public FmodGvrAudioRoom.SurfaceMaterial materialFront;
    public FmodGvrAudioRoom.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;

    // Parameter which allows the reverberation time across all frequency bands to be increased or
    // decreased. This parameter has no effect when set to 1.0f.
    public float reverbTime;

    // Parameter which allows the ratio of high frequncy reverb components to low frequency reverb
    // components to be adjusted. This parameter has no effect when set to 0.0f.
    public float reverbBrightness;
  };

  // Returns the FMOD GVR 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(FmodGvrAudioRoom 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 GVR 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;
            }
          }
        }
      }
    }
    Debug.LogError(listenerPluginName + " not found in the FMOD project.");
    return dsp;
  }

  // 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 GVR Listener FMOD Plugin.
  private static readonly string listenerPluginName = "Google GVR Listener";

  // Size of |RoomProperties| struct in bytes.
  private static readonly int roomPropertiesSize = Marshal.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<FmodGvrAudioRoom> enabledRooms = new List<FmodGvrAudioRoom>();

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

  // FMOD GVR Listener Plugin.
  private static FMOD.DSP listenerPlugin;
}
