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

// The controller is not available for versions of Unity without the
// GVR native integration.

using UnityEngine;
using UnityEngine.VR;
using System.Collections;

/// The GvrArmModel is a standard interface to interact with a scene with the controller.
/// It is responsible for:
/// -  Determining the orientation and location of the controller.
/// -  Predict the location of the shoulder, elbow, wrist, and pointer.
///
/// There should only be one instance in the scene, and it should be attached
/// to the GvrController.
[RequireComponent(typeof(GvrController))]
public class GvrArmModel : MonoBehaviour {
#if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)
  private static GvrArmModel instance = null;

  /// Initial relative location of the shoulder (meters).
  private static readonly Vector3 DEFAULT_SHOULDER_RIGHT = new Vector3(0.19f, -0.19f, -0.03f);

  /// The range of movement from the elbow position due to accelerometer (meters).
  private static readonly Vector3 ELBOW_MIN_RANGE = new Vector3(-0.05f, -0.1f, 0.0f);
  private static readonly Vector3 ELBOW_MAX_RANGE = new Vector3(0.05f, 0.1f, 0.2f);

  /// Offset of the laser pointer origin relative to the wrist (meters)
  private static readonly Vector3 POINTER_OFFSET = new Vector3(0.0f, -0.009f, 0.099f);

  /// Rest position parameters for arm model (meters).
  private static readonly Vector3 ELBOW_POSITION = new Vector3(0.195f, -0.5f, -0.075f);
  private static readonly Vector3 WRIST_POSITION = new Vector3(0.0f, 0.0f, 0.25f);
  private static readonly Vector3 ARM_EXTENSION_OFFSET = new Vector3(-0.13f, 0.14f, 0.08f);

  /// Strength of the acceleration filter (unitless).
  private const float GRAVITY_CALIB_STRENGTH = 0.999f;

  /// Strength of the velocity suppression (unitless).
  private const float VELOCITY_FILTER_SUPPRESS = 0.99f;

  /// Strength of the velocity suppression during low acceleration (unitless).
  private const float LOW_ACCEL_VELOCITY_SUPPRESS = 0.9f;

  /// Strength of the acceleration suppression during low velocity (unitless).
  private const float LOW_VELOCITY_ACCEL_SUPPRESS = 0.5f;

  /// The minimum allowable accelerometer reading before zeroing (m/s^2).
  private const float MIN_ACCEL = 1.0f;

  /// The expected force of gravity (m/s^2).
  private const float GRAVITY_FORCE = 9.807f;

  /// Amount of normalized alpha transparency to change per second.
  private const float DELTA_ALPHA = 4.0f;

  /// Angle ranges the for arm extension offset to start and end (degrees).
  private const float MIN_EXTENSION_ANGLE = 7.0f;
  private const float MAX_EXTENSION_ANGLE = 60.0f;

  /// Increases elbow bending as the controller moves up (unitless).
  private const float EXTENSION_WEIGHT = 0.4f;

  /// Offset of the elbow due to the accelerometer
  private Vector3 elbowOffset;

  /// Forward direction of the arm model.
  private Vector3 torsoDirection;

  /// Filtered velocity of the controller.
  private Vector3 filteredVelocity;

  /// Filtered acceleration of the controller.
  private Vector3 filteredAccel;

  /// Used to calibrate the ambient gravitational force.
  private Vector3 zeroAccel;

  /// Indicates if this is the first frame to receive new IMU measurements.
  private bool firstUpdate;

  /// Multiplier for handedness such that 1 = Right, 0 = Center, -1 = left.
  private Vector3 handedMultiplier;

#if UNITY_EDITOR
  private Camera editorHeadCamera;
#endif // UNITY_EDITOR

  /// Use the GvrController singleton to obtain a singleton for this class.
  public static GvrArmModel Instance {
    get {
      if (instance == null) {
        instance = GvrController.ArmModel;
      }
      return instance != null && instance.isActiveAndEnabled ? instance : null;
    }
  }

  /// Represents when gaze-following behavior should occur.
  public enum GazeBehavior {
    Never,        /// The shoulder will never follow the gaze.
    DuringMotion, /// The shoulder will follow the gaze during controller motion.
    Always        /// The shoulder will always follow the gaze.
  }

  /// Height of the elbow  (m).
  [Range(0.0f, 0.2f)]
  public float addedElbowHeight = 0.0f;

  /// Depth of the elbow  (m).
  [Range(0.0f, 0.2f)]
  public float addedElbowDepth = 0.0f;

  /// The Downward tilt or pitch of the laser pointer relative to the controller (degrees).
  [Range(0.0f, 30.0f)]
  public float pointerTiltAngle = 15.0f;

  /// Controller distance from the face after which the controller disappears (meters).
  [Range(0.0f, 0.4f)]
  public float fadeDistanceFromFace = 0.32f;

  /// Controller distance from face after which the tooltips appear (meters).
  [Range(0.4f, 0.6f)]
  public float tooltipMinDistanceFromFace = 0.45f;

  /// When the angle (degrees) between the controller and the head is larger than
  /// this value, the tooltips disappear.
  /// If the value is 180, then the tooltips are always shown.
  /// If the value is 90, the tooltips are only shown when they are facing the camera.
  [Range(0, 180)]
  public int tooltipMaxAngleFromCamera = 80;

  /// Determines if the shoulder should follow the gaze
  public GazeBehavior followGaze = GazeBehavior.DuringMotion;

  /// Determines if the accelerometer should be used.
  public bool useAccelerometer = false;

  /// Vector to represent the pointer's location.
  /// NOTE: This is in meatspace coordinates.
  public Vector3 pointerPosition { get; private set; }

  /// Quaternion to represent the pointer's rotation.
  /// NOTE: This is in meatspace coordinates.
  public Quaternion pointerRotation { get; private set; }

  /// Vector to represent the wrist's location.
  /// NOTE: This is in meatspace coordinates.
  public Vector3 wristPosition { get; private set; }

  /// Quaternion to represent the wrist's rotation.
  /// NOTE: This is in meatspace coordinates.
  public Quaternion wristRotation { get; private set; }

  /// Vector to represent the elbow's location.
  /// NOTE: This is in meatspace coordinates.
  public Vector3 elbowPosition { get; private set; }

  /// Quaternion to represent the elbow's rotation.
  /// NOTE: This is in meatspace coordinates.
  public Quaternion elbowRotation { get; private set; }

  /// Vector to represent the shoulder's location.
  /// NOTE: This is in meatspace coordinates.
  public Vector3 shoulderPosition { get; private set; }

  /// Vector to represent the shoulder's location.
  /// NOTE: This is in meatspace coordinates.
  public Quaternion shoulderRotation { get; private set; }

  /// The suggested rendering alpha value of the controller.
  /// This is to prevent the controller from intersecting the face.
  /// The range is always 0 - 1 but can be scaled by individual
  /// objects when using the GvrBaseControllerVisual script.
  public float preferredAlpha { get; private set; }

  /// The suggested rendering alpha value of the controller tooltips.
  /// This is to only display the tooltips when the player is looking
  /// at the controller, and also to prevent the tooltips from intersecting the
  /// player's face.
  public float tooltipAlphaValue { get; private set; }

  /// Event handler that occurs when the state of the ArmModel is updated.
  public delegate void OnArmModelUpdateEvent();
  public event OnArmModelUpdateEvent OnArmModelUpdate;

  void Start() {
    // Obtain the Gvr controller from the scene.
    GvrController controller = GetComponent<GvrController>();

    UpdateHandedness();

    // Register the controller update listener.
    controller.OnControllerUpdate += OnControllerUpdate;

    // Reset other relevant state.
    firstUpdate = true;
    elbowOffset = Vector3.zero;
    zeroAccel.Set(0, GRAVITY_FORCE, 0);
  }

  void OnDestroy() {
    // Unregister the controller update listener.
    GvrController controller = GetComponent<GvrController>();
    controller.OnControllerUpdate -= OnControllerUpdate;

    // Reset the singleton instance.
    instance = null;
  }

#if UNITY_EDITOR
  void Update() {
    editorHeadCamera = Camera.main;
  }
#endif // UNITY_EDITOR

  private void OnControllerUpdate() {
    if (GvrController.Recentered) {
      ResetState();
    }

    UpdateHandedness();
    UpdateTorsoDirection();
    if (GvrController.State == GvrConnectionState.Connected) {
      UpdateFromController();
    } else {
      ResetState();
    }
    if (useAccelerometer) {
      UpdateVelocity();
      TransformElbow();
    } else {
      elbowOffset = Vector3.zero;
    }
    ApplyArmModel();
    UpdateTransparency();
    UpdatePointer();

    firstUpdate = false;
    if (OnArmModelUpdate != null) {
      OnArmModelUpdate();
    }
  }

  private void UpdateHandedness() {
    // Update user handedness if the setting has changed
    GvrSettings.UserPrefsHandedness handedness = GvrSettings.Handedness;

    // Determine handedness multiplier.
    handedMultiplier.Set(0, 1, 1);
    if (handedness == GvrSettings.UserPrefsHandedness.Right) {
      handedMultiplier.x = 1.0f;
    } else if (handedness == GvrSettings.UserPrefsHandedness.Left) {
      handedMultiplier.x = -1.0f;
    }

    // Place the shoulder in anatomical positions based on the height and handedness.
    shoulderRotation = Quaternion.identity;
    shoulderPosition = Vector3.Scale(DEFAULT_SHOULDER_RIGHT, handedMultiplier);
  }

  private Vector3 GetHeadOrientation() {
#if UNITY_EDITOR
    if (editorHeadCamera == null) {
      Debug.LogWarning("No Head Camera.");
      return Vector3.forward;
    }

    Vector3 forward = editorHeadCamera.transform.forward;
    if (editorHeadCamera.transform.parent != null) {
      forward = editorHeadCamera.transform.parent.InverseTransformDirection(forward);
    }

    return forward;
#else
    return InputTracking.GetLocalRotation(VRNode.Head) * Vector3.forward;
#endif // UNITY_EDITOR
  }

  private void UpdateTorsoDirection() {
    // Ignore updates here if requested.
    if (followGaze == GazeBehavior.Never) {
      return;
    }

    // Determine the gaze direction horizontally.
    Vector3 gazeDirection = GetHeadOrientation();
    gazeDirection.y = 0.0f;
    gazeDirection.Normalize();

    // Use the gaze direction to update the forward direction.
    if (followGaze == GazeBehavior.Always || firstUpdate) {
      torsoDirection = gazeDirection;
    } else if (followGaze == GazeBehavior.DuringMotion) {
      float angularVelocity = GvrController.Gyro.magnitude;
      float gazeFilterStrength = Mathf.Clamp((angularVelocity - 0.2f) / 45.0f, 0.0f, 0.1f);
      torsoDirection = Vector3.Slerp(torsoDirection, gazeDirection, gazeFilterStrength);
    }

    // Rotate the fixed joints.
    Quaternion gazeRotation = Quaternion.FromToRotation(Vector3.forward, torsoDirection);
    shoulderRotation = gazeRotation;
    shoulderPosition = gazeRotation * shoulderPosition;
  }

  private void UpdateFromController() {
    // Get the orientation-adjusted acceleration.
    Vector3 accel = GvrController.Orientation * GvrController.Accel;

    // Very slowly calibrate gravity force out of acceleration.
    zeroAccel = zeroAccel * GRAVITY_CALIB_STRENGTH + accel * (1.0f - GRAVITY_CALIB_STRENGTH);
    filteredAccel = accel - zeroAccel;

    // If no tracking history, reset the velocity.
    if (firstUpdate) {
      filteredVelocity = Vector3.zero;
    }

    // IMPORTANT: The accelerometer is not reliable at these low magnitudes
    // so ignore it to prevent drift.
    if (filteredAccel.magnitude < MIN_ACCEL) {
      // Suppress the acceleration.
      filteredAccel = Vector3.zero;
      filteredVelocity *= LOW_ACCEL_VELOCITY_SUPPRESS;
    } else {
      // If the velocity is decreasing, prevent snap-back by reducing deceleration.
      Vector3 newVelocity = filteredVelocity + filteredAccel * Time.deltaTime;
      if (newVelocity.sqrMagnitude < filteredVelocity.sqrMagnitude) {
        filteredAccel *= LOW_VELOCITY_ACCEL_SUPPRESS;
      }
    }
  }

  private void UpdateVelocity() {
    // Update the filtered velocity.
    filteredVelocity += filteredAccel * Time.deltaTime;
    filteredVelocity *= VELOCITY_FILTER_SUPPRESS;
  }

  private void ResetState() {
    // We've lost contact, quickly reset the state.
    filteredVelocity *= 0.5f;
    filteredAccel *= 0.5f;
    firstUpdate = true;
  }

  private void TransformElbow() {
    // Apply the filtered velocity to update the elbow offset position.
    if (useAccelerometer) {
      elbowOffset += filteredVelocity * Time.deltaTime;
      elbowOffset.x = Mathf.Clamp(elbowOffset.x, ELBOW_MIN_RANGE.x, ELBOW_MAX_RANGE.x);
      elbowOffset.y = Mathf.Clamp(elbowOffset.y, ELBOW_MIN_RANGE.y, ELBOW_MAX_RANGE.y);
      elbowOffset.z = Mathf.Clamp(elbowOffset.z, ELBOW_MIN_RANGE.z, ELBOW_MAX_RANGE.z);
    }
  }

  private void ApplyArmModel() {
    // Find the controller's orientation relative to the player
    Quaternion controllerOrientation = GvrController.Orientation;
    controllerOrientation = Quaternion.Inverse(shoulderRotation) * controllerOrientation;

    // Get the relative positions of the joints
    elbowPosition = ELBOW_POSITION + new Vector3(0.0f, addedElbowHeight, addedElbowDepth);
    elbowPosition = Vector3.Scale(elbowPosition, handedMultiplier) + elbowOffset;
    wristPosition = Vector3.Scale(WRIST_POSITION, handedMultiplier);
    Vector3 armExtensionOffset = Vector3.Scale(ARM_EXTENSION_OFFSET, handedMultiplier);

    // Extract just the x rotation angle
    Vector3 controllerForward = controllerOrientation * Vector3.forward;
    float xAngle = 90.0f - Vector3.Angle(controllerForward, Vector3.up);

    // Remove the z rotation from the controller
    Quaternion xyRotation = Quaternion.FromToRotation(Vector3.forward, controllerForward);

    // Offset the elbow by the extension
    float normalizedAngle = (xAngle - MIN_EXTENSION_ANGLE) / (MAX_EXTENSION_ANGLE - MIN_EXTENSION_ANGLE);
    float extensionRatio = Mathf.Clamp(normalizedAngle, 0.0f, 1.0f);
    if (!useAccelerometer) {
      elbowPosition += armExtensionOffset * extensionRatio;
    }

    // Calculate the lerp interpolation factor
    float totalAngle = Quaternion.Angle(xyRotation, Quaternion.identity);
    float lerpSuppresion = 1.0f - Mathf.Pow(totalAngle / 180.0f, 6);
    float lerpValue = lerpSuppresion * (0.4f + 0.6f * extensionRatio * EXTENSION_WEIGHT);

    // Apply the absolute rotations to the joints
    Quaternion lerpRotation = Quaternion.Lerp(Quaternion.identity, xyRotation, lerpValue);
    elbowRotation = shoulderRotation * Quaternion.Inverse(lerpRotation) * controllerOrientation;
    wristRotation = shoulderRotation * controllerOrientation;

    // Determine the relative positions
    elbowPosition = shoulderRotation * elbowPosition;
    wristPosition = elbowPosition + elbowRotation * wristPosition;
  }

  private void UpdateTransparency() {
    // Determine how vertical the controller is pointing.
    float animationDelta = DELTA_ALPHA * Time.deltaTime;
    float distToFace = Vector3.Distance(wristPosition, Vector3.zero);
    if (distToFace < fadeDistanceFromFace) {
      preferredAlpha = Mathf.Max(0.0f, preferredAlpha - animationDelta);
    } else {
      preferredAlpha = Mathf.Min(1.0f, preferredAlpha + animationDelta);
    }

    float dot = Vector3.Dot(wristRotation * Vector3.up, -wristPosition.normalized);
    float minDot = (tooltipMaxAngleFromCamera - 90.0f) / -90.0f;
    if (distToFace < fadeDistanceFromFace
        || distToFace > tooltipMinDistanceFromFace
        || dot < minDot) {
      tooltipAlphaValue = Mathf.Max(0.0f, tooltipAlphaValue - animationDelta);
    } else {
      tooltipAlphaValue = Mathf.Min(1.0f, tooltipAlphaValue + animationDelta);
    }
  }

  private void UpdatePointer() {
    // Determine the direction of the ray.
    pointerPosition = wristPosition + wristRotation * POINTER_OFFSET;
    pointerRotation = wristRotation * Quaternion.AngleAxis(pointerTiltAngle, Vector3.right);
  }

#endif  // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)
}
