using System.Runtime.CompilerServices;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Processors;
using UnityEngine.InputSystem.Utilities;

////REVIEW: change 'clampToConstant' to simply 'clampToMin'?

////TODO: if AxisControl fields where properties, we wouldn't need ApplyParameterChanges, maybe it's ok breaking change?

namespace UnityEngine.InputSystem.Controls
{
    /// <summary>
    /// A floating-point axis control.
    /// </summary>
    /// <remarks>
    /// Can optionally be configured to perform normalization.
    /// Stored as either a float, a short, a byte, or a single bit.
    /// </remarks>
    public class AxisControl : InputControl<float>
    {
        /// <summary>
        /// Clamping behavior for an axis control.
        /// </summary>
        public enum Clamp
        {
            /// <summary>
            /// Do not clamp values.
            /// </summary>
            None = 0,

            /// <summary>
            /// Clamp values to <see cref="clampMin"/> and <see cref="clampMax"/>
            /// before normalizing the value.
            /// </summary>
            BeforeNormalize = 1,

            /// <summary>
            /// Clamp values to <see cref="clampMin"/> and <see cref="clampMax"/>
            /// after normalizing the value.
            /// </summary>
            AfterNormalize = 2,

            /// <summary>
            /// Clamp values any value below <see cref="clampMin"/> or above <see cref="clampMax"/>
            /// to <see cref="clampConstant"/> before normalizing the value.
            /// </summary>
            ToConstantBeforeNormalize = 3,
        }

        // These can be added as processors but they are so common that we
        // build the functionality right into AxisControl to save us an
        // additional object and an additional virtual call.

        // NOTE: A number of the parameters here can be expressed in much simpler form.
        //       E.g. 'scale', 'scaleFactor' and 'invert' could all be rolled into a single
        //       multiplier. And maybe that's what we should do. However, the one advantage
        //       of the current setup is that it allows to set these operations up individually.
        //       For example, a given layout may want to have a very specific scale factor but
        //       then a derived layout needs the value to be inverted. If it was a single setting,
        //       the derived layout would have to know the specific scale factor in order to come
        //       up with a valid multiplier.

        /// <summary>
        /// Clamping behavior when reading values. <see cref="Clamp.None"/> by default.
        /// </summary>
        /// <value>Clamping behavior.</value>
        /// <remarks>
        /// When a value is read from the control's state, it is first converted
        /// to a floating-point number.
        /// </remarks>
        /// <seealso cref="clampMin"/>
        /// <seealso cref="clampMax"/>
        /// <seealso cref="clampConstant"/>
        public Clamp clamp;

        /// <summary>
        /// Lower end of the clamping range when <see cref="clamp"/> is not
        /// <see cref="Clamp.None"/>.
        /// </summary>
        /// <value>Lower bound of clamping range. Inclusive.</value>
        public float clampMin;

        /// <summary>
        /// Upper end of the clamping range when <see cref="clamp"/> is not
        /// <see cref="Clamp.None"/>.
        /// </summary>
        /// <value>Upper bound of clamping range. Inclusive.</value>
        public float clampMax;

        /// <summary>
        /// When <see cref="clamp"/> is set to <see cref="Clamp.ToConstantBeforeNormalize"/>
        /// and the value is outside of the range defined by <see cref="clampMin"/> and
        /// <see cref="clampMax"/>, this value is returned.
        /// </summary>
        /// <value>Constant value to return when value is outside of clamping range.</value>
        public float clampConstant;

        ////REVIEW: why not just roll this into scaleFactor?
        /// <summary>
        /// If true, the input value will be inverted, i.e. multiplied by -1. Off by default.
        /// </summary>
        /// <value>Whether to invert the input value.</value>
        public bool invert;

        /// <summary>
        /// If true, normalize the input value to [0..1] or [-1..1] (depending on the
        /// value of <see cref="normalizeZero"/>. Off by default.
        /// </summary>
        /// <value>Whether to normalize input values or not.</value>
        /// <seealso cref="normalizeMin"/>
        /// <seealso cref="normalizeMax"/>
        public bool normalize;

        ////REVIEW: shouldn't these come from the control min/max value by default?

        /// <summary>
        /// If <see cref="normalize"/> is on, this is the input value that corresponds
        /// to 0 of the normalized [0..1] or [-1..1] range.
        /// </summary>
        /// <value>Input value that should become 0 or -1.</value>
        /// <remarks>
        /// In other words, with <see cref="normalize"/> on, input values are mapped from
        /// the range of [normalizeMin..normalizeMax] to [0..1] or [-1..1] (depending on
        /// <see cref="normalizeZero"/>).
        /// </remarks>
        public float normalizeMin;

        /// <summary>
        /// If <see cref="normalize"/> is on, this is the input value that corresponds
        /// to 1 of the normalized [0..1] or [-1..1] range.
        /// </summary>
        /// <value>Input value that should become 1.</value>
        /// <remarks>
        /// In other words, with <see cref="normalize"/> on, input values are mapped from
        /// the range of [normalizeMin..normalizeMax] to [0..1] or [-1..1] (depending on
        /// <see cref="normalizeZero"/>).
        /// </remarks>
        public float normalizeMax;

        /// <summary>
        /// Where to put the zero point of the normalization range. Only relevant
        /// if <see cref="normalize"/> is set to true. Defaults to 0.
        /// </summary>
        /// <value>Zero point of normalization range.</value>
        /// <remarks>
        /// The value of this property determines where the zero point is located in the
        /// range established by <see cref="normalizeMin"/> and <see cref="normalizeMax"/>.
        ///
        /// If <c>normalizeZero</c> is placed at <see cref="normalizeMin"/>, the normalization
        /// returns a value in the [0..1] range mapped from the input value range of
        /// <see cref="normalizeMin"/> and <see cref="normalizeMax"/>.
        ///
        /// If <c>normalizeZero</c> is placed in-between <see cref="normalizeMin"/> and
        /// <see cref="normalizeMax"/>, normalization returns a value in the [-1..1] mapped
        /// from the input value range of <see cref="normalizeMin"/> and <see cref="normalizeMax"/>
        /// and the zero point between the two established by <c>normalizeZero</c>.
        /// </remarks>
        public float normalizeZero;

        ////REVIEW: why not just have a default scaleFactor of 1?

        /// <summary>
        /// Whether the scale the input value by <see cref="scaleFactor"/>. Off by default.
        /// </summary>
        /// <value>True if inputs should be scaled by <see cref="scaleFactor"/>.</value>
        public bool scale;

        /// <summary>
        /// Value to multiple input values with. Only applied if <see cref="scale"/> is <c>true</c>.
        /// </summary>
        /// <value>Multiplier for input values.</value>
        public float scaleFactor;

        /// <summary>
        /// Apply modifications to the given value according to the parameters configured
        /// on the control (<see cref="clamp"/>, <see cref="normalize"/>, etc).
        /// </summary>
        /// <param name="value">Input value.</param>
        /// <returns>A processed value (clamped, normalized, etc).</returns>
        /// <seealso cref="clamp"/>
        /// <seealso cref="normalize"/>
        /// <seealso cref="scale"/>
        /// <seealso cref="invert"/>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected float Preprocess(float value)
        {
            if (scale)
                value *= scaleFactor;
            if (clamp == Clamp.ToConstantBeforeNormalize)
            {
                if (value < clampMin || value > clampMax)
                    value = clampConstant;
            }
            else if (clamp == Clamp.BeforeNormalize)
                value = Mathf.Clamp(value, clampMin, clampMax);
            if (normalize)
                value = NormalizeProcessor.Normalize(value, normalizeMin, normalizeMax, normalizeZero);
            if (clamp == Clamp.AfterNormalize)
                value = Mathf.Clamp(value, clampMin, clampMax);
            if (invert)
                value *= -1.0f;
            return value;
        }

        private float Unpreprocess(float value)
        {
            // Does not reverse the effect of clamping (we don't know what the unclamped value should be).

            if (invert)
                value *= -1f;
            if (normalize)
                value = NormalizeProcessor.Denormalize(value, normalizeMin, normalizeMax, normalizeZero);
            if (scale)
                value /= scaleFactor;
            return value;
        }

        /// <summary>
        /// Default-initialize the control.
        /// </summary>
        /// <remarks>
        /// Defaults the format to <see cref="InputStateBlock.FormatFloat"/>.
        /// </remarks>
        public AxisControl()
        {
            m_StateBlock.format = InputStateBlock.FormatFloat;
        }

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

            // if we don't have any default state, and we are using normalizeZero, then the default value
            // should not be zero. Generate it from normalizeZero.
            if (!hasDefaultState && normalize && Mathf.Abs(normalizeZero) > Mathf.Epsilon)
                m_DefaultState = stateBlock.FloatToPrimitiveValue(normalizeZero);
        }

        /// <inheritdoc />
        public override unsafe float ReadUnprocessedValueFromState(void* statePtr)
        {
            switch (m_OptimizedControlDataType)
            {
                case InputStateBlock.kFormatFloat:
                    return *(float*)((byte*)statePtr + m_StateBlock.m_ByteOffset);
                case InputStateBlock.kFormatByte:
                    return *((byte*)statePtr + m_StateBlock.m_ByteOffset) != 0 ? 1.0f : 0.0f;
                default:
                {
                    var value = stateBlock.ReadFloat(statePtr);
                    ////REVIEW: this isn't very raw
                    return Preprocess(value);
                }
            }
        }

        /// <inheritdoc />
        public override unsafe void WriteValueIntoState(float value, void* statePtr)
        {
            switch (m_OptimizedControlDataType)
            {
                case InputStateBlock.kFormatFloat:
                    *(float*)((byte*)statePtr + m_StateBlock.m_ByteOffset) = value;
                    break;
                case InputStateBlock.kFormatByte:
                    *((byte*)statePtr + m_StateBlock.m_ByteOffset) = (byte)(value >= 0.5f ? 1 : 0);
                    break;
                default:
                    value = Unpreprocess(value);
                    stateBlock.WriteFloat(statePtr, value);
                    break;
            }
        }

        /// <inheritdoc />
        public override unsafe bool CompareValue(void* firstStatePtr, void* secondStatePtr)
        {
            var currentValue = ReadValueFromState(firstStatePtr);
            var valueInState = ReadValueFromState(secondStatePtr);
            return !Mathf.Approximately(currentValue, valueInState);
        }

        /// <inheritdoc />
        public override unsafe float EvaluateMagnitude(void* statePtr)
        {
            return EvaluateMagnitude(ReadValueFromStateWithCaching(statePtr));
        }

        private float EvaluateMagnitude(float value)
        {
            if (m_MinValue.isEmpty || m_MaxValue.isEmpty)
                return Mathf.Abs(value);

            var min = m_MinValue.ToSingle();
            var max = m_MaxValue.ToSingle();

            var clampedValue = Mathf.Clamp(value, min, max);

            // If part of our range is in negative space, evaluate magnitude as two
            // separate subspaces.
            if (min < 0)
            {
                if (clampedValue < 0)
                    return NormalizeProcessor.Normalize(Mathf.Abs(clampedValue), 0, Mathf.Abs(min), 0);
                return NormalizeProcessor.Normalize(clampedValue, 0, max, 0);
            }

            return NormalizeProcessor.Normalize(clampedValue, min, max, 0);
        }

        protected override FourCC CalculateOptimizedControlDataType()
        {
            var noProcessingNeeded =
                clamp == Clamp.None &&
                invert == false &&
                normalize == false &&
                scale == false;

            if (noProcessingNeeded &&
                m_StateBlock.format == InputStateBlock.FormatFloat &&
                m_StateBlock.sizeInBits == 32 &&
                m_StateBlock.bitOffset == 0)
                return InputStateBlock.FormatFloat;
            if (noProcessingNeeded &&
                m_StateBlock.format == InputStateBlock.FormatBit &&
                // has to be 8, otherwise we might be mapping to a state which only represents first bit in the byte, while other bits might map to some other controls
                // like in the mouse where LMB/RMB map to the same byte, just LMB maps to first bit and RMB maps to second bit
                m_StateBlock.sizeInBits == 8 &&
                m_StateBlock.bitOffset == 0)
                return InputStateBlock.FormatByte;
            return InputStateBlock.FormatInvalid;
        }
    }
}