// 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
{
	internal sealed class StreamSourceDocument : RazorSourceDocument
	{
		// Internal for testing
		internal readonly RazorSourceDocument _innerSourceDocument;

		private readonly byte[] _checksum;

		public StreamSourceDocument( Stream stream, Encoding encoding, RazorSourceDocumentProperties properties )
		{
			if ( stream == null )
			{
				throw new ArgumentNullException( nameof( stream ) );
			}

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

			// Notice we don't validate the encoding here. StreamSourceDocument can compute it.
			_checksum = ComputeChecksum( stream );
			_innerSourceDocument = CreateInnerSourceDocument( stream, encoding, properties );
		}

		public override char this[int position] => _innerSourceDocument[position];

		public override Encoding Encoding => _innerSourceDocument.Encoding;

		public override string FilePath => _innerSourceDocument.FilePath;

		public override int Length => _innerSourceDocument.Length;

		public override RazorSourceLineCollection Lines => _innerSourceDocument.Lines;

		public override string RelativePath => _innerSourceDocument.RelativePath;

		public override void CopyTo( int sourceIndex, char[] destination, int destinationIndex, int count )
			=> _innerSourceDocument.CopyTo( sourceIndex, destination, destinationIndex, count );

		public override byte[] GetChecksum()
		{
			var copiedChecksum = new byte[_checksum.Length];
			_checksum.CopyTo( copiedChecksum, 0 );

			return copiedChecksum;
		}

		private static byte[] ComputeChecksum( Stream stream )
		{
			using ( var hashAlgorithm = SHA1.Create() )
			{
				var checksum = hashAlgorithm.ComputeHash( stream );
				stream.Position = 0;

				return checksum;
			}
		}

		private static RazorSourceDocument CreateInnerSourceDocument( Stream stream, Encoding encoding, RazorSourceDocumentProperties properties )
		{
			var streamLength = (int)stream.Length;
			var content = string.Empty;
			var contentEncoding = encoding ?? Encoding.UTF8;

			if ( streamLength > 0 )
			{
				var bufferSize = Math.Min( streamLength, LargeObjectHeapLimitInChars );

				var reader = new StreamReader(
					stream,
					contentEncoding,
					detectEncodingFromByteOrderMarks: true,
					bufferSize: bufferSize,
					leaveOpen: true );

				using ( reader )
				{
					reader.Peek();      // Just to populate the encoding

					if ( encoding == null )
					{
						contentEncoding = reader.CurrentEncoding;
					}
					else if ( encoding != reader.CurrentEncoding )
					{
						throw new InvalidOperationException();
					}

					if ( streamLength > LargeObjectHeapLimitInChars )
					{
						// If the resulting string would end up on the large object heap, then use LargeTextSourceDocument.
						return new LargeTextSourceDocument(
							reader,
							LargeObjectHeapLimitInChars,
							contentEncoding,
							properties );
					}

					content = reader.ReadToEnd();
				}
			}

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