// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE

using OpenTK;
using osu.Game.Beatmaps;
using osu.Game.Modes.Objects;
using osu.Game.Modes.Osu.Objects;
using osu.Game.Modes.Osu.Objects.Drawables;
using System.Collections.Generic;
using osu.Game.Modes.Objects.Types;
using System.Linq;

namespace osu.Game.Modes.Osu.Beatmaps
{
    internal class OsuBeatmapConverter : IBeatmapConverter<OsuHitObject>
    {
        public Beatmap<OsuHitObject> Convert(Beatmap original)
        {
            return new Beatmap<OsuHitObject>(original)
            {
                HitObjects = convertHitObjects(original.HitObjects, original.BeatmapInfo?.StackLeniency ?? 0.7f)
            };
        }

        private List<OsuHitObject> convertHitObjects(List<HitObject> hitObjects, float stackLeniency)
        {
            List<OsuHitObject> converted = hitObjects.Select(convertHitObject).ToList();

            updateStacking(converted, stackLeniency);

            return converted;
        }

        private OsuHitObject convertHitObject(HitObject original)
        {
            IHasCurve curveData = original as IHasCurve;
            IHasEndTime endTimeData = original as IHasEndTime;
            IHasPosition positionData = original as IHasPosition;
            IHasCombo comboData = original as IHasCombo;

            if (curveData != null)
            {
                return new Slider
                {
                    StartTime = original.StartTime,
                    Sample = original.Sample,
                    CurveObject = curveData,
                    Position = positionData?.Position ?? Vector2.Zero,
                    NewCombo = comboData?.NewCombo ?? false
                };
            }

            if (endTimeData != null)
            {
                return new Spinner
                {
                    StartTime = original.StartTime,
                    Sample = original.Sample,
                    Position = new Vector2(512, 384) / 2,
                    EndTime = endTimeData.EndTime
                };
            }

            return new HitCircle
            {
                StartTime = original.StartTime,
                Sample = original.Sample,
                Position = positionData?.Position ?? Vector2.Zero,
                NewCombo = comboData?.NewCombo ?? false
            };
        }

        private void updateStacking(List<OsuHitObject> hitObjects, float stackLeniency, int startIndex = 0, int endIndex = -1)
        {
            if (endIndex == -1)
                endIndex = hitObjects.Count - 1;

            const int stack_distance = 3;
            float stackThreshold = DrawableOsuHitObject.TIME_PREEMPT * stackLeniency;

            // Reset stacking inside the update range
            for (int i = startIndex; i <= endIndex; i++)
                hitObjects[i].StackHeight = 0;

            // Extend the end index to include objects they are stacked on
            int extendedEndIndex = endIndex;
            for (int i = endIndex; i >= startIndex; i--)
            {
                int stackBaseIndex = i;
                for (int n = stackBaseIndex + 1; n < hitObjects.Count; n++)
                {
                    OsuHitObject stackBaseObject = hitObjects[stackBaseIndex];
                    if (stackBaseObject is Spinner) break;

                    OsuHitObject objectN = hitObjects[n];
                    if (objectN is Spinner)
                        continue;

                    double endTime = (stackBaseObject as IHasEndTime)?.EndTime ?? stackBaseObject.StartTime;

                    if (objectN.StartTime - endTime > stackThreshold)
                        //We are no longer within stacking range of the next object.
                        break;

                    if (Vector2.Distance(stackBaseObject.Position, objectN.Position) < stack_distance ||
                        stackBaseObject is Slider && Vector2.Distance(stackBaseObject.EndPosition, objectN.Position) < stack_distance)
                    {
                        stackBaseIndex = n;

                        // HitObjects after the specified update range haven't been reset yet
                        objectN.StackHeight = 0;
                    }
                }

                if (stackBaseIndex > extendedEndIndex)
                {
                    extendedEndIndex = stackBaseIndex;
                    if (extendedEndIndex == hitObjects.Count - 1)
                        break;
                }
            }

            //Reverse pass for stack calculation.
            int extendedStartIndex = startIndex;
            for (int i = extendedEndIndex; i > startIndex; i--)
            {
                int n = i;
                /* We should check every note which has not yet got a stack.
                    * Consider the case we have two interwound stacks and this will make sense.
                    *
                    * o <-1      o <-2
                    *  o <-3      o <-4
                    *
                    * We first process starting from 4 and handle 2,
                    * then we come backwards on the i loop iteration until we reach 3 and handle 1.
                    * 2 and 1 will be ignored in the i loop because they already have a stack value.
                    */

                OsuHitObject objectI = hitObjects[i];
                if (objectI.StackHeight != 0 || objectI is Spinner) continue;

                /* If this object is a hitcircle, then we enter this "special" case.
                    * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
                    * Any other case is handled by the "is Slider" code below this.
                    */
                if (objectI is HitCircle)
                {
                    while (--n >= 0)
                    {
                        OsuHitObject objectN = hitObjects[n];
                        if (objectN is Spinner) continue;

                        double endTime = (objectN as IHasEndTime)?.EndTime ?? objectN.StartTime;

                        if (objectI.StartTime - endTime > stackThreshold)
                            //We are no longer within stacking range of the previous object.
                            break;

                        // HitObjects before the specified update range haven't been reset yet
                        if (n < extendedStartIndex)
                        {
                            objectN.StackHeight = 0;
                            extendedStartIndex = n;
                        }

                        /* This is a special case where hticircles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern.
                            *    o==o <- slider is at original location
                            *        o <- hitCircle has stack of -1
                            *         o <- hitCircle has stack of -2
                            */
                        if (objectN is Slider && Vector2.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
                        {
                            int offset = objectI.StackHeight - objectN.StackHeight + 1;
                            for (int j = n + 1; j <= i; j++)
                            {
                                //For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
                                OsuHitObject objectJ = hitObjects[j];
                                if (Vector2.Distance(objectN.EndPosition, objectJ.Position) < stack_distance)
                                    objectJ.StackHeight -= offset;
                            }

                            //We have hit a slider.  We should restart calculation using this as the new base.
                            //Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop.
                            break;
                        }

                        if (Vector2.Distance(objectN.Position, objectI.Position) < stack_distance)
                        {
                            //Keep processing as if there are no sliders.  If we come across a slider, this gets cancelled out.
                            //NOTE: Sliders with start positions stacking are a special case that is also handled here.

                            objectN.StackHeight = objectI.StackHeight + 1;
                            objectI = objectN;
                        }
                    }
                }
                else if (objectI is Slider)
                {
                    /* We have hit the first slider in a possible stack.
                        * From this point on, we ALWAYS stack positive regardless.
                        */
                    while (--n >= startIndex)
                    {
                        OsuHitObject objectN = hitObjects[n];
                        if (objectN is Spinner) continue;

                        if (objectI.StartTime - objectN.StartTime > stackThreshold)
                            //We are no longer within stacking range of the previous object.
                            break;

                        if (Vector2.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
                        {
                            objectN.StackHeight = objectI.StackHeight + 1;
                            objectI = objectN;
                        }
                    }
                }
            }
        }
    }
}