using System;
using ANX.Framework.NonXNA.Development;

// This file is part of the ANX.Framework created by the
// "ANX.Framework developer group" and released under the Ms-PL license.
// For details see: http://anxframework.codeplex.com/license

namespace ANX.Framework
{
#if !WINDOWSMETRO // TODO: find replacement for Win8!
    [Serializable]
#endif
    [PercentageComplete(100)]
    [Developer("floAr")]
    [TestState(TestStateAttribute.TestState.Tested)]
    public class Curve
    {
        public CurveLoopType PreLoop { get; set; }
        public CurveLoopType PostLoop { get; set; }
        public CurveKeyCollection Keys { get; private set; }

        public bool IsConstant
        {
            get { return Keys.Count <= 1; }
        }

        public Curve()
        {
            Keys = new CurveKeyCollection();
        }

        public Curve Clone()
        {
            return new Curve { Keys = Keys.Clone(), PreLoop = PreLoop, PostLoop = PostLoop };
        }

        #region ComputeTangent
        //formulas from: http://msdn.microsoft.com/de-de/library/microsoft.xna.framework.curvetangent%28v=xnagamestudio.40%29.aspx
        public void ComputeTangent(int index, CurveTangent tangentInOutType)
        {
            if (index < 0 || index >= Keys.Count)
            {
                throw new ArgumentOutOfRangeException();
            }
            CurveKey prev = index > 0 ? this.Keys[index - 1] : this.Keys[index];
            CurveKey current = this.Keys[index];
            current.TangentIn = 0;
            CurveKey next = index < this.Keys.Count - 1 ? this.Keys[index + 1] : this.Keys[index];


            switch (tangentInOutType)
            {
                case CurveTangent.Flat:
                    current.TangentIn = 0;
                    current.TangentOut = 0;
                    break;
                case CurveTangent.Linear:
                    current.TangentIn = current.Value - prev.Value;
                    current.TangentOut = next.Value - current.Value;
                    break;
                case CurveTangent.Smooth:
                    current.TangentIn = ((next.Value - prev.Value) * ((current.Position - prev.Position) / (next.Position - prev.Position)));
                    current.TangentOut = ((next.Value - prev.Value) * ((next.Position - current.Position) / (next.Position - prev.Position)));
                    break;
            }
        }

        public void ComputeTangent(int index, CurveTangent tangentInType, CurveTangent tangentOutType)
        {
            if (index < 0 || index >= Keys.Count)
            {
                throw new ArgumentOutOfRangeException();
            }

            CurveKey prev = index > 0 ? this.Keys[index - 1] : this.Keys[index];
            CurveKey current = this.Keys[index];
            current.TangentIn = 0;
            CurveKey next = index < this.Keys.Count - 1 ? this.Keys[index + 1] : this.Keys[index];


            switch (tangentInType)
            {
                case CurveTangent.Flat:
                    current.TangentIn = 0;
                    break;
                case CurveTangent.Linear:
                    current.TangentIn = current.Value - prev.Value;
                    break;
                case CurveTangent.Smooth:
                    current.TangentIn = ((next.Value - prev.Value) * ((current.Position - prev.Position) / (next.Position - prev.Position)));
                    break;
            }
            switch (tangentOutType)
            {
                case CurveTangent.Flat:
                    current.TangentOut = 0;
                    break;
                case CurveTangent.Linear:
                    current.TangentOut = next.Value - current.Value;
                    break;
                case CurveTangent.Smooth:
                    current.TangentOut = ((next.Value - prev.Value) * ((next.Position - current.Position) / (next.Position - prev.Position)));
                    break;
            }

        }

        public void ComputeTangents(CurveTangent tangentInOutType)
        {
            for (int i = 0; i < this.Keys.Count; ++i)
            {
                this.ComputeTangent(i, tangentInOutType);
            }
        }

        public void ComputeTangents(CurveTangent tangentInType, CurveTangent tangentOutType)
        {
            for (int i = 0; i < this.Keys.Count; ++i)
            {
                this.ComputeTangent(i, tangentInType, tangentOutType);
            }
        }
        #endregion

        public float Evaluate(float position)
        {
            if (Keys.Count == 0)
                return 0f;

            if (Keys.Count == 1)
                return Keys[0].Value;

            CurveKey firstPointOfCurve = Keys[0];
            CurveKey lastPointOfCurve = Keys[Keys.Count - 1];

            if (position < firstPointOfCurve.Position)
                return EvalualtePreLoop(firstPointOfCurve, lastPointOfCurve, position);

            if (position > lastPointOfCurve.Position)
                return EvalualtePostLoop(firstPointOfCurve, lastPointOfCurve, position);

            return Interpolate(position);
        }

        #region EvalualtePreLoop
        private float EvalualtePreLoop(CurveKey first, CurveKey last, float position)
        {
            int wraps;
            float firstPosition = first.Position;
            float lastPosition = last.Position;
            // tspan is min 1 to avoid deadlock at constant curves
            float timeSpan = Math.Max(1, lastPosition - firstPosition);

            // Description from : http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.curvelooptype.aspx
            switch (PreLoop)
            {
                case CurveLoopType.Constant:
                    // The Curve will evaluate to its first key for positions before the first point in the Curve and to
                    // the last key for positions after the last point.
                    return first.Value;

                case CurveLoopType.Linear:
                    // Linear interpolation will be performed to determine the value.
                    return first.Value - first.TangentIn * (first.Position - position);

                case CurveLoopType.Cycle:
                    // Positions specified past the ends of the curve will wrap around to the opposite side of the Curve.
                    position -= timeSpan * CountWraps(position);
                    return Interpolate(position);

                case CurveLoopType.CycleOffset:
                    // Positions specified past the ends of the curv will wrap around to the opposite side of the Curve. 
                    // The value will be offset by the difference between the values of the first and last CurveKey 
                    // multiplied by the number of times the position wraps around. If the position is before the first 
                    // point in the Curve, the difference will be subtracted from its value; otherwise, the difference 
                    // will be added
                    wraps = CountWraps(position);
                    float difference = (last.Value - first.Value) * wraps;
                    position -= timeSpan * CountWraps(position);
                    return Interpolate(position) + difference;

                case CurveLoopType.Oscillate:
                    // Positions specified past the ends of the Curve act as an offset from the same side of the Curve 
                    // toward the opposite side.
                    wraps = CountWraps(position);
                    float offset = position - (first.Position + wraps * timeSpan);
                    float wrappedPosition = (wraps & 1) != 0 ? (last.Position - offset) : (first.Position + offset);
                    return Interpolate(wrappedPosition);
            }

            return Interpolate(position);
        }
        #endregion

        #region EvalualtePostLoop
        private float EvalualtePostLoop(CurveKey first, CurveKey last, float position)
        {
            int wraps;
            float firstPosition = first.Position;
            float lastPosition = last.Position;
            //tspan is min 1 to avoid deadlock at constant curves
            float timeSpan = Math.Max(1, lastPosition - firstPosition);

            switch (PostLoop)
            {
                case CurveLoopType.Constant:
                    // The Curve will evaluate to its first key for positions before the first point in the Curve and to 
                    // the last key for positions after the last point.
                    return last.Value;

                case CurveLoopType.Linear:
                    // Linear interpolation will be performed to determine the value.
                    return last.Value - last.TangentOut * (last.Position - position);

                case CurveLoopType.Cycle:
                    // Positions specified past the ends of the curve will wrap around to the opposite side of the Curve.
                    position -= timeSpan * CountWraps(position);
                    return Interpolate(position);

                case CurveLoopType.CycleOffset:
                    // Positions specified past the ends of the curve will wrap around to the opposite side of the Curve. 
                    // The value will be offset by the difference between the values of the first and last CurveKey 
                    // multiplied by the number of times the position wraps around. If the position is before the first 
                    // point in the Curve, the difference will be subtracted from its value; otherwise, the difference 
                    // will be added. 
                    wraps = CountWraps(position);
                    float difference = (last.Value - first.Value) * wraps;
                    position -= timeSpan * CountWraps(position);
                    return Interpolate(position) + difference;

                case CurveLoopType.Oscillate:
                    // Positions specified past the ends of the Curve act as an offset from the same side of the Curve 
                    // toward the opposite side.
                    wraps = CountWraps(position);
                    float offset = position - (first.Position + wraps * timeSpan);
                    float wrappedPosition = (wraps & 1) != 0 ? (last.Position - offset) : (first.Position + offset);
                    return Interpolate(wrappedPosition);
            }

            return Interpolate(position);
        }
        #endregion

        private int CountWraps(float position)
        {
            float timeRange = Keys[Keys.Count - 1].Position - Keys[0].Position;
            float wraps = (position - Keys[0].Position) / timeRange;
            if (wraps < 0)
                wraps--;

            return (int)wraps;
        }

        private float Interpolate(float position)
        {
            //interpolate inside the curve with cubic hermite: http://forums.create.msdn.com/forums/p/53392/323814.aspx

            //assume position is inside curve
            CurveKey a = Keys[0];
            CurveKey b;
            for (int nextKeyIndex = 1; nextKeyIndex < Keys.Count; nextKeyIndex++)
            {
                b = Keys[nextKeyIndex];
                if (b.Position >= position)
                {
                    //stepping 
                    if (a.Continuity == CurveContinuity.Step)
                        return position == b.Position ? b.Value : a.Value;

                    //smooth
                    //get the location between a and b in [0,1]
                    float moment = (position - a.Position) / (b.Position - a.Position);
                    return MathHelper.Hermite(a.Value, a.TangentOut, b.Value, b.TangentOut, moment);

                }
                //get next pair
                a = b;
            }
            return 0f;
        }
    }
}