// 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.Collections.Generic;
using System.IO;
using System.Text;
using System.Web.Razor.Resources;
using System.Web.Razor.Utils;

namespace System.Web.Razor.Text
{
    public class BufferingTextReader : LookaheadTextReader
    {
        private Stack<BacktrackContext> _backtrackStack = new Stack<BacktrackContext>();
        private int _currentBufferPosition;

        private int _currentCharacter;
        private SourceLocationTracker _locationTracker;

        public BufferingTextReader(TextReader source)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            InnerReader = source;
            _locationTracker = new SourceLocationTracker();

            UpdateCurrentCharacter();
        }

        internal StringBuilder Buffer { get; set; }
        internal bool Buffering { get; set; }
        internal TextReader InnerReader { get; private set; }

        public override SourceLocation CurrentLocation
        {
            get { return _locationTracker.CurrentLocation; }
        }

        protected virtual int CurrentCharacter
        {
            get { return _currentCharacter; }
        }

        public override int Read()
        {
            int ch = CurrentCharacter;
            NextCharacter();
            return ch;
        }

        // TODO: Optimize Read(char[],int,int) to copy direct from the buffer where possible

        public override int Peek()
        {
            return CurrentCharacter;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                InnerReader.Dispose();
            }
            base.Dispose(disposing);
        }

        public override IDisposable BeginLookahead()
        {
            // Is this our first lookahead?
            if (Buffer == null)
            {
                // Yes, setup the backtrack buffer
                Buffer = new StringBuilder();
            }

            if (!Buffering)
            {
                // We're not already buffering, so we need to expand the buffer to hold the first character
                ExpandBuffer();
                Buffering = true;
            }

            // Mark the position to return to when we backtrack
            // Use the closures and the "using" statement rather than an explicit stack
            BacktrackContext context = new BacktrackContext()
            {
                BufferIndex = _currentBufferPosition,
                Location = CurrentLocation
            };
            _backtrackStack.Push(context);
            return new DisposableAction(() =>
            {
                EndLookahead(context);
            });
        }

        // REVIEW: This really doesn't sound like the best name for this...
        public override void CancelBacktrack()
        {
            if (_backtrackStack.Count == 0)
            {
                throw new InvalidOperationException(RazorResources.CancelBacktrack_Must_Be_Called_Within_Lookahead);
            }
            // Just pop the current backtrack context so that when the lookahead ends, it won't be backtracked
            _backtrackStack.Pop();
        }

        private void EndLookahead(BacktrackContext context)
        {
            // If the specified context is not the one on the stack, it was popped by a call to DoNotBacktrack
            if (_backtrackStack.Count > 0 && ReferenceEquals(_backtrackStack.Peek(), context))
            {
                _backtrackStack.Pop();
                _currentBufferPosition = context.BufferIndex;
                _locationTracker.CurrentLocation = context.Location;

                UpdateCurrentCharacter();
            }
        }

        protected virtual void NextCharacter()
        {
            int prevChar = CurrentCharacter;
            if (prevChar == -1)
            {
                return; // We're at the end of the source
            }

            if (Buffering)
            {
                if (_currentBufferPosition >= Buffer.Length - 1)
                {
                    // If there are no more lookaheads (thus no need to continue with the buffer) we can just clean up the buffer
                    if (_backtrackStack.Count == 0)
                    {
                        // Reset the buffer
                        Buffer.Length = 0;
                        _currentBufferPosition = 0;
                        Buffering = false;
                    }
                    else if (!ExpandBuffer())
                    {
                        // Failed to expand the buffer, because we're at the end of the source
                        _currentBufferPosition = Buffer.Length; // Force the position past the end of the buffer
                    }
                }
                else
                {
                    // Not at the end yet, just advance the buffer pointer
                    _currentBufferPosition++;
                }
            }
            else
            {
                // Just act like normal
                InnerReader.Read(); // Don't care about the return value, Peek() is used to get characters from the source
            }

            UpdateCurrentCharacter();
            _locationTracker.UpdateLocation((char)prevChar, (char)CurrentCharacter);
        }

        protected bool ExpandBuffer()
        {
            // Pull another character into the buffer and update the position
            int ch = InnerReader.Read();

            // Only append the character to the buffer if there actually is one
            if (ch != -1)
            {
                Buffer.Append((char)ch);
                _currentBufferPosition = Buffer.Length - 1;
                return true;
            }
            return false;
        }

        private void UpdateCurrentCharacter()
        {
            if (Buffering && _currentBufferPosition < Buffer.Length)
            {
                // Read from the buffer
                _currentCharacter = (int)Buffer[_currentBufferPosition];
            }
            else
            {
                // No buffer? Peek from the source
                _currentCharacter = InnerReader.Peek();
            }
        }

        private class BacktrackContext
        {
            public int BufferIndex { get; set; }
            public SourceLocation Location { get; set; }
        }
    }
}
