// 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 UnityEngine.EventSystems;
using Gvr.Internal;
using System;
using System.Collections;
using System.Collections.Generic;

// Events to update the keyboard.
// These values depend on C API keyboard values
public enum GvrKeyboardEvent {
  /// Unknown error.
  GVR_KEYBOARD_ERROR_UNKNOWN = 0,
  /// The keyboard service could not be connected. This is usually due to the
  /// keyboard service not being installed.
  GVR_KEYBOARD_ERROR_SERVICE_NOT_CONNECTED = 1,
  /// No locale was found in the keyboard service.
  GVR_KEYBOARD_ERROR_NO_LOCALES_FOUND = 2,
  /// The keyboard SDK tried to load dynamically but failed. This is usually due
  /// to the keyboard service not being installed or being out of date.
  GVR_KEYBOARD_ERROR_SDK_LOAD_FAILED = 3,
  /// Keyboard becomes visible.
  GVR_KEYBOARD_SHOWN = 4,
  /// Keyboard becomes hidden.
  GVR_KEYBOARD_HIDDEN = 5,
  /// Text has been updated.
  GVR_KEYBOARD_TEXT_UPDATED = 6,
  /// Text has been committed.
  GVR_KEYBOARD_TEXT_COMMITTED = 7,
};

// These values depend on C API keyboard values.
public enum GvrKeyboardError {
  UNKNOWN = 0,
  SERVICE_NOT_CONNECTED = 1,
  NO_LOCALES_FOUND = 2,
  SDK_LOAD_FAILED = 3
};

// These values depend on C API keyboard values.
public enum GvrKeyboardInputMode {
  DEFAULT = 0,
  NUMERIC = 1
};

// Handles keyboard state management such as hiding and displaying
// the keyboard, directly modifying text and stereoscopic rendering.
[HelpURL("https://developers.google.com/vr/unity/reference/class/GvrKeyboard")]
public class GvrKeyboard : MonoBehaviour {

  private static GvrKeyboard instance;
  private static IKeyboardProvider keyboardProvider;
  private KeyboardState keyboardState = new KeyboardState();
  private IEnumerator keyboardUpdate;

  // Keyboard delegate types.
  public delegate void StandardCallback();
  public delegate void EditTextCallback(string edit_text);
  public delegate void ErrorCallback(GvrKeyboardError err);
  public delegate void KeyboardCallback(IntPtr closure, GvrKeyboardEvent evt);

  // Private data and callbacks.
  private ErrorCallback errorCallback = null;
  private StandardCallback showCallback = null;
  private StandardCallback hideCallback = null;
  private EditTextCallback updateCallback = null;
  private EditTextCallback enterCallback = null;

#if UNITY_ANDROID
  // Which eye is currently being rendered.
  private bool isRight = false;
#endif // UNITY_ANDROID

  private bool isKeyboardHidden = false;
  private const float kExecuterWait = 0.01f;
  private static List<GvrKeyboardEvent> threadSafeCallbacks =
    new List<GvrKeyboardEvent>();
  private static System.Object callbacksLock = new System.Object();

  // Public parameters.
  public GvrKeyboardDelegateBase keyboardDelegate = null;
  public GvrKeyboardInputMode inputMode = GvrKeyboardInputMode.DEFAULT;
  public bool useRecommended = true;
  public float distance = 0;

  public string EditorText {
    get { return instance != null ? instance.keyboardState.editorText : string.Empty; }
    set { keyboardProvider.EditorText = value; }
  }

  public GvrKeyboardInputMode Mode {
    get { return instance != null ? instance.keyboardState.mode : GvrKeyboardInputMode.DEFAULT; }
  }

  public bool IsValid {
    get { return instance != null ? instance.keyboardState.isValid : false; }
  }

  public bool IsReady {
    get { return instance != null ? instance.keyboardState.isReady : false; }
  }

  public Matrix4x4 WorldMatrix {
    get { return instance != null ? instance.keyboardState.worldMatrix : Matrix4x4.zero; }
  }

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

  void OnDestroy() {
    instance = null;
    threadSafeCallbacks.Clear();
  }

  // Use this for initialization.
  void Start() {
    if (keyboardDelegate != null) {
      errorCallback  = keyboardDelegate.OnKeyboardError;
      showCallback   = keyboardDelegate.OnKeyboardShow;
      hideCallback   = keyboardDelegate.OnKeyboardHide;
      updateCallback = keyboardDelegate.OnKeyboardUpdate;
      enterCallback  = keyboardDelegate.OnKeyboardEnterPressed;
      keyboardDelegate.KeyboardHidden += KeyboardDelegate_KeyboardHidden;
      keyboardDelegate.KeyboardShown += KeyboardDelegate_KeyboardShown;
    }
    keyboardProvider.ReadState(keyboardState);

    if (IsValid) {
      if (keyboardProvider.Create(OnKeyboardCallback)) {
        keyboardProvider.SetInputMode(inputMode);
      }
    } else {
      Debug.LogError("Could not validate keyboard");
    }
  }

  // Update per-frame data.
  void Update() {
    if (keyboardProvider == null) {
      return;
    }
    keyboardProvider.ReadState(keyboardState);
    if (IsReady) {
      // Reset position of keyboard.
      if (transform.hasChanged) {
        Show();
        transform.hasChanged = false;
      }

      keyboardProvider.UpdateData();
    }
  }

  // Use this function for procedural rendering
  // Gets called twice per frame, once for each eye.
  // On each frame, left eye renders before right eye so
  // we keep track of a boolean that toggles back and forth
  // between each eye.
  void OnRenderObject() {
    if (keyboardProvider == null || !IsReady) {
      return;
    }
#if UNITY_ANDROID
    Camera camera = Camera.current;
    if (camera && camera == Camera.main) {
      // Get current eye.
      Camera.StereoscopicEye camEye = isRight ? Camera.StereoscopicEye.Right : Camera.StereoscopicEye.Left;

      // Camera matrices.
      Matrix4x4 proj = camera.GetStereoProjectionMatrix(camEye);
      Matrix4x4 modelView = camera.GetStereoViewMatrix(camEye);

      // Camera viewport.
      Rect viewport = camera.pixelRect;

      // Render keyboard.
      keyboardProvider.Render((int) camEye, modelView, proj, viewport);

      // Swap.
      isRight = !isRight;
    }
#endif  // !UNITY_ANDROID
  }

  // Resets keyboard text.
  public void ClearText() {
    if (keyboardProvider != null) {
      keyboardProvider.EditorText = string.Empty;
    }
  }

  public void Show() {
    if (keyboardProvider == null) {
      return;
    }

    // Get user matrix.
    Quaternion fixRot = new Quaternion(transform.rotation.x * -1, transform.rotation.y * -1,
      transform.rotation.z, transform.rotation.w);
    // Need to convert from left handed to right handed for the Keyboard coordinates.
    Vector3 fixPos = new Vector3(transform.position.x, transform.position.y,
      transform.position.z * -1);
    Matrix4x4 modelMatrix = Matrix4x4.TRS(fixPos, fixRot, Vector3.one);
    Matrix4x4 mat = Matrix4x4.identity;
    Vector3 position = gameObject.transform.position;
    if (position.x == 0 && position.y == 0 && position.z == 0 && !useRecommended) {
      // Force use recommended to be true, otherwise keyboard won't show up.
      keyboardProvider.Show(mat, true, distance, modelMatrix);
      return;
    }

    // Matrix needs to be set only if we're not using the recommended one.
    // Uses the values of the keyboard gameobject transform as reported by Unity. If this is
    // the zero vector, parent it under another gameobject instead.
    if (!useRecommended) {
      mat = GetKeyboardObjectMatrix(position);
    }

    keyboardProvider.Show(mat, useRecommended, distance, modelMatrix);
  }

  public void Hide() {
    if (keyboardProvider != null) {
      keyboardProvider.Hide();
    }
  }

  public void OnPointerClick(BaseEventData data) {
    if (isKeyboardHidden) {
      Show();
    }
  }

  void OnEnable() {
    keyboardUpdate = Executer();
    StartCoroutine(keyboardUpdate);
  }

  void OnDisable() {
    StopCoroutine(keyboardUpdate);
  }

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

  IEnumerator Executer() {
    while (true) {
      yield return new WaitForSeconds(kExecuterWait);

      while (threadSafeCallbacks.Count > 0) {
        GvrKeyboardEvent keyboardEvent = threadSafeCallbacks[0];
        PoolKeyboardCallbacks(keyboardEvent);
        lock (callbacksLock) {
          threadSafeCallbacks.RemoveAt(0);
        }
      }
    }
  }

  private void PoolKeyboardCallbacks(GvrKeyboardEvent keyboardEvent) {
    switch (keyboardEvent) {
      case GvrKeyboardEvent.GVR_KEYBOARD_ERROR_UNKNOWN:
        errorCallback(GvrKeyboardError.UNKNOWN);
        break;
      case GvrKeyboardEvent.GVR_KEYBOARD_ERROR_SERVICE_NOT_CONNECTED:
        errorCallback(GvrKeyboardError.SERVICE_NOT_CONNECTED);
        break;
      case GvrKeyboardEvent.GVR_KEYBOARD_ERROR_NO_LOCALES_FOUND:
        errorCallback(GvrKeyboardError.NO_LOCALES_FOUND);
        break;
      case GvrKeyboardEvent.GVR_KEYBOARD_ERROR_SDK_LOAD_FAILED:
        errorCallback(GvrKeyboardError.SDK_LOAD_FAILED);
        break;
      case GvrKeyboardEvent.GVR_KEYBOARD_SHOWN:
        showCallback();
        break;
      case GvrKeyboardEvent.GVR_KEYBOARD_HIDDEN:
        hideCallback();
        break;
      case GvrKeyboardEvent.GVR_KEYBOARD_TEXT_UPDATED:
        updateCallback(keyboardProvider.EditorText);
        break;
      case GvrKeyboardEvent.GVR_KEYBOARD_TEXT_COMMITTED:
        enterCallback(keyboardProvider.EditorText);
        break;
    }
  }

  [AOT.MonoPInvokeCallback(typeof(GvrKeyboardEvent))]
  private static void OnKeyboardCallback(IntPtr closure, GvrKeyboardEvent keyboardEvent) {
    lock (callbacksLock) {
      threadSafeCallbacks.Add(keyboardEvent);
    }
  }

  private void KeyboardDelegate_KeyboardShown(object sender, System.EventArgs e) {
    isKeyboardHidden = false;
  }

  private void KeyboardDelegate_KeyboardHidden(object sender, System.EventArgs e) {
    isKeyboardHidden = true;
  }

  // Returns a matrix populated by the keyboard's gameobject position. If the position is not
  // zero, but comes back as zero, parent this under another gameobject instead.
  private Matrix4x4 GetKeyboardObjectMatrix(Vector3 position) {
    // Set keyboard position based on this gameObject's position.
    float angleX = Mathf.Atan2(position.y, position.x);
    float kTanAngleX = Mathf.Tan(angleX);
    float newPosX = kTanAngleX * position.x;

    float angleY = Mathf.Atan2(position.x, position.y);
    float kTanAngleY = Mathf.Tan(angleY);
    float newPosY = kTanAngleY * position.y;

    float angleZ = Mathf.Atan2(position.y, position.z);
    float kTanAngleZ = Mathf.Tan(angleZ);
    float newPosZ = kTanAngleZ * position.z;

    Vector3 keyboardPosition = new Vector3(newPosX, newPosY, newPosZ);
    Vector3 lookPosition = Camera.main.transform.position;

    Quaternion rotation = Quaternion.LookRotation(lookPosition);
    Matrix4x4 mat = new Matrix4x4();
    mat.SetTRS(keyboardPosition, rotation, position);

    // Set diagonal to identity if any of them are zero.
    if (mat[0, 0] == 0) {
      Vector4 row0 = mat.GetRow(0);
      mat.SetRow(0, new Vector4(1, row0.y, row0.z, row0.w));
    }
    if (mat[1, 1] == 0) {
      Vector4 row1 = mat.GetRow(1);
      mat.SetRow(1, new Vector4(row1.x, 1, row1.z, row1.w));
    }
    if (mat[2, 2] == 0) {
      Vector4 row2 = mat.GetRow(2);
      mat.SetRow(2, new Vector4(row2.x, row2.y, 1, row2.w));
    }
    return mat;
  }
}