using UnityEngine;
using UnityEngine.EventSystems;

namespace Passer {

    public enum InputDeviceIDs {
        Head,
        LeftHand,
        RightHand,
        Controller
    }

    public class InteractionEventData : PointerEventData {
        public InteractionEventData(EventSystem eventSystem) : base(eventSystem) { }

        public InputDeviceIDs inputDevice;
        public Transform transform;
    }

    public class InteractionModule : BaseInputModule {
        protected static void DebugLog(string s) {
#if UNITY_EDITOR
            //Debug.Log(s);
#endif
        }

        private const int maxInteractions = 8;
        private int nInteractions = 3;

        public enum PointerType {
            Point,
            Touch
        }

        public class InteractionPointer {
            public Transform pointerTransform;
            public Vector3 localPointingDirection;
            public InteractionEventData data;

            public bool focusingEnabled = false;
            public bool focusing;

            public GameObject focusObject;
            public GameObject previousFocusObject;

            public Vector3 focusPosition;
            public Quaternion focusRotation;

            public Vector2 previousScreenPosition;
            public Vector3 previousFocusPosition;
            public Vector3 focusPositionDelta;

            public float focusStart;
            public float focusTimeToTouch = 2;

            public bool externalRayCast = false;

            public bool clicking = false;
            public GameObject touchedObject;
            public Vector3 touchPosition;

            public ControllerSide controllerInputSide;
            public Controller.Button controllerButton = Controller.Button.None;

            public bool hasClicked;

            public PointerType type;

            public InteractionPointer(PointerType type, Transform pointerTransform, EventSystem eventSystem) {
                this.type = type;
                this.pointerTransform = pointerTransform;
                this.data = new InteractionEventData(eventSystem);
            }

            public void ProcessFocus() {
                if (!focusingEnabled)
                    return;

                if (focusObject != previousFocusObject) {
                    DebugLog("Focus Change " + focusObject);

                    if (previousFocusObject != null) {
                        ExecuteEvents.ExecuteHierarchy(previousFocusObject, data, ExecuteEvents.pointerExitHandler);
                        ExecuteEvents.ExecuteHierarchy(previousFocusObject, data, ExecuteEvents.deselectHandler);
                    }
                    if (focusObject != null) {
                        ExecuteEvents.ExecuteHierarchy(focusObject, data, ExecuteEvents.pointerEnterHandler);
                        ExecuteEvents.ExecuteHierarchy(focusObject, data, ExecuteEvents.selectHandler);
                        focusStart = Time.time;
                        hasClicked = false;
                        focusing = true;
                    }
                    previousFocusObject = focusObject;
                }
            }

            public void ProcessNoFocus() {
                if (previousFocusObject != null) {
                    DebugLog("No Focus ");

                    ExecuteEvents.ExecuteHierarchy(previousFocusObject, data, ExecuteEvents.pointerExitHandler);
                    ExecuteEvents.ExecuteHierarchy(previousFocusObject, data, ExecuteEvents.deselectHandler);
                    focusing = false;
                    // no focus = no clicking
                    clicking = false;
                    previousFocusObject = null;
                }
            }

            public void ProcessTouch() {
                if (!focusing) {
                    // This is not working, because Unity may set this to the wrong object autonomously
                    //touchedObject = data.pointerCurrentRaycast.gameObject;
                    //if (touchedObject == null) {// object is a 3D object, as we do not use Physicsraycaster, use the focusObject
                    touchedObject = focusObject;
                    //}
                    DebugLog("Touch " + touchedObject);

                    ExecuteEvents.ExecuteHierarchy(touchedObject, data, ExecuteEvents.pointerEnterHandler);
                    ExecuteEvents.ExecuteHierarchy(touchedObject, data, ExecuteEvents.selectHandler);

                    focusing = true;
                    return;
                }
                else
                    ProcessFocus();

                if (!clicking) { // first activation
                    touchedObject = data.pointerCurrentRaycast.gameObject;
                    if (touchedObject == null) // object is a 3D object, as we do not use Physicsraycaster, use the focusObject
                        touchedObject = focusObject;
                    else
                        focusObject = touchedObject;

                    ExecuteEvents.ExecuteHierarchy(touchedObject, data, ExecuteEvents.pointerDownHandler);

                    clicking = true;
                }
                else { // we were already touching
                    if (focusPositionDelta.sqrMagnitude > 0) {  // moved finger during touch
                        GameObject pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(touchedObject);
                        if (!data.dragging) { // we were not dragging yet
                            if (pointerDrag != null) { // start dragging only where there is something to drag
                                data.pointerDrag = pointerDrag;
                                data.dragging = ExecuteEvents.ExecuteHierarchy(data.pointerDrag, data, ExecuteEvents.beginDragHandler);
                            }
                        }
                        else { // still dragging
                            ExecuteEvents.ExecuteHierarchy(data.pointerDrag, data, ExecuteEvents.dragHandler);
                        }

                    }
                    else { // finger did not move
                        if (data.dragging) { // we were dragging
                            ExecuteEvents.ExecuteHierarchy(data.pointerDrag, data, ExecuteEvents.endDragHandler);
                            data.dragging = false;
                        }
                    }
                }
            }

            public void ProcessNoTouch() {
                if (clicking) { // We were touching
                    if (data.dragging) {
                        ExecuteEvents.ExecuteHierarchy(data.pointerDrag, data, ExecuteEvents.endDragHandler);
                        data.dragging = false;
                    }
                    ExecuteEvents.ExecuteHierarchy(touchedObject, data, ExecuteEvents.pointerUpHandler);
                    ExecuteEvents.ExecuteHierarchy(touchedObject, data, ExecuteEvents.pointerClickHandler);
                    clicking = false;
                    touchedObject = null;
                }
                ProcessNoFocus();
            }

            public void ProcessClick() {
                if (hasClicked)
                    return;

                if (touchedObject != null) {
                    ExecuteEvents.ExecuteHierarchy(touchedObject, data, ExecuteEvents.pointerUpHandler);
                }
                else {
                    ExecuteEvents.ExecuteHierarchy(focusObject, data, ExecuteEvents.pointerUpHandler);
                }
                hasClicked = true;
            }
        }

        public InteractionPointer[] pointers = new InteractionPointer[maxInteractions];

        public int CreateNewInteraction(Transform transform, float timedClick) {
            InteractionPointer pointer = new InteractionPointer(PointerType.Point, transform, eventSystem);
            pointers[nInteractions++] = pointer;

            pointer.focusingEnabled = false;
            pointer.localPointingDirection = Vector3.forward;

            pointer.focusTimeToTouch = timedClick;

            return nInteractions - 1;
        }

        public int CreateNewInteraction(Transform transform, float timedClick, EventSystem eventSystem) {
            InteractionPointer pointer = new InteractionPointer(PointerType.Point, transform, eventSystem);
            pointers[nInteractions++] = pointer;

            pointer.focusingEnabled = false;
            pointer.localPointingDirection = Vector3.forward;

            pointer.focusTimeToTouch = timedClick;

            return nInteractions - 1;
        }


        public void ActivatePointing(int inputDevice, bool active) {
            if (!pointers[inputDevice].focusingEnabled && active)
                pointers[inputDevice].focusStart = 0;
            pointers[inputDevice].focusingEnabled = active;
        }

        public bool IsPointing(int inputDevice) {
            return pointers[inputDevice].focusingEnabled;
        }

        public void SetPointingDirection(int inputDevice, Vector3 direction) {
            InteractionPointer pointer = pointers[inputDevice];
            pointer.localPointingDirection = pointer.pointerTransform.InverseTransformDirection(direction);
        }

        public bool IsTimedClick(int inputDevice) {
            InteractionPointer pointer = pointers[inputDevice];

            return
                !pointer.hasClicked &&
                pointer.focusTimeToTouch != 0 &&
                pointer.focusStart > 0 &&
                Time.time - pointer.focusStart > pointer.focusTimeToTouch;
        }

        public void ClickDown(int inputDevice) {
            pointers[inputDevice].ProcessTouch();
        }

        public void ClickUp(int inputDevice) {
            pointers[inputDevice].ProcessNoTouch();
        }

        #region FingerInput

        public void EnableTouchInput(Humanoid.HumanoidControl humanoid, bool isLeft, float autoActivation) {
            if (humanoid.avatarRig == null)
                return;

            if (pointers == null)
                pointers = new InteractionPointer[maxInteractions]; // 0 = left index, 1 = right index, 2 = head, 3 = controller

            EventSystem eventSystem = GameObject.FindObjectOfType<EventSystem>();
            if (eventSystem == null)
                eventSystem = humanoid.gameObject.AddComponent<EventSystem>();

            Controller controllerInput = Controllers.GetController(0);

            Transform indexFingerDistal = null;
            Transform indexFingerTip = null;

            InteractionPointer pointer = null;
            if (isLeft) {
                indexFingerDistal = humanoid.leftHandTarget.fingers.index.distal.bone.transform;
                if (indexFingerDistal != null) {
                    if (indexFingerDistal.childCount == 1)
                        indexFingerTip = indexFingerDistal.GetChild(0);
                    else
                        indexFingerTip = indexFingerDistal;
                    pointer = new InteractionPointer(PointerType.Touch, indexFingerTip, eventSystem);
                    pointers[(int)InputDeviceIDs.LeftHand] = pointer;

                    if (controllerInput != null)
                        pointer.controllerInputSide = controllerInput.left;
                }
            }
            else {
                indexFingerDistal = humanoid.rightHandTarget.fingers.index.distal.bone.transform;
                if (indexFingerDistal != null) {
                    if (indexFingerDistal.childCount == 1)
                        indexFingerTip = indexFingerDistal.GetChild(0);
                    else
                        indexFingerTip = indexFingerDistal;
                    pointer = new InteractionPointer(PointerType.Touch, indexFingerTip, eventSystem);
                    pointers[(int)InputDeviceIDs.RightHand] = pointer;

                    if (controllerInput != null)
                        pointer.controllerInputSide = controllerInput.right;
                }
            }

            if (indexFingerDistal != null) {
                if (indexFingerDistal.childCount > 0) {
                    indexFingerTip = indexFingerDistal.GetChild(0);
                    pointer.localPointingDirection = indexFingerTip.InverseTransformDirection(indexFingerTip.position - indexFingerDistal.position).normalized;
                }
                else {
                    pointer.localPointingDirection = indexFingerDistal.InverseTransformDirection(indexFingerDistal.position - indexFingerDistal.parent.position).normalized;
                }
            }

            if (pointer != null) {
                pointer.focusTimeToTouch = autoActivation;

                // To support hovering
                pointer.focusingEnabled = true;
            }
        }

        public void ActivateFingerPointing(bool isPointing, bool isLeft) {
            InteractionPointer pointer = isLeft ? pointers[(int)InputDeviceIDs.LeftHand] : pointers[(int)InputDeviceIDs.RightHand];

            if (pointer != null) {
                pointer.focusingEnabled = isPointing; // only focusing when we are pointing
            }
        }

        public void OnFingerTouchStart(bool isLeft, GameObject obj) {
            InteractionPointer pointer = isLeft ? pointers[(int)InputDeviceIDs.LeftHand] : pointers[(int)InputDeviceIDs.RightHand];
            if (pointer == null)
                return;

            pointer.focusObject = obj;
            pointer.data.delta = Vector3.zero;
            pointer.ProcessTouch();
        }

        public void OnFingerTouchEnd(bool isLeft) {
            InteractionPointer pointer = isLeft ? pointers[(int)InputDeviceIDs.LeftHand] : pointers[(int)InputDeviceIDs.RightHand];
            if (pointer == null)
                return;

            pointer.ProcessNoTouch();
        }

        public void HandInteractionActivation(bool isLeft) {
            InteractionPointer pointer = isLeft ? pointers[(int)InputDeviceIDs.LeftHand] : pointers[(int)InputDeviceIDs.RightHand];
            if (pointer == null)
                return;

            pointer.ProcessTouch();
        }
        #endregion

        public Vector3 GetFocusPoint(int inputDeviceID) {
            return pointers[inputDeviceID].focusPosition;
        }
        public Quaternion GetFocusRotation(int inputDeviceID) {
            return pointers[inputDeviceID].focusRotation;
        }

        public GameObject GetFocusObject(int inputDeviceID) {
            if (pointers[(int)inputDeviceID].focusObject != null)
                return pointers[(int)inputDeviceID].focusObject;
            else
                return null;
        }

        public GameObject GetTouchObject(InputDeviceIDs inputDeviceID) {
            InteractionPointer pointer = pointers[(int)inputDeviceID];
            if (pointer == null)
                return null;

            if (pointer.touchedObject != null) {
                return pointer.touchedObject;
            }
            else
                return null;
        }

        public float GetGazeDuration(InputDeviceIDs inputDeviceID) {
            return Time.time - pointers[(int)inputDeviceID].focusStart;
        }

        public void SetExternalRayCast(int inputDeviceID, Vector3 focusPosition, Quaternion focusRotation, GameObject focusObject) {
            InteractionPointer pointer = pointers[inputDeviceID];
            pointer.focusPosition = focusPosition;
            pointer.focusRotation = focusRotation;
            pointer.focusObject = focusObject;
            pointer.externalRayCast = true;
        }

        protected virtual void Update() {
            for (int i = 0; i < pointers.Length; i++) {
                if (pointers[i] != null) {
                    ProcessPointer(pointers[i], (InputDeviceIDs)i);
                }
            }
        }

        // This is disabled because it only works when the game window is selected
        public override void Process() {
        }

        private void ProcessPointer(InteractionPointer pointer, InputDeviceIDs inputDeviceID) {
            CastRayFromPointer(pointer, inputDeviceID);

            if (pointer.focusingEnabled && pointer.type == PointerType.Point)
                pointer.ProcessFocus();
            if (pointer.focusObject != null && pointer.focusTimeToTouch != 0 && Time.time - pointer.focusStart > pointer.focusTimeToTouch) { // we are clicking
                pointer.ProcessTouch();
                pointer.ProcessNoTouch();
            }
            else if (pointer.clicking) {
                pointer.ProcessTouch();
            }

            if (pointer.data.pointerCurrentRaycast.gameObject == null) { // no focus
                return;
            }

            pointer.data.pressPosition = pointer.data.position;
            pointer.data.pointerPressRaycast = pointer.data.pointerCurrentRaycast;
            pointer.data.pointerPress = null; //Clear this for setting later
            pointer.data.useDragThreshold = true;

            if (pointer.type == PointerType.Touch) {
                // UI touch without colliders works on the finger tip
                float distance = DistanceTipToTransform(pointer.pointerTransform, pointer.data.pointerCurrentRaycast.gameObject.transform);
                if (distance < 0) // we are touching
                    pointer.ProcessTouch();                
                else if (distance > 0.05F)
                    pointer.ProcessNoTouch();
                else
                    pointer.ProcessFocus();

            }
        }

        private void CastRayFromPointer(InteractionPointer pointer, InputDeviceIDs inputDeviceID) {
            pointer.data.Reset();
            pointer.data.inputDevice = inputDeviceID;
            pointer.data.transform = pointer.pointerTransform;
            if (pointer.pointerTransform == null)
                return;

            //CastPhysicsRayFromPointer(pointer);
            CastUIRayFromPointer(pointer);

            pointer.data.scrollDelta = Vector2.zero;
            if (pointer.focusObject != null || pointer.touchedObject != null) {
                if (pointer.previousFocusPosition.sqrMagnitude == 0) {
                    pointer.data.delta = Vector2.zero;
                    pointer.focusPositionDelta = Vector3.zero;
                }
                else {
                    pointer.data.delta = pointer.data.position - pointer.previousScreenPosition;
                    pointer.focusPositionDelta = pointer.focusPosition - pointer.previousFocusPosition;
                }
                pointer.previousScreenPosition = pointer.data.position;
                pointer.previousFocusPosition = pointer.focusPosition;

                pointer.touchPosition = pointer.data.pointerCurrentRaycast.worldPosition;
            }
        }

        private void CastPhysicsRayFromPointer(InteractionPointer pointer) {
            RaycastResult raycastResult = new RaycastResult();

            if (pointer.focusingEnabled && pointer.type == PointerType.Point) {
                if (pointer.externalRayCast) {
                    raycastResult.worldPosition = pointer.focusPosition;
                    raycastResult.worldNormal = pointer.focusRotation * Vector3.forward;
                    raycastResult.distance = (raycastResult.worldPosition - pointer.pointerTransform.position).magnitude;
                    raycastResult.gameObject = pointer.focusObject;
                    Debug.Log("raycasthit 1: " + pointer.focusObject);
                }
                else {
                    Vector3 pointingDirection = pointer.pointerTransform.rotation * pointer.localPointingDirection;

                    RaycastHit hit;
                    bool raycastHit = Physics.Raycast(pointer.pointerTransform.position, pointingDirection * 10, out hit);
                    if (raycastHit) {
                        pointer.focusPosition = hit.point;
                        pointer.focusRotation = Quaternion.LookRotation(hit.normal);
                        pointer.focusObject = hit.transform.gameObject;

                        raycastResult.worldPosition = hit.point;
                        raycastResult.worldNormal = hit.normal;
                        raycastResult.distance = hit.distance;
                        raycastResult.gameObject = hit.transform.gameObject;
                        Debug.Log("raycasthit 2: " + hit.transform.gameObject);

                    }
                    else {
                        pointer.focusPosition = pointer.pointerTransform.position + pointingDirection * 10;
                        pointer.focusRotation = Quaternion.LookRotation(-pointingDirection);
                        pointer.focusObject = null;

                        raycastResult.worldPosition = pointer.pointerTransform.position + pointingDirection * 10;
                        raycastResult.worldNormal = -pointingDirection;
                        raycastResult.distance = 0;
                        raycastResult.gameObject = null;
                    }

                    if (Camera.main != null) {
                        pointer.data.position = Camera.main.WorldToScreenPoint(pointer.focusPosition);
                        raycastResult.screenPosition = Camera.main.WorldToScreenPoint(pointer.focusPosition);
                    }
                }
            }
            else {
                pointer.focusPosition = pointer.pointerTransform.position;
                pointer.focusRotation = Quaternion.identity;
                pointer.focusObject = null;

                raycastResult.worldPosition = pointer.pointerTransform.position;
                raycastResult.worldNormal = Vector3.up;
                raycastResult.distance = 0;
                raycastResult.gameObject = null;

                if (Camera.main != null) {
                    pointer.data.position = Camera.main.WorldToScreenPoint(pointer.pointerTransform.position);
                    raycastResult.screenPosition = Camera.main.WorldToScreenPoint(pointer.focusPosition);
                }
            }

            pointer.data.pointerCurrentRaycast = raycastResult;
        }

        //private EventSystem eventSystem;
        private void CastUIRayFromPointer(InteractionPointer pointer) {
            // We only support touch UI interaction without colliders
            if (Camera.main == null || pointer.type != PointerType.Touch)
                return;

            pointer.data.position = Camera.main.WorldToScreenPoint(pointer.pointerTransform.position);
            float distanceToPointer = Vector3.Distance(Camera.main.transform.position, pointer.pointerTransform.position);

            System.Collections.Generic.List<RaycastResult> m_RaycastResultCache = new System.Collections.Generic.List<RaycastResult>();
            eventSystem.RaycastAll(pointer.data, m_RaycastResultCache);
            RaycastResult raycastResult = FindFirstRaycast(m_RaycastResultCache, distanceToPointer);
            m_RaycastResultCache.Clear();


            if (raycastResult.gameObject != null) {
                if (pointer.type == PointerType.Touch) {
                    float distance = DistanceTipToTransform(pointer.pointerTransform, raycastResult.gameObject.transform);
                    // Focus only when hovering
                    if (distance > 0.05F)
                        return;
                }

                pointer.data.pointerCurrentRaycast = raycastResult;
                pointer.focusObject = pointer.data.pointerCurrentRaycast.gameObject;
                // EventSystem.RaycastAll always casts from main.camera. This is why we need a trick to look like it is casting from the pointerlocation (e.g. finger)
                Vector3 focusDirection = (pointer.focusPosition - Camera.main.transform.position).normalized;
                pointer.focusPosition = Camera.main.transform.position + focusDirection * pointer.data.pointerCurrentRaycast.distance; // pointer.data.pointerCurrentRaycast.worldPosition == Vector.zero unfortunately
                                                                                                                                       //pointer.data.position = pointer.focusPosition;
                pointer.data.position = Camera.main.WorldToScreenPoint(pointer.focusPosition);

            }
        }

        private RaycastResult FindFirstRaycast(System.Collections.Generic.List<RaycastResult> raycastResults, float pointerDistance) {
            foreach (RaycastResult result in raycastResults) {
                float resultDistance = Vector3.Distance(Camera.main.transform.position, result.worldPosition);
                if (result.isValid &&
                    result.worldPosition != Vector3.zero &&
                    Mathf.Abs(resultDistance - pointerDistance) < 0.02F) {
                    return result;
                }
            }
            return new RaycastResult();
        }

        private Vector2 NormalizedCartesianToSpherical(Vector3 cartCoords) {
            cartCoords.Normalize();
            if (cartCoords.x == 0)
                cartCoords.x = Mathf.Epsilon;
            float outPolar = Mathf.Atan(cartCoords.z / cartCoords.x);
            if (cartCoords.x < 0)
                outPolar += Mathf.PI;
            float outElevation = Mathf.Asin(cartCoords.y);
            return new Vector2(outPolar, outElevation);
        }

        private float DistanceTipToTransform(Transform fingerTip, Transform transform) {
            return (-transform.InverseTransformPoint(fingerTip.position).z * transform.lossyScale.z) - 0.01F;
        }
    }
}