using System;

// 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
{
    public class Curve
    {
        private CurveKeyCollection keys;
        private CurveLoopType preLoop;
        private CurveLoopType postLoop;

        public CurveLoopType PreLoop
        {
            get { return preLoop; }
            set { preLoop = value; }
        }
        public CurveLoopType PostLoop
        {
            get { return postLoop; }
            set { postLoop = value; }
        }

        public CurveKeyCollection Keys
        {
            get
            {
                return keys;
            }
        }

        public Boolean IsConstant
        {
            get
            {
                return this.keys.Count <= 1;
            }
        }

        public Curve Clone()
        {
            Curve result = new Curve();
            result.keys = this.keys.Clone();
            result.preLoop = this.preLoop;
            result.postLoop = this.postLoop;
            return result;
        }
        public Curve()
        {
            this.keys = new CurveKeyCollection();
        }

        #region tangent calculation
        //formulas from: http://msdn.microsoft.com/de-de/library/microsoft.xna.framework.curvetangent%28v=xnagamestudio.40%29.aspx
        public void ComputeTangent(Int32 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(Int32 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 Single Evaluate(Single position)
        {
            int wraps;
            //Get first and last Point
            CurveKey first = keys[0];
            float firstPosition = first.Position;
            CurveKey last = keys[keys.Count - 1];
            float lastPosition = last.Position;
            //tspan is min 1 to avoid deadlock at constant curves
            float timeSpan = Math.Max(1, lastPosition - firstPosition);
            //wanted point before first point
            if (position < first.Position)
            {
                // Description from : http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.curvelooptype.aspx
                switch (this.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 this.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 = (this.keys[this.keys.Count - 1].Value - this.keys[0].Value) * wraps;

                        position -= timeSpan * countWraps(position);

                        return this.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);

                        if (wraps % 2 == 1)
                        {
                            return this.interpolate(lastPosition - position + firstPosition - (wraps * timeSpan));
                        }
                        return this.interpolate(position + (wraps * timeSpan));
                }
            }
            //wanted point behind last point
            else if (position > last.Position)
            {
                switch (this.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 * (position - last.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 this.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 = (this.keys[this.keys.Count - 1].Value - this.keys[0].Value) * wraps;

                        position -= timeSpan * countWraps(position);

                        return this.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);
                        position += timeSpan * wraps;
                        if (wraps % 2 == 1)
                        {
                            return this.interpolate(lastPosition - position + firstPosition - (wraps * timeSpan));
                        }
                        return this.interpolate(position + (wraps * timeSpan));
                }
            }

            //in curve
            return interpolate(position);

        }
        private int countWraps(float position)
        {
            float wraps = (position - keys[0].Position) / (keys[keys.Count - 1].Position - keys[0].Position);
            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 = this.keys[0];
            CurveKey b;
            for (int i = 1; i < this.keys.Count; ++i)
            {
                b = this.Keys[i];
                if (b.Position >= position)
                {
                    //stepping 
                    if (a.Continuity == CurveContinuity.Step)
                    {
                        if (position == b.Position)
                        {
                            return b.Position;
                        }
                        return 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;
        }

    }
}