/******************************************************************************
 * Copyright (C) Leap Motion, Inc. 2011-2017.                                 *
 * Leap Motion proprietary and  confidential.                                 *
 *                                                                            *
 * Use subject to the terms of the Leap Motion SDK Agreement available at     *
 * https://developer.leapmotion.com/sdk_agreement, or another agreement       *
 * between Leap Motion and you, your company or other organization.           *
 ******************************************************************************/

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Leap.Unity.Query;
using Leap.Unity.RuntimeGizmos;
using Leap.Unity.Interaction.Internal;

namespace Leap.Unity.Interaction {

  [CustomEditor(typeof(InteractionManager))]
  public class InteractionManagerEditor : CustomEditorBase<InteractionManager> {

    protected override void OnEnable() {
      base.OnEnable();

      // Interaction Controllers
      specifyCustomDrawer("_interactionControllers", drawControllersStatusEditor);

      // Layers
      SerializedProperty autoGenerateLayerProperty = serializedObject.FindProperty("_autoGenerateLayers");
      specifyConditionalDrawing(() => autoGenerateLayerProperty.boolValue,
                                "_templateLayer");
      specifyConditionalDrawing(() => !autoGenerateLayerProperty.boolValue,
                                "_interactionLayer",
                                "_interactionNoContactLayer",
                                "_contactBoneLayer");
      specifyCustomDecorator("_interactionLayer", drawInteractionLayerDecorator);

      specifyCustomDecorator("_drawControllerRuntimeGizmos", drawControllerRuntimeGizmoDecorator);
      specifyCustomPostDecorator("_drawControllerRuntimeGizmos", drawPostControllerRuntimeGizmoDecorator);
    }

    public override void OnInspectorGUI() {
      checkForRigidHands();

      base.OnInspectorGUI();
    }

    private void checkForRigidHands() {
      var rigidHandObjects = FindObjectsOfType<RigidHand>().Query().Select(x => x.gameObject).ToArray();
      if (rigidHandObjects.Length != 0 && InteractionPreferences.shouldCheckForRigidHands) {
        EditorGUILayout.BeginHorizontal();

        EditorGUILayout.HelpBox("Rigid Hands are present in your scene. Rigid Hands are "
                              + "not compatible with the Interaction Engine and should "
                              + "never be used in tandem with it. You should remove them "
                              + "from your scene.", MessageType.Error);

        EditorGUILayout.BeginVertical();
        if (GUILayout.Button(new GUIContent("Ignore", "Don't show this warning anymore."), GUILayout.ExpandHeight(true), GUILayout.MaxHeight(40F))) {
          if (EditorUtility.DisplayDialog("Really ignore this warning?",
                                          "Your interactions will not work correctly if "
                                        + "Rigid Hands are enabled at the same time as "
                                        + "Interaction Hands.",
                                          "Yes, ignore this warning",
                                          "Cancel")) {
            InteractionPreferences.shouldCheckForRigidHands = false;
          }
        }

        if (GUILayout.Button(new GUIContent("Select Rigid Hands", "Select RigidHand objects in the current scene."), GUILayout.ExpandHeight(true), GUILayout.MaxHeight(40F))) {
          Selection.objects = rigidHandObjects;
        }
        EditorGUILayout.EndVertical();

        EditorGUILayout.EndHorizontal();
      }
    }

    public override bool RequiresConstantRepaint() {
      return Application.isPlaying;
    }

    private void drawInteractionLayerDecorator(SerializedProperty property) {
      if (!Physics.GetIgnoreLayerCollision(target.interactionNoContactLayer.layerIndex,
                                           target.contactBoneLayer.layerIndex)) {
        EditorGUILayout.HelpBox("The No Contact layer should NOT collide with the Contact "
                              + "Bone layer. (Check your layer collision settings in Edit "
                              + "/Project Settings/Physics.)", MessageType.Error);
      }

      if (Physics.GetIgnoreLayerCollision(target.interactionLayer.layerIndex,
                                          target.contactBoneLayer.layerIndex)) {
        EditorGUILayout.HelpBox("The Interaction layer should collide with the Contact "
                              + "Bone layer. (Check your layer collision settings in Edit "
                              + "/Project Settings/Physics.)", MessageType.Error);
      }
    }

    private RuntimeGizmoManager _runtimeGizmoManager;

    private void drawControllerRuntimeGizmoDecorator(SerializedProperty property) {
      if (property.boolValue && _runtimeGizmoManager == null) {
        _runtimeGizmoManager = FindObjectOfType<RuntimeGizmoManager>();

        if (_runtimeGizmoManager == null) {
          EditorGUILayout.Space();
          EditorGUILayout.Space();
          EditorGUILayout.HelpBox("Draw Controller Runtime Gizmos is checked, but there "
                                + "is no RuntimeGizmoManager in your scene, or it is "
                                + "disabled.", MessageType.Warning);
        }
      }
    }

    private void drawPostControllerRuntimeGizmoDecorator(SerializedProperty property) {
      if (property.boolValue && _runtimeGizmoManager != null) {
        drawControllerRuntimeGizmosColorLegend();
      }
    }

    public void drawControllerRuntimeGizmosColorLegend() {
      EditorGUILayout.Space();
      EditorGUILayout.LabelField("Controller Gizmos Legend", EditorStyles.boldLabel);

      EditorGUI.BeginDisabledGroup(true);

      EditorGUILayout.ColorField(new GUIContent("Contact Bone Colliders",
                                                "The gizmo color for contact bone colliders "
                                              + "when soft contact is disabled."),
                                InteractionController.GizmoColors.ContactBone);
      EditorGUILayout.ColorField(new GUIContent("Soft Contact Bone Colliders",
                                                "The gizmo color for contact bones colliders "
                                              + "when soft contact is enabled."),
                                 InteractionController.GizmoColors.SoftContactBone);

      EditorGUILayout.Space();
      EditorGUILayout.ColorField(new GUIContent("Hover Points",
                                                "The gizmo color for hover points. Gizmo "
                                              + "does not reflect the actual hover radius."),
                                 InteractionController.GizmoColors.HoverPoint);
      EditorGUILayout.ColorField(new GUIContent("Primary Hover Points",
                                                "The gizmo color for primary hover points."),
                                 InteractionController.GizmoColors.PrimaryHoverPoint);

      EditorGUILayout.Space();
      EditorGUILayout.ColorField(new GUIContent("Grasp Points",
                                                "The gizmo color for grasp points. "
                                              + "InteractionHands do not use grasp points, "
                                              + "so no gizmo is drawn for them."),
                                 InteractionController.GizmoColors.GraspPoint);
      EditorGUILayout.ColorField(new GUIContent("Graspable Objects",
                                                "The gizmo color for the wire sphere "
                                              + "that appears at objects when they are "
                                              + "graspable by an interaction controller. "),
                                 InteractionController.GizmoColors.Graspable);

      EditorGUI.EndDisabledGroup();
    }

    private void drawControllersStatusEditor(SerializedProperty property) {
      EditorGUILayout.Space();
      EditorGUILayout.LabelField("Interaction Controller Status", EditorStyles.boldLabel);

      if (target.interactionControllers.Count == 0) {
        EditorGUILayout.HelpBox("This Interaction Manager has no interaction controllers "
                              + "assigned to it. Please add at least one InteractionHand "
                              + "or an InteractionVRController as a child of this object.",
                              MessageType.Warning);

        return;
      }

      EditorGUILayout.BeginVertical();

      _leftHand = null;
      _rightHand = null;
      _leftVRNodeController = null;
      _rightVRNodeController = null;
      foreach (var controller in target.interactionControllers) {
        EditorGUILayout.BeginHorizontal();

        drawControllerStatusEditor(controller);

        EditorGUILayout.EndHorizontal();
      }

      EditorGUILayout.EndVertical();
    }

    public static class Colors {
      public static Color DarkGray  { get { return new Color(0.4F, 0.4F, 0.4F); } }
      public static Color LightGray { get { return new Color(0.7F, 0.7F, 0.7F); } }

      public static Color Good      { get { return Color.Lerp(Color.green, LightGray, 0.2F); } }
      public static Color Caution   { get { return Color.Lerp(Good, Color.yellow, 0.8F); } }
      public static Color Warning   { get { return Color.Lerp(Color.yellow, Problem, 0.5F); } }
      public static Color Problem   { get { return Color.Lerp(Color.red, Color.yellow, 0.3F); } }
    }

    private struct ControllerStatusMessage {
      public string message;
      public string tooltip;
      public Color color;
    }
    private List<ControllerStatusMessage> statusMessagesBuffer = new List<ControllerStatusMessage>();

    private void drawControllerStatusEditor(InteractionController controller) {
      // Controller object
      EditorGUI.BeginDisabledGroup(true);
      EditorGUILayout.ObjectField(controller, typeof(InteractionController), true);
      EditorGUI.EndDisabledGroup();

      // Status
      var messages = statusMessagesBuffer;
      messages.Clear();

      // Check various states and add messages to the messages buffer.
      checkTrackingStatus(controller, messages);

      if (controller.intHand != null) {
        checkInteractionHandStatus(controller.intHand, messages);
      }
      else if (controller is InteractionVRController) {
        checkInteractionVRControllerStatus(controller as InteractionVRController, messages);
      }

      // Render the status messages.
      Rect statusMessagesRect = EditorGUILayout.BeginVertical(GUILayout.MinHeight(20));

      EditorGUI.DrawRect(statusMessagesRect, Colors.DarkGray);
      statusMessagesRect = statusMessagesRect.ShrinkOne();
      EditorGUI.DrawRect(statusMessagesRect, Colors.LightGray);
      statusMessagesRect = statusMessagesRect.ShrinkOne();
      EditorGUI.DrawRect(statusMessagesRect, Colors.DarkGray);

      if (messages.Count == 0) {
        messages.Add(new ControllerStatusMessage() {
          message = "No Status Messages",
          tooltip = "",
          color = Colors.Good
        });
      }

      foreach (var statusMessage in messages) {
        var messageColorStyle = new GUIStyle(EditorStyles.label);
        messageColorStyle.normal.textColor = statusMessage.color;

        EditorGUILayout.LabelField(new GUIContent("[" + statusMessage.message + "]",
                                                  statusMessage.tooltip),
                                   messageColorStyle);
        GUILayout.Space(1);
      }
      GUILayout.Space(1);

      EditorGUILayout.EndVertical();
    }

    private void checkTrackingStatus(InteractionController controller,
                                     List<ControllerStatusMessage> messages) {
      if (Application.isPlaying) {
        if (controller.isTracked) {
          if (controller.isBeingMoved) {
            messages.Add(new ControllerStatusMessage() {
              message = "Tracked",
              tooltip = "This interaction controller is currently being tracked.",
              color = Colors.Good
            });
          } else {
            messages.Add(new ControllerStatusMessage() {
              message = "Not Moving",
              tooltip = "This interaction controller is currently not being moved.",
              color = Colors.Caution
            });
          }
        } else {
          messages.Add(new ControllerStatusMessage() {
            message = "Untracked",
            tooltip = "This interaction controller is not currently being tracked.",
            color = Colors.Warning
          });
        }
      }
    }

    private LeapProvider _provider = null;
    private InteractionHand _leftHand = null;
    private InteractionHand _rightHand = null;

    private void checkInteractionHandStatus(InteractionHand intHand,
                                            List<ControllerStatusMessage> messages) {
      if (!Application.isPlaying) {
        // Check for valid InteractionHand data state.
        if (intHand.handDataMode == HandDataMode.Custom) {
          messages.Add(new ControllerStatusMessage() {
            message = "HandDataMode: Custom",
            tooltip = "This interaction hand has its data mode set to Custom. "
                    + "A custom script will be required to ensure hand data gets to "
                    + "the interaction hand properly. Upon pressing play, an error will "
                    + "be raised by the hand itself if it is misconfigured.",
            color = Colors.Caution
          });
        }
        else {
          // Check for a LeapProvider in the scene somewhere.
          if (_provider == null) {
            _provider = FindObjectOfType<LeapProvider>();
          }
          if (_provider == null) {
            messages.Add(new ControllerStatusMessage() {
              message = "No LeapProvider",
              tooltip = "No LeapProvider object was found in your scene. "
                      + "InteractionHands require a LeapProvider to function; consider "
                      + "dragging in the LeapHeadMountedRig prefab or creating and "
                      + "configuring a LeapServiceProvider.",
              color = Colors.Warning
            });
          }
        }
      }

      // Check if the player has multiple left hands or multiple right hands.
      if (intHand.handDataMode == HandDataMode.PlayerLeft && _leftHand != null
       || intHand.handDataMode == HandDataMode.PlayerRight && _rightHand != null) {
        messages.Add(new ControllerStatusMessage() {
          message = "Duplicate Hand",
          tooltip = "You already have a hand with this data mode in your scene. "
                  + "You should remove one of the duplicates.",
          color = Colors.Problem
        });
      }
      if (_leftHand == null && intHand.handDataMode == HandDataMode.PlayerLeft) {
        _leftHand = intHand;
      }
      else if (_rightHand == null && intHand.handDataMode == HandDataMode.PlayerRight) {
        _rightHand = intHand;
      }
    }

    private InteractionVRController _leftVRNodeController;
    private InteractionVRController _rightVRNodeController;

    private void checkInteractionVRControllerStatus(InteractionVRController controller,
                                                    List<ControllerStatusMessage> messages) {
      // Check if the controller is configured correctly if it is set up with a custom
      // tracking provider.
      if (controller.isUsingCustomTracking) {
        messages.Add(new ControllerStatusMessage() {
          message = "Custom Tracking Provider",
          tooltip = "You are using a custom tracking provider for this VR controller.",
          color = Colors.Caution
        });
      }

      // Check if the player has duplicate VRNode left controllers or right controllers.
      bool isLeftVRNodeController  = controller.trackingProvider is DefaultVRNodeTrackingProvider
                                  && controller.chirality == Chirality.Left;
      bool isRightVRNodeController = controller.trackingProvider is DefaultVRNodeTrackingProvider
                                  && controller.chirality == Chirality.Right;

      if (isLeftVRNodeController && _leftVRNodeController != null
             || isRightVRNodeController && _rightVRNodeController != null) {

        var alreadyExistsController = isLeftVRNodeController ? _leftVRNodeController : _rightVRNodeController;

        string message;
        string tooltip;
        Color color;

        if (controller.deviceJoystickTokens.Equals(alreadyExistsController.deviceJoystickTokens)) {
          message = "Duplicate VR Controller";
          tooltip = "You already have a VRNode controller with this chirality and device "
                  + "string in your scene. You should remove one of the duplicates.";
          color = Colors.Problem;
        } else {
          message = "Multiple VR Controllers";
          tooltip = "You have multiple VR controllers of the same chirality enabled with "
                  + "different device strings. If both device strings match attached "
                  + "Unity joysticks, you may get duplicate controllers.";
          color = Colors.Caution;
        }

        messages.Add(new ControllerStatusMessage() {
          message = message,
          tooltip = tooltip,
          color = color
        });
      }

      if (isLeftVRNodeController) {
        _leftVRNodeController = controller;
      }
      if (isRightVRNodeController) {
        _rightVRNodeController = controller;
      }

      string wrongChiralityToken = controller.isLeft ? "right" : "left";
      if (controller.deviceJoystickTokens.Contains(wrongChiralityToken)) {
        messages.Add(new ControllerStatusMessage() {
          message = "Wrong Chirality?",
          tooltip = "This VR controller's device joystick string specifies a chirality "
                  + "that is different from the chirality of the controller itself. You "
                  + "should confirm this controller's device string or chirality setting.",
          color = Colors.Warning
        });
      }

    }

  }

  public static class Extensions {

    public static Rect ShrinkOne(this Rect rect) {
      rect.x += 1;
      rect.y += 1;
      rect.width -= 2;
      rect.height -= 2;
      return rect;
    }

  }

}
