/// <summary>
        /// By calling this function, all references to LineInfo in "listToChange" passed in
        /// constructor can be changed. Retiming includes removing/introducing commercial breaks.
        ///
        /// By using the "noSplitting=true" option, the whole list gets shifted as one.
        /// </summary>
        public void Retime(bool noSplitting = false)
        {
            var stopwatch = new System.Diagnostics.Stopwatch();

            stopwatch.Start();

            var queue = new Queue <RemainingSlice>();

            // initilize first slice (with all lines)
            RemainingSlice fullSlice = new RemainingSlice(new List <LineInfo>(m_referenceList), new List <LineInfo>(m_listToChange));

            queue.Enqueue(fullSlice);

            // just find best offset
            if (noSplitting)
            {
                double bestOffset = FindBestOffset(fullSlice);
                UtilsSubtitle.ShiftByTime(m_listToChange, bestOffset);
                return;
            }


            // Only lines that are in the same slice (list of lines on both sides) can be matched.
            // When processing the slices, a consecutive row of lines is recognized as "good". Two new
            // slices (one left of the right of the row) are then created.
            //
            // This queue will process all these slices that have some elements in them.
            while (queue.Count > 0)
            {
                RemainingSlice slice = queue.Dequeue();
                //if(slice.listToChangeLines.Count == 0 || slice.referenceListLines.Count == 0) {
                //	// T O D O: these shouldn't just be moved out of the way but left in another slice
                //	// this is more debug code than anything else
                //	UtilsSubtitle.ShiftByTime(slice.listToChangeLines, 10000);
                //	UtilsSubtitle.ShiftByTime(slice.referenceListLines, 10000);
                //	continue;
                //}

                double bestOffset = FindBestOffset(slice);
                ApplyOffset(slice, bestOffset, queue);
            }

            Console.WriteLine(stopwatch.ElapsedMilliseconds);
        }
        /// <summary>
        /// Find best offset for "listToChange" so it matches "referenceList". There can be
        /// multiple "peaks" if one subtitle has additional breaks the other does not have.
        /// Only one of them will be returned.
        /// </summary>
        private double FindBestOffset(RemainingSlice slice)
        {
            var subtitleMatcherParams = SubtitleMatcher.GetParameterCache(slice.referenceListLines, slice.listToChangeLines);

            // the tuple is (offset, rating)
            var firstPassList = new List <OffsetRatingTuple>(5);

            FindGoodOffsets(subtitleMatcherParams, 0, 0.3, 1000, firstPassList);

            // find fine grained offsets around approximated offsets
            var secondPassList = new List <OffsetRatingTuple>(5);

            foreach (var offsetRatingTuple in firstPassList)
            {
                FindGoodOffsets(subtitleMatcherParams, offsetRatingTuple.offset, 0.01, 90, secondPassList);
            }

            return(secondPassList[0].offset);
        }
        /// <summary>
        /// This function will...
        /// a) move all lines to change in slice by offset
        /// b) match the lines in "listToChange" and "referenceList"
        /// c) find a threshold value for matchings
        /// d) find the longest row of consecutive matchings that are above the threshold
        /// e) create slices for lines before and lines after this "good row"
        /// f) reset offset of these remaining lines and put slice into queue
        /// </summary>
        private void ApplyOffset(RemainingSlice slice, double offset, Queue <RemainingSlice> queue)
        {
            UtilsSubtitle.ShiftByTime(slice.listToChangeLines, offset);

            // match lines
            var subtitleMatcherParameters = SubtitleMatcher.GetParameterCache(slice.referenceListLines, slice.listToChangeLines);
            var biMatchedLinesLinkedList  = SubtitleMatcher.MatchSubtitles(subtitleMatcherParameters);
            var biMatchedLinesList        = new List <SubtitleMatcher.BiMatchedLines>(biMatchedLinesLinkedList);

            biMatchedLinesList.Sort(delegate(SubtitleMatcher.BiMatchedLines x, SubtitleMatcher.BiMatchedLines y) {
                return(GetStartTime(x) < GetStartTime(y) ? -1 : 1);
            });


            // --------------------------------------------------
            // find threshold rating
            double averageRating = 0;
            int    numRatings    = 0;

            foreach (var biMatchedLines in biMatchedLinesList)
            {
                double rating = RateBiMatchedLines(biMatchedLines);
                if (rating > 0.0001)                  // ignore "zero" ratings
                {
                    averageRating += rating;
                    numRatings++;
                }
            }
            averageRating /= numRatings;
            double thresholdValue = averageRating * 0.8;

            // --------------------------------------------------
            // Find longest row over threshold rating.
            //
            // Zero ratings may be inbetween good ratings when some
            // lines couldn't get matched (for example street-sign
            // translations that aren't in subtitle file in native language).
            // These are stepped over: There can be a limited number of zero ratings
            // ratings in the row except at the beginning and the end (these will
            // get matched at a different time if possible).
            int numGoodMatched     = 0;
            int currentRowStart    = 0;
            int allowedZeroRatings = 0;


            int maxNumGoodMatched = -1;
            int bestRowStart      = 0;

            for (int index = 0; index < biMatchedLinesList.Count; index++)
            {
                var    biMatchedLines = biMatchedLinesList[index];
                double rating         = RateBiMatchedLines(biMatchedLines);

                if (rating < thresholdValue)
                {
                    // step over zero ratings
                    if (rating > 0.000001 || allowedZeroRatings-- < 0)
                    {
                        numGoodMatched = 0;                         // not a zero rating
                    }
                }
                else
                {
                    // update row start/end
                    if (numGoodMatched == 0)
                    {
                        currentRowStart = index;
                    }
                    numGoodMatched = index - currentRowStart + 1;

                    // save best value
                    if (numGoodMatched > maxNumGoodMatched)
                    {
                        maxNumGoodMatched = numGoodMatched;
                        bestRowStart      = currentRowStart;
                    }

                    allowedZeroRatings = 4;
                }
            }

            // could good row be found?
            if (maxNumGoodMatched == -1)
            {
                return;
            }

            // create new slices left and right
            RemainingSlice newSlice;

            // left slice
            newSlice = GetSubSlice(biMatchedLinesList, 0, bestRowStart);
            //UtilsSubtitle.ShiftByTime(newSlice.listToChangeLines, -offset); // by disabling this code, remaining slices are now embedded in greater slices
            queue.Enqueue(newSlice);

            // right slice
            newSlice = GetSubSlice(biMatchedLinesList, bestRowStart + maxNumGoodMatched + 1, biMatchedLinesList.Count);
            //UtilsSubtitle.ShiftByTime(newSlice.listToChangeLines, -offset);
            queue.Enqueue(newSlice);
        }