// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using UnityEngine;
using System;

namespace HoloToolkit.Unity.InputModule
{
    /// <summary>
    /// AxisController uses the keyboard, mouse, or joystick and allows
    /// you to map a 1 axis controller to 1 axis displacement via GetDiplacement1()
    ///  or to map a 2 axis controller to 2 axis displacement via GetDisplacement2()
    ///  or to map a 2 axis controller to 2 of the 3 axis displacement via GetDisplacement3()
    /// </summary>
    public class AxisController : MonoBehaviour
    {
        /// <summary>
        /// Type of input axis, based on device.
        /// </summary>
        public enum AxisType
        {
            // Axis are double axis (XY) unless indicated as single axis (X only)
            InputManagerAxis,

            KeyboardArrows,
            KeyboardWASD,
            KeyboardQE,     // single axis
            KeyboardIJKL,
            KeyboardUO,     // single axis
            Keyboard8426,
            Keyboard7193,
            KeyboardPeriodComma,    // single axis
            KeyboardBrackets,
            KeyBoardHomeEndPgUpPgDown,

            Mouse,
            MouseScroll,    // single axis

            None
        }

        /// <summary>
        /// Each input axis, x, y, or z, will get mapped to an output axis, with potential inversion
        /// </summary>
        public enum AxisDestination
        {
            PositiveX,
            NegativeX,
            PositiveY,
            NegativeY,
            PositiveZ,
            NegativeZ,
            None
        }

        public float SensitivityScale = 3.0f;

        [Tooltip("Use unscaled time. This is useful for games that have a pause mechanism or otherwise adjust the game timescale.")]
        public bool UseUnscaledTime = true;

        public AxisType axisType = AxisType.Mouse;
        public ButtonController.ButtonType buttonType = ButtonController.ButtonType.None;

        public string InputManagerHorizontalAxisName;
        public string InputManagerVerticalAxisName;

        public AxisDestination Axis0Destination = AxisDestination.PositiveX;
        public AxisDestination Axis1Destination = AxisDestination.PositiveY;
        public AxisDestination Axis2Destination = AxisDestination.None;

        private Vector3 lastMousePosition = Vector3.zero;

        private const float MouseSensitivity = 0.015f;           // always affects the mouse sensitivity
        private const float MouseUnlockedSensitivity = 0.1f;    // affects the sensitivity when using the mouse buttons
        private const float KeyboardSensitivity = 10.0f;
        private const float InputManagerAxisSensitivity = 0.05f;

        private bool isMouseJumping = false;
        private bool appHasFocus = true;
        private bool usingMouse = false;

        private bool inputManagerAxesNeedApproval = true;
        private bool inputManagerHorizontalAxisApproved = false;
        private bool inputManagerVerticalAxisApproved = false;

        public bool AxisTypeIsKeyboard
        {
            get { return AxisType.KeyboardArrows <= axisType && axisType <= AxisType.KeyBoardHomeEndPgUpPgDown; }
        }
        public bool AxisTypeIsInputManagerAxis
        {
            get { return axisType == AxisType.InputManagerAxis; }
        }
        public bool AxisTypeIsMouse
        {
            get { return axisType == AxisType.Mouse; }
        }
        public bool AxisTypeIsMouseScroll
        {
            get { return axisType == AxisType.MouseScroll; }
        }

        public void EnableAndCheck(bool value)
        {
            this.enabled = value;
            if (value)
            {
                InputManagerAxisCheck();
            }
        }

        private void Awake()
        {
            // AxisController is for development only and should not exist--and certainly not be used--in
            // any non-Editor scenario.
#if !UNITY_EDITOR
            Destroy(this);
#else
            // Workaround for Remote Desktop.  Ctrl-mouse, Shift-mouse, and Alt-mouse don't work, so they should be avoided.
            if (IsRunningUnderRemoteDesktop())
            {
                if (this.buttonType == ButtonController.ButtonType.Control)
                {
                    this.buttonType = ButtonController.ButtonType.Left;
                    Debug.LogWarning("Running under Remote Desktop, so changed AxisContol method to Left mouse button");
                }
                if (this.buttonType == ButtonController.ButtonType.Alt)
                {
                    this.buttonType = ButtonController.ButtonType.Right;
                    Debug.LogWarning("Running under Remote Desktop, so changed AxisContol method to Right mouse button");
                }
                if (this.buttonType == ButtonController.ButtonType.Shift)
                {
                    this.buttonType = ButtonController.ButtonType.Middle;
                    Debug.LogWarning("Running under Remote Desktop, so changed AxisContol method to Middle mouse button");
                }
            }

            UnityEngine.Cursor.lockState = CursorLockMode.None;
            UnityEngine.Cursor.visible = true;
#endif
        }

        private static float InputCurve(float x)
        {
            // smoothing input curve, converts from [-1,1] to [-2,2]
            return (Mathf.Sign(x) * (1.0f - Mathf.Cos(.5f * Mathf.PI * Mathf.Clamp(x, -1.0f, 1.0f))));
        }

        /// <summary>
        /// Get a Vector3 populated with axis mapped displacements.
        /// </summary>
        public Vector3 GetDisplacementVector3()
        {
            Vector3 source = GetDisplacement();
            Vector3 dest = Vector3.zero;
            RemapAdditive(source, 0, ref dest, Axis0Destination);
            RemapAdditive(source, 1, ref dest, Axis1Destination);
            RemapAdditive(source, 2, ref dest, Axis2Destination);
            return dest;
        }

        /// <summary>
        /// Get a Vector2 populated with axis mapped displacements.
        /// </summary>
        public Vector2 GetDisplacementVector2()
        {
            Vector3 source = GetDisplacement();
            Vector3 middle = Vector3.zero;
            Vector2 dest = Vector2.zero;
            RemapAdditive(source, 0, ref middle, Axis0Destination);
            RemapAdditive(source, 1, ref middle, Axis1Destination);
            dest[0] = middle[0];
            dest[1] = middle[1];
            return dest;
        }

        /// <summary>
        /// Get a float populated with axis mapped displacements.
        /// </summary>
        public float GetDisplacementFloat()
        {
            Vector3 source = GetDisplacement();
            Vector3 middle = Vector2.zero;
            RemapAdditive(source, 0, ref middle, Axis0Destination);
            return middle[0];
        }

        private void RemapAdditive(Vector3 source, int sourceDim, ref Vector3 dest, AxisDestination destDim)
        {
            float inp = source[sourceDim];
            if (destDim == AxisDestination.NegativeX || destDim == AxisDestination.NegativeY || destDim == AxisDestination.NegativeZ)
            {
                inp = -inp;
            }
            if (destDim == AxisDestination.PositiveX || destDim == AxisDestination.NegativeX)
            {
                dest[0] += inp;
            }
            else if (destDim == AxisDestination.PositiveY || destDim == AxisDestination.NegativeY)
            {
                dest[1] += inp;
            }
            else if (destDim == AxisDestination.PositiveZ || destDim == AxisDestination.NegativeZ)
            {
                dest[2] += inp;
            }
        }

        private Vector3 GetDisplacement()
        {
            Vector3 rot = Vector3.zero;

            // this check enables us to check the InputManagerAxes names when we are switching on the fly
            if (!AxisTypeIsInputManagerAxis)
            {
                inputManagerAxesNeedApproval = true;
            }

            // Now check to see what sort of input we have, and dispatch the appropriate LookTick routine
            if (AxisTypeIsInputManagerAxis)
            {
                if (inputManagerAxesNeedApproval)
                {
                    InputManagerAxisCheck();
                }
                if (ShouldControl())
                {
                    rot = InputManagerAxisLookTick();
                }
            }
            else if (AxisTypeIsKeyboard)
            {
                if (ShouldControl())
                    rot = KeyboardLookTick();
            }
            else if (AxisTypeIsMouseScroll)
            {
                if (ShouldControl())
                    rot.x += Input.GetAxis("Mouse ScrollWheel");
            }
            else if (AxisTypeIsMouse)
            {
                if (ShouldControl())
                {
                    if (!this.usingMouse)
                    {
                        OnStartMouseLook();
                        this.usingMouse = true;
                    }
                    rot = MouseLookTick();
                }
                else
                {
                    if (this.usingMouse)
                    {
                        OnEndMouseLook();
                        this.usingMouse = false;
                    }
                }
            }

            rot *= this.SensitivityScale;
            return rot;
        }

        private void OnStartMouseLook()
        {
            if (this.buttonType <= ButtonController.ButtonType.Middle)
            {
                // if mouse button is either left, right or middle
                SetWantsMouseJumping(true);
            }
            else if (this.buttonType <= ButtonController.ButtonType.Focused)
            {
                // if mouse button is either control, shift or focused
                UnityEngine.Cursor.lockState = CursorLockMode.Locked;
                UnityEngine.Cursor.visible = false;
            }

            // do nothing if (this.MouseLookButton == MouseButton.None)
        }

        private void OnEndMouseLook()
        {
            if (this.buttonType <= ButtonController.ButtonType.Middle)
            {
                // if mouse button is either left, right or middle
                SetWantsMouseJumping(false);
            }
            else if (this.buttonType <= ButtonController.ButtonType.Focused)
            {
                // if mouse button is either control, shift or focused
                UnityEngine.Cursor.lockState = CursorLockMode.None;
                UnityEngine.Cursor.visible = true;
            }

            // do nothing if (this.MouseLookButton == MouseButton.None)
        }

        private Vector3 MouseLookTick()
        {
            Vector3 rot = Vector3.zero;

            // Use frame-to-frame mouse delta in pixels to determine mouse rotation. The traditional
            // GetAxis("Mouse X") method doesn't work under Remote Desktop.
            Vector3 mousePositionDelta = Input.mousePosition - this.lastMousePosition;
            this.lastMousePosition = Input.mousePosition;

            if (UnityEngine.Cursor.lockState == CursorLockMode.Locked)
            {
                mousePositionDelta.x = Input.GetAxis("Mouse X");
                mousePositionDelta.y = Input.GetAxis("Mouse Y");
            }
            else
            {
                mousePositionDelta.x *= MouseUnlockedSensitivity;
                mousePositionDelta.y *= MouseUnlockedSensitivity;
            }

            rot.x += -InputCurve(mousePositionDelta.y) * MouseSensitivity;
            rot.y += InputCurve(mousePositionDelta.x) * MouseSensitivity;
            return rot;
        }

        private float GetKeyDir(KeyCode neg, KeyCode pos)
        {
            return Input.GetKey(neg) ? -1.0f : Input.GetKey(pos) ? 1.0f : 0.0f;
        }

        private float GetKeyDir(string neg, string pos)
        {
            return Input.GetKey(neg) ? -1.0f : Input.GetKey(pos) ? 1.0f : 0.0f;
        }

        private void InputManagerAxisCheck()
        {
            inputManagerHorizontalAxisApproved = false;
            inputManagerVerticalAxisApproved = false;

            {
                inputManagerHorizontalAxisApproved = true;
                try
                {
                    Input.GetAxis(InputManagerHorizontalAxisName);
                }
                catch (Exception)
                {
                    Debug.LogWarningFormat("Input Axis {0} is not setup. Use Edit -> Project Settings -> Input", InputManagerHorizontalAxisName);
                    inputManagerHorizontalAxisApproved = false;
                }
            }

            {
                inputManagerVerticalAxisApproved = true;
                try
                {
                    Input.GetAxis(InputManagerVerticalAxisName);
                }
                catch (Exception)
                {
                    Debug.LogWarningFormat("Input Axis {0} is not setup. Use Edit -> Project Settings -> Input", InputManagerVerticalAxisName);
                    inputManagerVerticalAxisApproved = false;
                }
            }
            inputManagerAxesNeedApproval = false;
        }

        private Vector3 InputManagerAxisLookTick()
        {
            Vector3 rot = Vector3.zero;
            if (inputManagerHorizontalAxisApproved)
            {
                rot.x += InputManagerAxisSensitivity * InputCurve(Input.GetAxis(InputManagerHorizontalAxisName));
            }
            if (inputManagerVerticalAxisApproved)
            {
                rot.y += InputManagerAxisSensitivity * InputCurve(Input.GetAxis(InputManagerVerticalAxisName));
            }
            return rot;
        }

        private Vector3 KeyboardLookTick()
        {
            float deltaTime = UseUnscaledTime
                ? Time.unscaledDeltaTime
                : Time.deltaTime;

            Vector3 rot = Vector3.zero;
            if (axisType == AxisType.KeyboardArrows)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.LeftArrow, KeyCode.RightArrow));
                rot.y += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.DownArrow, KeyCode.UpArrow));
            }
            else if (axisType == AxisType.KeyboardWASD)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.A, KeyCode.D));
                rot.y += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.S, KeyCode.W));
            }
            else if (axisType == AxisType.KeyboardQE)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.Q, KeyCode.E));
            }
            else if (axisType == AxisType.KeyboardIJKL)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.J, KeyCode.L));
                rot.y += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.K, KeyCode.I));
            }
            else if (axisType == AxisType.KeyboardUO)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.U, KeyCode.O));
            }
            else if (axisType == AxisType.Keyboard8426)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.Keypad4, KeyCode.Keypad6));
                rot.y += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.Keypad2, KeyCode.Keypad8));
            }
            else if (axisType == AxisType.Keyboard7193)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.Keypad1, KeyCode.Keypad7));
                rot.y += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.Keypad3, KeyCode.Keypad9));
            }
            else if (axisType == AxisType.KeyboardPeriodComma)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.Comma, KeyCode.Period));
            }
            else if (axisType == AxisType.KeyboardBrackets)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.LeftBracket, KeyCode.RightBracket));
            }
            else if (axisType == AxisType.KeyBoardHomeEndPgUpPgDown)
            {
                rot.x += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.End, KeyCode.Home));
                rot.y += InputCurve(deltaTime * KeyboardSensitivity * GetKeyDir(KeyCode.PageDown, KeyCode.PageUp));
            }
            return rot;
        }

        /// <summary>
        /// Only allow the mouse to control rotation when Unity has focus. This enables
        /// the player to temporarily alt-tab away without having the player look around randomly
        /// back in the Unity Game window.
        /// </summary>
        /// <returns>Whether the user is holding down the control button.</returns>
        public bool ShouldControl()
        {
            if (!this.appHasFocus)
            {
                return false;
            }
            else if (this.buttonType == ButtonController.ButtonType.None)
            {
                return true;
            }
            else if (this.buttonType <= ButtonController.ButtonType.Middle)
            {
                return Input.GetMouseButton((int)this.buttonType);
            }
            else if (this.buttonType == ButtonController.ButtonType.Control)
            {
                return Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
            }
            else if (this.buttonType == ButtonController.ButtonType.Shift)
            {
                return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
            }
            else if (this.buttonType == ButtonController.ButtonType.Alt)
            {
                return Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);
            }
            else if (this.buttonType == ButtonController.ButtonType.Space)
            {
                return Input.GetKey(KeyCode.Space);
            }
            else if (this.buttonType == ButtonController.ButtonType.Return)
            {
                return Input.GetKey(KeyCode.Return);
            }
            else if (this.buttonType == ButtonController.ButtonType.Focused)
            {
                if (!this.usingMouse)
                {
                    // any kind of click will capture focus
                    return Input.GetMouseButtonDown((int)ButtonController.ButtonType.Left)
                        || Input.GetMouseButtonDown((int)ButtonController.ButtonType.Right)
                        || Input.GetMouseButtonDown((int)ButtonController.ButtonType.Middle);
                }
                else
                {
                    // pressing escape will stop capture
                    return !Input.GetKeyDown(KeyCode.Escape);
                }
            }

            return false;
        }

        private void OnApplicationFocus(bool focusStatus)
        {
            this.appHasFocus = focusStatus;
        }

        /// <summary>
        ///  Mouse jumping is typically used during one of the mouse button modes.
        ///  It means that the cursor will be invisible when it is outside of the
        ///  Unity game view window, and visible when it breaches the outer edges.
        /// </summary>
        /// <param name="wantsJumping">Whether the mouse cursor should be visible over the game window.</param>
        private void SetWantsMouseJumping(bool wantsJumping)
        {
            if (wantsJumping != this.isMouseJumping)
            {
                this.isMouseJumping = wantsJumping;

                if (wantsJumping)
                {
                    // unlock the cursor if it was locked
                    UnityEngine.Cursor.lockState = CursorLockMode.None;

                    // hide the cursor
                    UnityEngine.Cursor.visible = false;

                    this.lastMousePosition = Input.mousePosition;
                }
                else
                {
                    // recenter the cursor (setting lockCursor has side-effects under the hood)
                    UnityEngine.Cursor.lockState = CursorLockMode.Locked;
                    UnityEngine.Cursor.lockState = CursorLockMode.None;

                    // show the cursor
                    UnityEngine.Cursor.visible = true;
                }

#if UNITY_EDITOR
                UnityEditor.EditorGUIUtility.SetWantsMouseJumping(wantsJumping ? 1 : 0);
#endif
            }
        }

#if UNITY_EDITOR
        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        private static extern uint GetCurrentProcessId();

        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        private static extern bool ProcessIdToSessionId(uint dwProcessId, out uint pSessionId);

        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        private static extern uint WTSGetActiveConsoleSessionId();

        private bool IsRunningUnderRemoteDesktop()
        {
            uint processId = GetCurrentProcessId();
            uint sessionId;
            return ProcessIdToSessionId(processId, out sessionId) && (sessionId != WTSGetActiveConsoleSessionId());
        }
#else
        private bool IsRunningUnderRemoteDesktop()
        {
            return false;
        }
#endif
    }

}
