// 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.IO;
using System.Runtime.InteropServices;

/// This is the main GVR audio class that communicates with the native code implementation of
/// the audio system. Native functions of the system can only be called through this class to
/// preserve the internal system functionality. Public function calls are *not* thread-safe.
public static class GvrAudio {
  /// Audio system rendering quality.
  public enum Quality {
    Stereo = 0,  ///< Stereo-only rendering
    Low = 1,  ///< Low quality binaural rendering (first-order HRTF)
    High = 2  ///< High quality binaural rendering (third-order HRTF)
  }

  /// Native audio spatializer effect data.
  public enum SpatializerData {
    Id = 0,  /// ID.
    Type = 1,  /// Spatializer type.
    NumChannels = 2,  /// Number of input channels.
    ChannelSet = 3,  /// Soundfield channel set.
    Gain = 4,  /// Gain.
    DistanceAttenuation = 5,  /// Computed distance attenuation.
    MinDistance = 6,  /// Minimum distance for distance-based attenuation.
    ZeroOutput = 7,  /// Should zero out the output buffer?
  }

  /// Native audio spatializer type.
  public enum SpatializerType {
    Source = 0,  /// 3D sound object.
    Soundfield = 1  /// First-order ambisonic soundfield.
  }

  /// System sampling rate.
  public static int SampleRate {
    get { return sampleRate; }
  }
  private static int sampleRate = -1;

  /// System number of output channels.
  public static int NumChannels {
    get { return numChannels; }
  }
  private static int numChannels = -1;

  /// System number of frames per buffer.
  public static int FramesPerBuffer {
    get { return framesPerBuffer; }
  }
  private static int framesPerBuffer = -1;

  /// Initializes the audio system with the current audio configuration.
  /// @note This should only be called from the main Unity thread.
  public static void Initialize (GvrAudioListener listener, Quality quality) {
    if (!initialized) {
#if !UNITY_EDITOR && UNITY_ANDROID
      SetApplicationState();
#endif
      // Initialize the audio system.
      AudioConfiguration config = AudioSettings.GetConfiguration();
      sampleRate = config.sampleRate;
      numChannels = (int)config.speakerMode;
      framesPerBuffer = config.dspBufferSize;
      if (numChannels != (int)AudioSpeakerMode.Stereo) {
        Debug.LogError("Only 'Stereo' speaker mode is supported by GVR Audio.");
        return;
      }
      Initialize(quality, sampleRate, numChannels, framesPerBuffer);
      listenerTransform = listener.transform;

      initialized = true;
    } else if (listener.transform != listenerTransform) {
      Debug.LogError("Only one GvrAudioListener component is allowed in the scene.");
      GvrAudioListener.Destroy(listener);
    }
  }

  /// Shuts down the audio system.
  /// @note This should only be called from the main Unity thread.
  public static void Shutdown (GvrAudioListener listener) {
    if (initialized && listener.transform == listenerTransform) {
      initialized = false;

      Shutdown();
      sampleRate = -1;
      numChannels = -1;
      framesPerBuffer = -1;
      listenerTransform = null;
    }
  }

  /// Updates the audio listener.
  /// @note This should only be called from the main Unity thread.
  public static void UpdateAudioListener (float globalGainDb, LayerMask occlusionMask) {
    if (initialized) {
      occlusionMaskValue = occlusionMask.value;
      SetListenerGain(ConvertAmplitudeFromDb(globalGainDb));
    }
  }

  /// Creates a new first-order ambisonic soundfield with a unique id.
  /// @note This should only be called from the main Unity thread.
  public static int CreateAudioSoundfield () {
    int soundfieldId = -1;
    if (initialized) {
      soundfieldId = CreateSoundfield(numFoaChannels);
    }
    return soundfieldId;
  }

  /// Destroys the soundfield with given |id|.
  /// @note This should only be called from the main Unity thread.
  public static void DestroyAudioSoundfield (int id) {
    if (initialized) {
      DestroySoundfield(id);
    }
  }

  /// Creates a new audio source with a unique id.
  /// @note This should only be called from the main Unity thread.
  public static int CreateAudioSource (bool hrtfEnabled) {
    int sourceId = -1;
    if (initialized) {
      sourceId = CreateSource(hrtfEnabled);
    }
    return sourceId;
  }

  /// Destroys the audio source with given |id|.
  /// @note This should only be called from the main Unity thread.
  public static void DestroyAudioSource (int id) {
    if (initialized) {
      DestroySource(id);
    }
  }

  /// Updates the audio |source| with given |id| and its properties.
  /// @note This should only be called from the main Unity thread.
  public static void UpdateAudioSource (int id, GvrAudioSource source, float currentOcclusion) {
    if (initialized) {
      SetSourceBypassRoomEffects(id, source.bypassRoomEffects);
      SetSourceDirectivity(id, source.directivityAlpha, source.directivitySharpness);
      SetSourceListenerDirectivity(id, source.listenerDirectivityAlpha,
                                   source.listenerDirectivitySharpness);
      SetSourceOcclusionIntensity(id, currentOcclusion);
    }
  }

  /// 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(GvrAudioRoom 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(initialized) {
      if (enabledRooms.Count > 0) {
        GvrAudioRoom currentRoom = enabledRooms[enabledRooms.Count - 1];
        RoomProperties roomProperties = GetRoomProperties(currentRoom);
        // Pass the room properties into a pointer.
        IntPtr roomPropertiesPtr = Marshal.AllocHGlobal(Marshal.SizeOf(roomProperties));
        Marshal.StructureToPtr(roomProperties, roomPropertiesPtr, false);
        SetRoomProperties(roomPropertiesPtr);
        Marshal.FreeHGlobal(roomPropertiesPtr);
      } else {
        // Set the room properties to null, which will effectively disable the room effects.
        SetRoomProperties(IntPtr.Zero);
      }
    }
  }

  /// Computes the occlusion intensity of a given |source| using point source detection.
  /// @note This should only be called from the main Unity thread.
  public static float ComputeOcclusion (Transform sourceTransform) {
    float occlusion = 0.0f;
    if (initialized) {
      Vector3 listenerPosition = listenerTransform.position;
      Vector3 sourceFromListener = sourceTransform.position - listenerPosition;
      RaycastHit[] hits = Physics.RaycastAll(listenerPosition, sourceFromListener,
                                             sourceFromListener.magnitude, occlusionMaskValue);
      foreach (RaycastHit hit in hits) {
        if (hit.transform != listenerTransform && hit.transform != sourceTransform) {
          occlusion += 1.0f;
        }
      }
    }
    return occlusion;
  }

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

  /// Generates a set of points to draw a 2D polar pattern.
  public static Vector2[] Generate2dPolarPattern (float alpha, float order, int resolution) {
    Vector2[] points = new Vector2[resolution];
    float interval = 2.0f * Mathf.PI / resolution;
    for (int i = 0; i < resolution; ++i) {
      float theta = i * interval;
      // Magnitude |r| for |theta| in radians.
      float r = Mathf.Pow(Mathf.Abs((1 - alpha) + alpha * Mathf.Cos(theta)), order);
      points[i] = new Vector2(r * Mathf.Sin(theta), r * Mathf.Cos(theta));
    }
    return points;
  }

  /// Returns whether the listener is currently inside the given |room| boundaries.
  public static bool IsListenerInsideRoom(GvrAudioRoom room) {
    bool isInside = false;
    if(initialized) {
      Vector3 relativePosition = listenerTransform.position - room.transform.position;
      Quaternion rotationInverse = Quaternion.Inverse(room.transform.rotation);

      bounds.size = Vector3.Scale(room.transform.lossyScale, room.size);
      isInside = bounds.Contains(rotationInverse * relativePosition);
    }
    return isInside;
  }

  /// Listener directivity GUI color.
  public static readonly Color listenerDirectivityColor = 0.65f * Color.magenta;

  /// Source directivity GUI color.
  public static readonly Color sourceDirectivityColor = 0.65f * Color.blue;

  /// Minimum distance threshold between |minDistance| and |maxDistance|.
  public const float distanceEpsilon = 0.01f;

  /// Max distance limit that can be set for volume rolloff.
  public const float maxDistanceLimit = 1000000.0f;

  /// Min distance limit that can be set for volume rolloff.
  public const float minDistanceLimit = 990099.0f;

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

  /// Source occlusion detection rate in seconds.
  public const float occlusionDetectionInterval = 0.2f;

  // Number of first-order ambisonic input channels.
  public const int numFoaChannels = 4;

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

  // Converts given |position| and |rotation| from Unity space to audio space.
  private static void ConvertAudioTransformFromUnity (ref Vector3 position,
                                                      ref Quaternion rotation) {
    pose.SetRightHanded(Matrix4x4.TRS(position, rotation, Vector3.one));
    position = pose.Position;
    rotation = pose.Orientation;
  }

  // Returns room properties of the given |room|.
  private static RoomProperties GetRoomProperties(GvrAudioRoom 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;
  }

  // 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<GvrAudioRoom> enabledRooms = new List<GvrAudioRoom>();

  // Denotes whether the system is initialized properly.
  private static bool initialized = false;

  // Listener transform.
  private static Transform listenerTransform = null;

  // Occlusion layer mask.
  private static int occlusionMaskValue = -1;

  // 3D pose instance to be used in transform space conversion.
  private static MutablePose3D pose = new MutablePose3D();

#if !UNITY_EDITOR && UNITY_ANDROID
  private const string GvrAudioClass = "com.google.vr.audio.unity.GvrAudio";

  private static void SetApplicationState() {
    using (var gvrAudioClass = Gvr.Internal.BaseAndroidDevice.GetClass(GvrAudioClass)) {
      Gvr.Internal.BaseAndroidDevice.CallStaticMethod(gvrAudioClass, "setUnityApplicationState");
    }
  }
#endif

#if UNITY_IOS
  private const string pluginName = "__Internal";
#else
  private const string pluginName = "audioplugingvrunity";
#endif

  // Listener handlers.
  [DllImport(pluginName)]
  private static extern void SetListenerGain (float gain);

  // Soundfield handlers.
  [DllImport(pluginName)]
  private static extern int CreateSoundfield (int numChannels);

  [DllImport(pluginName)]
  private static extern void DestroySoundfield (int soundfieldId);

  // Source handlers.
  [DllImport(pluginName)]
  private static extern int CreateSource (bool enableHrtf);

  [DllImport(pluginName)]
  private static extern void DestroySource (int sourceId);

  [DllImport(pluginName)]
  private static extern void SetSourceBypassRoomEffects (int sourceId, bool bypassRoomEffects);

  [DllImport(pluginName)]
  private static extern void SetSourceDirectivity (int sourceId, float alpha, float order);

  [DllImport(pluginName)]
  private static extern void SetSourceListenerDirectivity (int sourceId, float alpha, float order);

  [DllImport(pluginName)]
  private static extern void SetSourceOcclusionIntensity (int sourceId, float intensity);

  // Room handlers.
  [DllImport(pluginName)]
  private static extern void SetRoomProperties (IntPtr roomProperties);

  // System handlers.
  [DllImport(pluginName)]
  private static extern void Initialize (Quality quality, int sampleRate, int numChannels,
                                         int framesPerBuffer);

  [DllImport(pluginName)]
  private static extern void Shutdown ();
}
