// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Microsoft.AspNetCore.Razor.Language
{
    internal class DefaultRazorSourceLineCollection : RazorSourceLineCollection
    {
        private readonly RazorSourceDocument _document;
        private readonly int[] _lineStarts;

        public DefaultRazorSourceLineCollection(RazorSourceDocument document)
        {
            _document = document;
            _lineStarts = GetLineStarts();
        }

        public override int Count => _lineStarts.Length;

        public override int GetLineLength(int index)
        {
            if (index < 0 || index >= _lineStarts.Length)
            {
                throw new IndexOutOfRangeException(nameof(index));
            }

            if (index == _lineStarts.Length - 1)
            {
                // Last line is special.
                return _document.Length - _lineStarts[index];
            }

            return _lineStarts[index + 1] - _lineStarts[index];
        }

        internal override SourceLocation GetLocation(int position)
        {
            if (position < 0 || position >= _document.Length)
            {
                throw new IndexOutOfRangeException(nameof(position));
            }

            var index = Array.BinarySearch<int>(_lineStarts, position);
            if (index >= 0)
            {
                // We have an exact match for the start of a line.
                Debug.Assert(_lineStarts[index] == position);

                return new SourceLocation(_document.FileName, position, index, characterIndex: 0);
            }


            // Index is the complement of the line *after* the one we want, because BinarySearch tells
            // us where we'd put position *if* it were the start of a line.
            index = (~index) - 1;
            if (index == -1)
            {
                // There's no preceding line, so it's based on the start of the string
                return new SourceLocation(_document.FileName, position, 0, position);
            }
            else
            {
                var characterIndex = position - _lineStarts[index];
                return new SourceLocation(_document.FileName, position, index, characterIndex);
            }
        }

        private int[] GetLineStarts()
        {
            var starts = new List<int>();

            // We always consider a document to have at least a 0th line, even if it's empty.
            starts.Add(0);

            var unprocessedCR = false;

            // Length - 1 because we don't care if there was a linebreak as the last character.
            var length = _document.Length;
            for (var i = 0; i < length - 1; i++)
            {
                var c = _document[i];
                var isLineBreak = false;

                switch (c)
                {
                    case '\r':
                        unprocessedCR = true;
                        continue;

                    case '\n':
                        unprocessedCR = false;
                        isLineBreak = true;
                        break;

                    case '\u0085':
                    case '\u2028':
                    case '\u2029':
                        isLineBreak = true;
                        break;

                }

                if (unprocessedCR)
                {
                    // If we get here it means that we had a CR followed by something other than an LF.
                    // Add the CR as a line break.
                    starts.Add(i);
                    unprocessedCR = false;
                }

                if (isLineBreak)
                {
                    starts.Add(i + 1);
                }
            }

            return starts.ToArray();
        }
    }
}
