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

#if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)
using UnityEngine.VR;
using System;
using System.Collections;

using Gvr.Internal;

/// Represents the controller's current connection state.
/// All values and semantics below (except for Error) are
/// from gvr_types.h in the GVR C API.
public enum GvrConnectionState {
  /// Indicates that an error has occurred.
  Error = -1,

  /// Indicates that the controller is disconnected.
  Disconnected = 0,
  /// Indicates that the device is scanning for controllers.
  Scanning = 1,
  /// Indicates that the device is connecting to a controller.
  Connecting = 2,
  /// Indicates that the device is connected to a controller.
  Connected = 3,
};

/// Represents the API status of the current controller state.
/// Values and semantics from gvr_types.h in the GVR C API.
public enum GvrControllerApiStatus {
  /// A Unity-localized error occurred.
  /// This is the only value that isn't in gvr_types.h.
  Error = -1,

  /// API is happy and healthy. This doesn't mean the controller itself
  /// is connected, it just means that the underlying service is working
  /// properly.
  Ok = 0,

  /// Any other status represents a permanent failure that requires
  /// external action to fix:

  /// API failed because this device does not support controllers (API is too
  /// low, or other required feature not present).
  Unsupported = 1,
  /// This app was not authorized to use the service (e.g., missing permissions,
  /// the app is blacklisted by the underlying service, etc).
  NotAuthorized = 2,
  /// The underlying VR service is not present.
  Unavailable = 3,
  /// The underlying VR service is too old, needs upgrade.
  ApiServiceObsolete = 4,
  /// The underlying VR service is too new, is incompatible with current client.
  ApiClientObsolete = 5,
  /// The underlying VR service is malfunctioning. Try again later.
  ApiMalfunction = 6,
};

/// Represents the controller's current battery level.
/// Values and semantics from gvr_types.h in the GVR C API.
public enum GvrControllerBatteryLevel {
  /// A Unity-localized error occurred.
  /// This is the only value that isn't in gvr_types.h.
  Error = -1,

  /// The battery state is currently unreported
  Unknown = 0,

  /// Equivalent to 1 out of 5 bars on the battery indicator
  CriticalLow = 1,

  /// Equivalent to 2 out of 5 bars on the battery indicator
  Low = 2,

  /// Equivalent to 3 out of 5 bars on the battery indicator
  Medium = 3,

  /// Equivalent to 4 out of 5 bars on the battery indicator
  AlmostFull = 4,

  /// Equivalent to 5 out of 5 bars on the battery indicator
  Full = 5,
};
#endif  // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)


/// Main entry point for the Daydream controller API.
///
/// To use this API, add this behavior to a GameObject in your scene, or use the
/// GvrControllerMain prefab. There can only be one object with this behavior on your scene.
///
/// This is a singleton object.
///
/// To access the controller state, simply read the static properties of this class. For example,
/// to know the controller's current orientation, use GvrController.Orientation.
public class GvrController : MonoBehaviour {
#if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)
  private static GvrController instance;
  private static IControllerProvider controllerProvider;

  private ControllerState controllerState = new ControllerState();
  private IEnumerator controllerUpdate;
  private WaitForEndOfFrame waitForEndOfFrame = new WaitForEndOfFrame();

  /// Event handler for receiving button, track pad, and IMU updates from the controller.
  public delegate void OnControllerUpdateEvent();
  public event OnControllerUpdateEvent OnControllerUpdate;

  public enum EmulatorConnectionMode {
    OFF,
    USB,
    WIFI,
  }
  /// Indicates how we connect to the controller emulator.
  [Tooltip("How to connect to the emulator: USB cable (recommended) or WIFI.")]
  public EmulatorConnectionMode emulatorConnectionMode = EmulatorConnectionMode.USB;

  /// Returns the arm model instance associated with the controller.
  public static GvrArmModel ArmModel {
    get {
      return instance != null ? instance.GetComponent<GvrArmModel>() : null;
    }
  }

  /// Returns the controller's current connection state.
  public static GvrConnectionState State {
    get {
      return instance != null ? instance.controllerState.connectionState : GvrConnectionState.Error;
    }
  }

  /// Returns the API status of the current controller state.
  public static GvrControllerApiStatus ApiStatus {
    get {
      return instance != null ? instance.controllerState.apiStatus : GvrControllerApiStatus.Error;
    }
  }

  /// Returns the controller's current orientation in space, as a quaternion.
  /// The space in which the orientation is represented is the usual Unity space, with
  /// X pointing to the right, Y pointing up and Z pointing forward. Therefore, to make an
  /// object in your scene have the same orientation as the controller, simply assign this
  /// quaternion to the GameObject's transform.rotation.
  public static Quaternion Orientation {
    get {
      return instance != null ? instance.controllerState.orientation : Quaternion.identity;
    }
  }

  /// Returns the controller's gyroscope reading. The gyroscope indicates the angular
  /// about each of its local axes. The controller's axes are: X points to the right,
  /// Y points perpendicularly up from the controller's top surface and Z lies
  /// along the controller's body, pointing towards the front. The angular speed is given
  /// in radians per second, using the right-hand rule (positive means a right-hand rotation
  /// about the given axis).
  public static Vector3 Gyro {
    get {
      return instance != null ? instance.controllerState.gyro : Vector3.zero;
    }
  }

  /// Returns the controller's accelerometer reading. The accelerometer indicates the
  /// effect of acceleration and gravity in the direction of each of the controller's local
  /// axes. The controller's local axes are: X points to the right, Y points perpendicularly
  /// up from the controller's top surface and Z lies along the controller's body, pointing
  /// towards the front. The acceleration is measured in meters per second squared. Note that
  /// gravity is combined with acceleration, so when the controller is resting on a table top,
  /// it will measure an acceleration of 9.8 m/s^2 on the Y axis. The accelerometer reading
  /// will be zero on all three axes only if the controller is in free fall, or if the user
  /// is in a zero gravity environment like a space station.
  public static Vector3 Accel {
    get {
      return instance != null ? instance.controllerState.accel : Vector3.zero;
    }
  }

  /// If true, the user is currently touching the controller's touchpad.
  public static bool IsTouching {
    get {
      return instance != null ? instance.controllerState.isTouching : false;
    }
  }

  /// If true, the user just started touching the touchpad. This is an event flag (it is true
  /// for only one frame after the event happens, then reverts to false).
  public static bool TouchDown {
    get {
      return instance != null ? instance.controllerState.touchDown : false;
    }
  }

  /// If true, the user just stopped touching the touchpad. This is an event flag (it is true
  /// for only one frame after the event happens, then reverts to false).
  public static bool TouchUp {
    get {
      return instance != null ? instance.controllerState.touchUp : false;
    }
  }

  public static Vector2 TouchPos {
    get {
      return instance != null ? instance.controllerState.touchPos : Vector2.zero;
    }
  }

  /// If true, the user is currently performing the recentering gesture. Most apps will want
  /// to pause the interaction while this remains true.
  public static bool Recentering {
    get {
      return instance != null ? instance.controllerState.recentering : false;
    }
  }

  /// If true, the user just completed the recenter gesture. The controller's orientation is
  /// now being reported in the new recentered coordinate system (the controller's orientation
  /// when recentering was completed was remapped to mean "forward"). This is an event flag
  /// (it is true for only one frame after the event happens, then reverts to false).
  /// The headset is recentered together with the controller.
  public static bool Recentered {
    get {
      return instance != null ? instance.controllerState.recentered : false;
    }
  }

  /// If true, the click button (touchpad button) is currently being pressed. This is not
  /// an event: it represents the button's state (it remains true while the button is being
  /// pressed).
  public static bool ClickButton {
    get {
      return instance != null ? instance.controllerState.clickButtonState : false;
    }
  }

  /// If true, the click button (touchpad button) was just pressed. This is an event flag:
  /// it will be true for only one frame after the event happens.
  public static bool ClickButtonDown {
    get {
      return instance != null ? instance.controllerState.clickButtonDown : false;
    }
  }

  /// If true, the click button (touchpad button) was just released. This is an event flag:
  /// it will be true for only one frame after the event happens.
  public static bool ClickButtonUp {
    get {
      return instance != null ? instance.controllerState.clickButtonUp : false;
    }
  }

  /// If true, the app button (touchpad button) is currently being pressed. This is not
  /// an event: it represents the button's state (it remains true while the button is being
  /// pressed).
  public static bool AppButton {
    get {
      return instance != null ? instance.controllerState.appButtonState : false;
    }
  }

  /// If true, the app button was just pressed. This is an event flag: it will be true for
  /// only one frame after the event happens.
  public static bool AppButtonDown {
    get {
      return instance != null ? instance.controllerState.appButtonDown : false;
    }
  }

  /// If true, the app button was just released. This is an event flag: it will be true for
  /// only one frame after the event happens.
  public static bool AppButtonUp {
    get {
      return instance != null ? instance.controllerState.appButtonUp : false;
    }
  }

  // Always false in the emulator.
  public static bool HomeButtonDown {
    get {
      return instance != null ? instance.controllerState.homeButtonDown : false;
    }
  }

  // Always false in the emulator.
  public static bool HomeButtonState {
    get {
      return instance != null ? instance.controllerState.homeButtonState : false;
    }
  }


  /// If State == GvrConnectionState.Error, this contains details about the error.
  public static string ErrorDetails {
    get {
      if (instance != null) {
        return instance.controllerState.connectionState == GvrConnectionState.Error ?
            instance.controllerState.errorDetails : "";
      } else {
        return "GvrController instance not found in scene. It may be missing, or it might "
            + "not have initialized yet.";
      }
    }
  }

  // Returns the GVR C library controller state pointer (gvr_controller_state*).
  public static IntPtr StatePtr {
    get {
      return instance != null? instance.controllerState.gvrPtr : IntPtr.Zero;
    }
  }

  /// If true, the user is currently touching the controller's touchpad.
  public static bool IsCharging {
    get {
      return instance != null ? instance.controllerState.isCharging : false;
    }
  }

  /// If true, the user is currently touching the controller's touchpad.
  public static GvrControllerBatteryLevel BatteryLevel {
    get {
      return instance != null ? instance.controllerState.batteryLevel : GvrControllerBatteryLevel.Error;
    }
  }

  void Awake() {
    if (instance != null) {
      Debug.LogError("More than one GvrController instance was found in your scene. "
          + "Ensure that there is only one GvrController.");
      this.enabled = false;
      return;
    }
    instance = this;
    if (controllerProvider == null) {
      controllerProvider = ControllerProviderFactory.CreateControllerProvider(this);
    }

    // Keep screen on here, since GvrController must be in any GVR scene in order to enable
    // controller capabilities.
    Screen.sleepTimeout = SleepTimeout.NeverSleep;
  }

  void OnDestroy() {
    instance = null;
  }

  private void UpdateController() {
    controllerProvider.ReadState(controllerState);

#if UNITY_EDITOR
    // If a headset recenter was requested, do it now.
    if (controllerState.recentered) {
      for (int i = 0; i < Camera.allCameras.Length; i++) {
        Camera cam = Camera.allCameras[i];
        // Do not reset pitch, which is how it works on the device.
        cam.transform.localRotation = Quaternion.Euler(cam.transform.localRotation.eulerAngles.x, 0, 0);
      }
    }
#endif  // UNITY_EDITOR
  }

  void OnApplicationPause(bool paused) {
    if (null == controllerProvider) return;
    if (paused) {
      controllerProvider.OnPause();
    } else {
      controllerProvider.OnResume();
    }
  }

  void OnEnable() {
    controllerUpdate = EndOfFrame();
    StartCoroutine(controllerUpdate);
  }

  void OnDisable() {
    StopCoroutine(controllerUpdate);
  }

  IEnumerator EndOfFrame() {
    while (true) {
      // This must be done at the end of the frame to ensure that all GameObjects had a chance
      // to read transient controller state (e.g. events, etc) for the current frame before
      // it gets reset.
      yield return waitForEndOfFrame;
      UpdateController();
      if (OnControllerUpdate != null) {
        OnControllerUpdate();
      }
    }
  }
#endif  // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)
}

