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

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Microsoft.AspNetCore.Razor.Language
{
	/// <summary>
	/// The Razor template source.
	/// </summary>
	public abstract class RazorSourceDocument
	{
		internal const int LargeObjectHeapLimitInChars = 40 * 1024; // 40K Unicode chars is 80KB which is less than the large object heap limit.

		internal static readonly RazorSourceDocument[] EmptyArray = Array.Empty<RazorSourceDocument>();

		/// <summary>
		/// Gets the encoding of the text in the original source document.
		/// </summary>
		/// <remarks>
		/// Depending on the method used to create a <see cref="RazorSourceDocument"/> the encoding may be used to
		/// read the file contents, or it may be solely informational. Refer to the documentation on the method
		/// used to create the <see cref="RazorSourceDocument"/> for details.
		/// </remarks>
		public abstract Encoding Encoding { get; }

		/// <summary>
		/// Gets the file path of the orginal source document.
		/// </summary>
		/// <remarks>
		/// The file path may be either an absolute path or project-relative path. An absolute path is required
		/// to generate debuggable assemblies.
		/// </remarks>
		public abstract string FilePath { get; }

		/// <summary>
		/// Gets the project-relative path to the source file. May be <c>null</c>.
		/// </summary>
		/// <remarks>
		/// The relative path (if provided) is used for display (error messages). The project-relative path may also
		/// be used to embed checksums of the original source documents to support runtime recompilation of Razor code.
		/// </remarks>
		public virtual string RelativePath => null;

		/// <summary>
		/// Gets a character at given position.
		/// </summary>
		/// <param name="position">The position to get the character from.</param>
		public abstract char this[int position] { get; }

		/// <summary>
		/// Gets the length of the text in characters.
		/// </summary>
		public abstract int Length { get; }

		/// <summary>
		/// Gets the <see cref="RazorSourceLineCollection"/>.
		/// </summary>
		public abstract RazorSourceLineCollection Lines { get; }

		/// <summary>
		/// Copies a range of characters from the <see cref="RazorSourceDocument"/> to the specified <paramref name="destination"/>.
		/// </summary>
		/// <param name="sourceIndex">The index of the first character in this instance to copy.</param>
		/// <param name="destination">The destination buffer.</param>
		/// <param name="destinationIndex">The index in destination at which the copy operation begins.</param>
		/// <param name="count">The number of characters in this instance to copy to destination.</param>
		public abstract void CopyTo( int sourceIndex, char[] destination, int destinationIndex, int count );

		/// <summary>
		/// Calculates the checksum for the <see cref="RazorSourceDocument"/>.
		/// </summary>
		/// <returns>The checksum.</returns>
		public abstract byte[] GetChecksum();

		/// <summary>
		/// Gets the name of the algorithm used to compute the checksum returned by <see cref="GetChecksum"/>.
		/// </summary>
		/// <remarks>
		/// This member did not exist in the 2.0 release, so it is possible for an implementation to return
		/// the wrong value (or <c>null</c>). Implementations of <see cref="RazorSourceDocument"/> should
		/// override this member and specify their choice of hash algorithm even if it is the same as the
		/// default (<c>SHA1</c>).
		/// </remarks>
		public virtual string GetChecksumAlgorithm()
		{
			return HashAlgorithmName.SHA1.Name;
		}

		/// <summary>
		/// Gets the file path in a format that should be used for display.
		/// </summary>
		/// <returns>The <see cref="RelativePath"/> if set, or the <see cref="FilePath"/>.</returns>
		public virtual string GetFilePathForDisplay()
		{
			return RelativePath ?? FilePath;
		}

		/// <summary>
		/// Reads the <see cref="RazorSourceDocument"/> from the specified <paramref name="stream"/>.
		/// </summary>
		/// <param name="stream">The <see cref="Stream"/> to read from.</param>
		/// <param name="fileName">The file name of the template.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		public static RazorSourceDocument ReadFrom( Stream stream, string fileName )
		{
			if ( stream == null )
			{
				throw new ArgumentNullException( nameof( stream ) );
			}

			var properties = new RazorSourceDocumentProperties( fileName, relativePath: null );
			return new StreamSourceDocument( stream, null, properties );
		}

		/// <summary>
		/// Reads the <see cref="RazorSourceDocument"/> from the specified <paramref name="stream"/>.
		/// </summary>
		/// <param name="stream">The <see cref="Stream"/> to read from.</param>
		/// <param name="fileName">The file name of the template.</param>
		/// <param name="encoding">The <see cref="System.Text.Encoding"/> to use to read the <paramref name="stream"/>.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		public static RazorSourceDocument ReadFrom( Stream stream, string fileName, Encoding encoding )
		{
			if ( stream == null )
			{
				throw new ArgumentNullException( nameof( stream ) );
			}

			if ( encoding == null )
			{
				throw new ArgumentNullException( nameof( encoding ) );
			}

			var properties = new RazorSourceDocumentProperties( fileName, relativePath: null );
			return new StreamSourceDocument( stream, encoding, properties );
		}

		/// <summary>
		/// Reads the <see cref="RazorSourceDocument"/> from the specified <paramref name="stream"/>.
		/// </summary>
		/// <param name="stream">The <see cref="Stream"/> to read from.</param>
		/// <param name="encoding">The <see cref="System.Text.Encoding"/> to use to read the <paramref name="stream"/>.</param>
		/// <param name="properties">Properties to configure the <see cref="RazorSourceDocument"/>.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		public static RazorSourceDocument ReadFrom( Stream stream, Encoding encoding, RazorSourceDocumentProperties properties )
		{
			if ( stream == null )
			{
				throw new ArgumentNullException( nameof( stream ) );
			}

			if ( encoding == null )
			{
				throw new ArgumentNullException( nameof( encoding ) );
			}

			if ( properties == null )
			{
				throw new ArgumentNullException( nameof( properties ) );
			}

			return new StreamSourceDocument( stream, encoding, properties );
		}

		/// <summary>
		/// Reads the <see cref="RazorSourceDocument"/> from the specified <paramref name="projectItem"/>.
		/// </summary>
		/// <param name="projectItem">The <see cref="RazorProjectItem"/> to read from.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		public static RazorSourceDocument ReadFrom( RazorProjectItem projectItem )
		{
			if ( projectItem == null )
			{
				throw new ArgumentNullException( nameof( projectItem ) );
			}

			// ProjectItem.PhysicalPath is usually an absolute (rooted) path.
			var filePath = projectItem.PhysicalPath;
			if ( string.IsNullOrEmpty( filePath ) )
			{
				// Fall back to the relative path only if necessary.
				filePath = projectItem.RelativePhysicalPath;
			}

			if ( string.IsNullOrEmpty( filePath ) )
			{
				// Then fall back to the FilePath (yeah it's a bad name) which is like an MVC view engine path
				// It's much better to have something than nothing.
				filePath = projectItem.FilePath;
			}

			if ( projectItem.RazorSourceDocument is not null )
			{
				return projectItem.RazorSourceDocument;
			}

			using ( var stream = projectItem.Read() )
			{
				// Autodetect the encoding.
				var relativePath = projectItem.RelativePhysicalPath ?? projectItem.FilePath;
				return new StreamSourceDocument( stream, null, new RazorSourceDocumentProperties( filePath, relativePath ) );
			}
		}

		/// <summary>
		/// Creates a <see cref="RazorSourceDocument"/> from the specified <paramref name="content"/>.
		/// </summary>
		/// <param name="content">The source document content.</param>
		/// <param name="fileName">The file name of the <see cref="RazorSourceDocument"/>.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		/// <remarks>Uses <see cref="System.Text.Encoding.UTF8" /></remarks>
		public static RazorSourceDocument Create( string content, string fileName )
		{
			if ( content == null )
			{
				throw new ArgumentNullException( nameof( content ) );
			}

			return Create( content, fileName, Encoding.UTF8 );
		}


		/// <summary>
		/// Creates a <see cref="RazorSourceDocument"/> from the specified <paramref name="content"/>.
		/// </summary>
		/// <param name="content">The source document content.</param>
		/// <param name="properties">Properties to configure the <see cref="RazorSourceDocument"/>.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		/// <remarks>Uses <see cref="System.Text.Encoding.UTF8" /></remarks>
		public static RazorSourceDocument Create( string content, RazorSourceDocumentProperties properties )
		{
			if ( content == null )
			{
				throw new ArgumentNullException( nameof( content ) );
			}

			if ( properties == null )
			{
				throw new ArgumentNullException( nameof( properties ) );
			}

			return Create( content, Encoding.UTF8, properties );
		}

		/// <summary>
		/// Creates a <see cref="RazorSourceDocument"/> from the specified <paramref name="content"/>.
		/// </summary>
		/// <param name="content">The source document content.</param>
		/// <param name="fileName">The file name of the <see cref="RazorSourceDocument"/>.</param>
		/// <param name="encoding">The <see cref="System.Text.Encoding"/> of the file <paramref name="content"/> was read from.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		public static RazorSourceDocument Create( string content, string fileName, Encoding encoding )
		{
			if ( content == null )
			{
				throw new ArgumentNullException( nameof( content ) );
			}

			if ( encoding == null )
			{
				throw new ArgumentNullException( nameof( encoding ) );
			}

			var properties = new RazorSourceDocumentProperties( fileName, relativePath: null );
			return new StringSourceDocument( content, encoding, properties );
		}

		/// <summary>
		/// Creates a <see cref="RazorSourceDocument"/> from the specified <paramref name="content"/>.
		/// </summary>
		/// <param name="content">The source document content.</param>
		/// <param name="encoding">The encoding of the source document.</param>
		/// <param name="properties">Properties to configure the <see cref="RazorSourceDocument"/>.</param>
		/// <returns>The <see cref="RazorSourceDocument"/>.</returns>
		public static RazorSourceDocument Create( string content, Encoding encoding, RazorSourceDocumentProperties properties )
		{
			if ( content == null )
			{
				throw new ArgumentNullException( nameof( content ) );
			}

			if ( encoding == null )
			{
				throw new ArgumentNullException( nameof( encoding ) );
			}

			if ( properties == null )
			{
				throw new ArgumentNullException( nameof( properties ) );
			}

			return new StringSourceDocument( content, encoding, properties );
		}
	}
}
