/// <returns>
        /// Returns a collection which is the projection of this collection onto the defined periodic function.
        /// </returns>
        /// <remarks>
        /// The object on which this method is called is a timeline's parent's collection of intervals.
        /// The periodic collection passed via parameters describes the active/fill periods of the timeline.
        /// The output is the projection of (this) object using the parameter function of the timeline.
        /// 
        /// We assume this function is ONLY called when this collection overlaps the active zone.
        /// 
        /// The periodic function maps values from domain to range within its activation period of [beginTime, endTime);
        /// in the fill period [endTime, endTime+fillDuration) everything maps to a constant post-fill value, and outside of
        /// those periods every value maps to null.
        /// 
        /// The projection process can be described as three major steps:
        /// 
        /// (1) NORMALIZE this collection: offset the TIC's coordinates by BeginTime and scale by SpeedRatio.
        /// 
        /// (2) FOLD this collection.  This means we convert from parent-time coordinate space into the space of
        ///     a single simpleDuration for the child.  This is equivalent to "cutting up" the parent TIC into
        ///     equal-length segments (of length Period) and overlapping them -- taking their union.  This lets us
        ///     know exactly which values inside the simpleDuration we have reached on the child.  In the case of
        ///     autoreversed timelines, we do the folding similiar to folding a strip of paper -- alternating direction.
        /// 
        /// (3) WARP the resulting collection.  We now convert from simpleDuration domain coordinates into
        ///     coordinates in the range of the timeline function.  We do this by applying the "warping" effects of
        ///     acceleration, and deceleration.
        /// 
        /// In the special case of infinite simple duration, we essentially are done after performing NORMALIZE,
        /// because no periodicity or acceleration is present.
        /// 
        /// In the ultimate degenerate case of zero duration, we terminate early and project the zero point.
        /// 
        /// </remarks>
        /// <param name="projection">An empty output projection, passed by reference to allow TIC reuse.</param>
        /// <param name="beginTime">Begin time of the periodic function.</param>
        /// <param name="endTime">The end (expiration) time of the periodic function.  Null indicates positive infinity.</param>
        /// <param name="fillDuration">The fill time appended at the end of the periodic function.  Zero indicates no fill period.  Forever indicates infinite fill period.</param>
        /// <param name="period">Length of a single iteration in the periodic collection.</param>
        /// <param name="appliedSpeedRatio">Ratio by which to scale down the periodic collection.</param>
        /// <param name="accelRatio">Ratio of the length of the accelerating portion of the iteration.</param>
        /// <param name="decelRatio">Ratio of the length of the decelerating portion of the iteration.</param>
        /// <param name="isAutoReversed">Indicates whether reversed arcs should follow after forward arcs.</param>
        internal void ProjectOntoPeriodicFunction(ref TimeIntervalCollection projection,
                                                  TimeSpan beginTime, Nullable<TimeSpan> endTime,
                                                  Duration fillDuration, Duration period,
                                                  double appliedSpeedRatio, double accelRatio, double decelRatio,
                                                  bool isAutoReversed)
        {
            Debug.Assert(projection.IsEmpty);
            Debug.Assert(!_invertCollection);  // Make sure we never leave inverted mode enabled
            Debug.Assert(!endTime.HasValue || beginTime <= endTime);     // Ensure legitimate begin/end clipping parameters

            Debug.Assert(!IsEmptyOfRealPoints);  // We assume this function is ONLY called when this collection overlaps the active zone.  So we cannot be empty.
            Debug.Assert(!endTime.HasValue || endTime >= _nodeTime[0]);  // EndTime must come at or after our first node (it can be infinite)
            Debug.Assert(_nodeTime[_count - 1] >= beginTime);  // Our last node must come at least at begin time (since we must intersect the active period)
            Debug.Assert(endTime.HasValue || fillDuration == TimeSpan.Zero);   // Either endTime is finite, or it's infinite hence we cannot have any fill zone
            Debug.Assert(!period.HasTimeSpan || period.TimeSpan > TimeSpan.Zero || (endTime.HasValue && beginTime == endTime));  // Check the consistency of degenerate case where simple duration is zero; expiration time should equal beginTime
            Debug.Assert(!_nodeIsInterval[_count - 1]);  // We should not have an infinite domain set

            // We initially project all intervals into a single period of the timeline, creating a union of the projected segments.
            // Then we warp the time coordinates of the resulting TIC from domain to range, applying the effects of speed/accel/decel

            bool nullPoint = _containsNullPoint  // Start by projecting the null point directly, then check whether we fall anywhere outside of the active and fill period

             || _nodeTime[0] < beginTime  // If we intersect space before beginTime, or...

              || (endTime.HasValue && fillDuration.HasTimeSpan  // ...the active and fill periods don't stretch forever, and...
               && (_nodeTime[_count - 1] > endTime.Value + fillDuration.TimeSpan  // ...we intersect space after endTime+fill, or...
                || (_nodeTime[_count - 1] == endTime.Value + fillDuration.TimeSpan  // ...as we fall right onto the end of fill zone...
                 && _nodeIsPoint[_count - 1] && (endTime > beginTime || fillDuration.TimeSpan > TimeSpan.Zero))));  // ...we may have a point intersection with the stopped zone

            // Now consider the main scenarios:

            if (endTime.HasValue && beginTime == endTime)  // Degenerate case when our active period is a single point; project only the point
            {
                projection.InitializePoint(TimeSpan.Zero);
            }
            else  // The case of non-zero active duration
            {
                bool includeFillPeriod = !fillDuration.HasTimeSpan || fillDuration.TimeSpan > TimeSpan.Zero;  // This variable represents whether we have a non-zero fill zone

                if (period.HasTimeSpan)  // We have a finite TimeSpan period and non-zero activation duration
                {
                    TimeIntervalCollection tempCollection = new TimeIntervalCollection();

                    ProjectionNormalize(ref tempCollection, beginTime, endTime, includeFillPeriod, appliedSpeedRatio);

                    long periodInTicks = period.TimeSpan.Ticks;
                    Nullable<TimeSpan> activeDuration;
                    bool includeMaxPoint;

                    if (endTime.HasValue)
                    {
                        activeDuration = endTime.Value - beginTime;
                        includeMaxPoint = includeFillPeriod && (activeDuration.Value.Ticks % periodInTicks == 0);  // Fill starts at a boundary
                    }
                    else
                    {
                        activeDuration = null;
                        includeMaxPoint = false;
                    }

                    projection.EnsureAllocatedCapacity(_minimumCapacity);
                    tempCollection.ProjectionFold(ref projection, activeDuration, periodInTicks, isAutoReversed, includeMaxPoint);

                    if (accelRatio + decelRatio > 0)
                    {
                        projection.ProjectionWarp(periodInTicks, accelRatio, decelRatio);
                    }
                }
                else  // Infinite period degenerate case; we perform straight 1-1 linear mapping, offset by begin time and clipped
                {
                    ProjectionNormalize(ref projection, beginTime, endTime, includeFillPeriod, appliedSpeedRatio);
                }
            }

            projection._containsNullPoint = nullPoint;  // Ensure we have the null point properly set
        }
        /// <summary>
        /// Used for projecting the end of a fill period.  When calling, we already know that we intersect the fill period
        /// but not the active period.
        /// </summary>
        /// <returns>
        /// Returns a collection which is the projection of the argument point onto the defined periodic function.
        /// </returns>
        /// <param name="projection">An empty output projection, passed by reference to allow TIC reuse.</param>
        /// <param name="beginTime">Begin time of the periodic function.</param>
        /// <param name="endTime">The end (expiration) time of the periodic function.</param>
        /// <param name="period">Length of a single iteration in the periodic collection.</param>
        /// <param name="appliedSpeedRatio">Ratio by which to scale down the periodic collection.</param>
        /// <param name="accelRatio">Ratio of the length of the accelerating portion of the iteration.</param>
        /// <param name="decelRatio">Ratio of the length of the decelerating portion of the iteration.</param>
        /// <param name="isAutoReversed">Indicates whether reversed arcs should follow after forward arcs.</param>
        internal void ProjectPostFillZone(ref TimeIntervalCollection projection,
                                          TimeSpan beginTime, TimeSpan endTime, Duration period,
                                          double appliedSpeedRatio, double accelRatio, double decelRatio,
                                          bool isAutoReversed)
        {
            Debug.Assert(projection.IsEmpty);  // Make sure the projection was properly cleared first
            Debug.Assert(!_invertCollection);  // Make sure we never leave inverted mode enabled
            Debug.Assert(beginTime <= endTime);     // Ensure legitimate begin/end clipping parameters

            Debug.Assert(!IsEmptyOfRealPoints);  // We assume this function is ONLY called when this collection overlaps the postfill zone.  So we cannot be empty.
            Debug.Assert(!period.HasTimeSpan || period.TimeSpan > TimeSpan.Zero || beginTime == endTime);  // Check the consistency of degenerate case where simple duration is zero; expiration time should equal beginTime
            Debug.Assert(!_nodeIsInterval[_count - 1]);  // We should not have an infinite domain set

            long outputInTicks;

            if (beginTime == endTime)  // Degenerate case when our active period is a single point; project only that point
            {
                outputInTicks = 0;
            }
            else  // The case of non-zero active duration
            {
                outputInTicks = (long)(appliedSpeedRatio * (double)(endTime - beginTime).Ticks);

                if (period.HasTimeSpan)  // Case of finite simple duration; in the infinite case we are already done
                {
                    long periodInTicks = period.TimeSpan.Ticks;  // Start by folding the point into its place inside a simple duration

                    if (isAutoReversed)
                    {
                        long doublePeriod = periodInTicks << 1;  // Fast multiply by 2
                        outputInTicks = outputInTicks % doublePeriod;

                        if (outputInTicks > periodInTicks)
                        {
                            outputInTicks = doublePeriod - outputInTicks;
                        }
                    }
                    else
                    {
                        outputInTicks = outputInTicks % periodInTicks;
                        if (outputInTicks == 0)
                        {
                            outputInTicks = periodInTicks;  // If we are at the end, stick to the max value
                        }
                    }

                    if (accelRatio + decelRatio > 0)  // Now if we have acceleration, warp the point by the correct amount
                    {
                        double dpPeriod = (double)periodInTicks;
                        double inversePeriod = 1 / dpPeriod;
                        double halfMaxRate = 1 / (2 - accelRatio - decelRatio);  // Constants to simplify 
                        double t;

                        long accelEnd = (long)(dpPeriod * accelRatio);
                        long decelStart = periodInTicks - (long)(dpPeriod * decelRatio);

                        if (outputInTicks < accelEnd)  // We are in accel zone
                        {
                            t = (double)outputInTicks;
                            outputInTicks = (long)(halfMaxRate * inversePeriod * t * t / accelRatio);
                        }
                        else if (outputInTicks <= decelStart)  // We are in the linear zone
                        {
                            t = (double)outputInTicks;
                            outputInTicks = (long)(halfMaxRate * (2 * t - accelRatio));
                        }
                        else  // We are in decel zone
                        {
                            t = (double)(periodInTicks - outputInTicks);
                            outputInTicks = periodInTicks - (long)(halfMaxRate * inversePeriod * t * t / decelRatio);
                        }
                    }
                }
            }

            projection.InitializePoint(TimeSpan.FromTicks(outputInTicks));
        }