// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;

/// <summary>
/// <para>
/// A <see cref="TextWriter"/> that is backed by a unbuffered writer (over the Response stream) and/or a
/// <see cref="ViewBuffer"/>
/// </para>
/// <para>
/// When <c>Flush</c> or <c>FlushAsync</c> is invoked, the writer copies all content from the buffer to
/// the writer and switches to writing to the unbuffered writer for all further write operations.
/// </para>
/// </summary>
internal sealed class ViewBufferTextWriter : TextWriter
{
    private readonly TextWriter _inner;
    private readonly HtmlEncoder _htmlEncoder;

    /// <summary>
    /// Creates a new instance of <see cref="ViewBufferTextWriter"/>.
    /// </summary>
    /// <param name="buffer">The <see cref="ViewBuffer"/> for buffered output.</param>
    /// <param name="encoding">The <see cref="System.Text.Encoding"/>.</param>
    public ViewBufferTextWriter(ViewBuffer buffer, Encoding encoding)
    {
        ArgumentNullException.ThrowIfNull(buffer);
        ArgumentNullException.ThrowIfNull(encoding);

        Buffer = buffer;
        Encoding = encoding;
    }

    /// <summary>
    /// Creates a new instance of <see cref="ViewBufferTextWriter"/>.
    /// </summary>
    /// <param name="buffer">The <see cref="ViewBuffer"/> for buffered output.</param>
    /// <param name="encoding">The <see cref="System.Text.Encoding"/>.</param>
    /// <param name="htmlEncoder">The HTML encoder.</param>
    /// <param name="inner">
    /// The inner <see cref="TextWriter"/> to write output to when this instance is no longer buffering.
    /// </param>
    public ViewBufferTextWriter(ViewBuffer buffer, Encoding encoding, HtmlEncoder htmlEncoder, TextWriter inner)
    {
        ArgumentNullException.ThrowIfNull(buffer);
        ArgumentNullException.ThrowIfNull(encoding);
        ArgumentNullException.ThrowIfNull(htmlEncoder);
        ArgumentNullException.ThrowIfNull(inner);

        Buffer = buffer;
        Encoding = encoding;
        _htmlEncoder = htmlEncoder;
        _inner = inner;
    }

    /// <inheritdoc />
    public override Encoding Encoding { get; }

    /// <summary>
    /// Gets the <see cref="ViewBuffer"/>.
    /// </summary>
    public ViewBuffer Buffer { get; }

    /// <summary>
    /// Gets a value that indicates if <see cref="Flush"/> or <see cref="FlushAsync" /> was invoked.
    /// </summary>
    public bool Flushed { get; private set; }

    /// <inheritdoc />
    public override void Write(char value)
    {
        Buffer.AppendHtml(value.ToString());
    }

    /// <inheritdoc />
    public override void Write(char[] buffer, int index, int count)
    {
        ArgumentNullException.ThrowIfNull(buffer);
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        ArgumentOutOfRangeException.ThrowIfNegative(count);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(count, buffer.Length - index);

        Buffer.AppendHtml(new string(buffer, index, count));
    }

    /// <inheritdoc />
    public override void Write(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return;
        }

        Buffer.AppendHtml(value);
    }

    /// <inheritdoc />
    public override void Write(object value)
    {
        if (value == null)
        {
            return;
        }

        if (value is IHtmlContentContainer container)
        {
            Write(container);
        }
        else if (value is IHtmlContent htmlContent)
        {
            Write(htmlContent);
        }
        else
        {
            Write(value.ToString());
        }
    }

    /// <summary>
    /// Writes an <see cref="IHtmlContent"/> value.
    /// </summary>
    /// <param name="value">The <see cref="IHtmlContent"/> value.</param>
    public void Write(IHtmlContent value)
    {
        if (value == null)
        {
            return;
        }

        Buffer.AppendHtml(value);
    }

    /// <summary>
    /// Writes an <see cref="IHtmlContentContainer"/> value.
    /// </summary>
    /// <param name="value">The <see cref="IHtmlContentContainer"/> value.</param>
    public void Write(IHtmlContentContainer value)
    {
        if (value == null)
        {
            return;
        }

        value.MoveTo(Buffer);
    }

    /// <inheritdoc />
    public override void WriteLine(object value)
    {
        if (value == null)
        {
            return;
        }

        if (value is IHtmlContentContainer container)
        {
            Write(container);
            Write(NewLine);
        }
        else if (value is IHtmlContent htmlContent)
        {
            Write(htmlContent);
            Write(NewLine);
        }
        else
        {
            Write(value.ToString());
            Write(NewLine);
        }
    }

    /// <inheritdoc />
    public override Task WriteAsync(char value)
    {
        Buffer.AppendHtml(value.ToString());
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public override Task WriteAsync(char[] buffer, int index, int count)
    {
        ArgumentNullException.ThrowIfNull(buffer);
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        if (count < 0 || (buffer.Length - index < count))
        {
            throw new ArgumentOutOfRangeException(nameof(count));
        }

        Buffer.AppendHtml(new string(buffer, index, count));
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public override Task WriteAsync(string value)
    {
        Buffer.AppendHtml(value);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public override void WriteLine()
    {
        Buffer.AppendHtml(NewLine);
    }

    /// <inheritdoc />
    public override void WriteLine(string value)
    {
        Buffer.AppendHtml(value);
        Buffer.AppendHtml(NewLine);
    }

    /// <inheritdoc />
    public override Task WriteLineAsync(char value)
    {
        Buffer.AppendHtml(value.ToString());
        Buffer.AppendHtml(NewLine);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public override Task WriteLineAsync(char[] value, int start, int offset)
    {
        Buffer.AppendHtml(new string(value, start, offset));
        Buffer.AppendHtml(NewLine);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public override Task WriteLineAsync(string value)
    {
        Buffer.AppendHtml(value);
        Buffer.AppendHtml(NewLine);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public override Task WriteLineAsync()
    {
        Buffer.AppendHtml(NewLine);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Copies the buffered content to the unbuffered writer and invokes flush on it.
    /// </summary>
    public override void Flush()
    {
        if (_inner == null || _inner is ViewBufferTextWriter)
        {
            return;
        }

        Flushed = true;

        Buffer.WriteTo(_inner, _htmlEncoder);
        Buffer.Clear();

        _inner.Flush();
    }

    /// <summary>
    /// Copies the buffered content to the unbuffered writer and invokes flush on it.
    /// </summary>
    /// <returns>A <see cref="Task"/> that represents the asynchronous copy and flush operations.</returns>
    public override async Task FlushAsync()
    {
        if (_inner == null || _inner is ViewBufferTextWriter)
        {
            return;
        }

        Flushed = true;

        await Buffer.WriteToAsync(_inner, _htmlEncoder);
        Buffer.Clear();

        await _inner.FlushAsync();
    }
}
