//
// System.Security.Cryptography CryptoStream.cs
//
// Authors:
//	Thomas Neidhart (tome@sbox.tugraz.at)
//	Sebastien Pouliot (sebastien@ximian.com)
//
// Portions (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
// (C) 2004 Novell (http://www.novell.com)
//

//
// Copyright (C) 2004 Novell, Inc (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;

namespace System.Security.Cryptography {

	public class CryptoStream : Stream {
		private Stream _stream;
		private ICryptoTransform _transform;
		private CryptoStreamMode _mode;
		private byte[] _currentBlock;
		private bool _disposed;
		private bool _flushedFinalBlock;
		private int _partialCount;
		private bool _endOfStream;

		private byte[] _waitingBlock;
		private int _waitingCount;

		private byte[] _transformedBlock;
		private int _transformedPos;
		private int _transformedCount;

		private byte[] _workingBlock;
		private int _workingCount;
		
		public CryptoStream (Stream stream, ICryptoTransform transform, CryptoStreamMode mode)
		{
			if ((mode == CryptoStreamMode.Read) && (!stream.CanRead)) {
				throw new ArgumentException (
					Locale.GetText ("Can't read on stream"));
			}
			if ((mode == CryptoStreamMode.Write) && (!stream.CanWrite)) {
				throw new ArgumentException (
					Locale.GetText ("Can't write on stream"));
			}
			_stream = stream;
			_transform = transform;
			_mode = mode;
			_disposed = false;
			if (transform != null) {
				_workingBlock = new byte [transform.InputBlockSize];
				if (mode == CryptoStreamMode.Read)
					_currentBlock = new byte [transform.InputBlockSize];
				else if (mode == CryptoStreamMode.Write)
					_currentBlock = new byte [transform.OutputBlockSize];
			}
		}

		~CryptoStream () 
		{
			Dispose (false);
		}
		
		public override bool CanRead {
			get { return (_mode == CryptoStreamMode.Read); }
		}

		public override bool CanSeek {
			get { return false; }
		}

		public override bool CanWrite {
			get { return (_mode == CryptoStreamMode.Write); }
		}
		
		public override long Length {
			get { throw new NotSupportedException ("Length"); }
		}

		public override long Position {
			get { throw new NotSupportedException ("Position"); }
			set { throw new NotSupportedException ("Position"); }
		}

		public void Clear () 
		{
			Dispose (true);
			GC.SuppressFinalize (this); // not called in Stream.Dispose
		}

		// LAMESPEC: A CryptoStream can be close in read mode
		public override void Close () 
		{
			// only flush in write mode (bugzilla 46143)
			if ((!_flushedFinalBlock) && (_mode == CryptoStreamMode.Write))
				FlushFinalBlock ();

			if (_stream != null)
				_stream.Close ();
		}

		public override int Read ([In,Out] byte[] buffer, int offset, int count)
		{
			if (_mode != CryptoStreamMode.Read) {
				throw new NotSupportedException (
					Locale.GetText ("not in Read mode"));
			}
			if (offset < 0) {
				throw new ArgumentOutOfRangeException ("offset", 
					Locale.GetText ("negative"));
			}
			if (count < 0) {
				throw new ArgumentOutOfRangeException ("count",
					Locale.GetText ("negative"));
			}
			// yes - buffer.Length will throw a NullReferenceException if buffer is null
			// but by doing so we match MS implementation
			// re-ordered to avoid integer overflow
			if (offset > buffer.Length - count) {
				throw new ArgumentException ("(offset+count)", 
					Locale.GetText ("buffer overflow"));
			}
			// for some strange reason ObjectDisposedException isn't throw
			// instead we get a ArgumentNullException (probably from an internal method)
			if (_workingBlock == null) {
				throw new ArgumentNullException (
					Locale.GetText ("object _disposed"));
			}

			int result = 0;
			if ((count == 0) || ((_transformedPos == _transformedCount) && (_endOfStream)))
				return result;

			if (_waitingBlock == null) {
				_transformedBlock = new byte [_transform.OutputBlockSize << 2];
				_transformedPos = 0;
				_transformedCount = 0;
				_waitingBlock = new byte [_transform.InputBlockSize];
				_waitingCount = _stream.Read (_waitingBlock, 0, _waitingBlock.Length);
			}
			
			while (count > 0) {
				// transformed but not yet returned
				int length = (_transformedCount - _transformedPos);

				// need more data - at least one full block must be available if we haven't reach the end of the stream
				if (length < _transform.InputBlockSize) {
					int transformed = 0;

					// load a new block
					_workingCount = _stream.Read (_workingBlock, 0, _workingBlock.Length);
					_endOfStream = (_workingCount < _transform.InputBlockSize);

					if (!_endOfStream) {
						// transform the waiting block
						transformed = _transform.TransformBlock (_waitingBlock, 0, _waitingBlock.Length, _transformedBlock, _transformedCount);

						// transfer temporary to waiting
						Buffer.BlockCopy (_workingBlock, 0, _waitingBlock, 0, _workingCount);
						_waitingCount = _workingCount;
					}
					else {
						if (_workingCount > 0) {
							// transform the waiting block
							transformed = _transform.TransformBlock (_waitingBlock, 0, _waitingBlock.Length, _transformedBlock, _transformedCount);

							// transfer temporary to waiting
							Buffer.BlockCopy (_workingBlock, 0, _waitingBlock, 0, _workingCount);
							_waitingCount = _workingCount;

							length += transformed;
							_transformedCount += transformed;
						}
						byte[] input = _transform.TransformFinalBlock (_waitingBlock, 0, _waitingCount);
						transformed = input.Length;
						Buffer.BlockCopy (input, 0, _transformedBlock, _transformedCount, input.Length);
						// zeroize this last block
						Array.Clear (input, 0, input.Length);
					}

					length += transformed;
					_transformedCount += transformed;
				}
				// compaction
				if (_transformedPos > _transform.InputBlockSize) {
					Buffer.BlockCopy (_transformedBlock, _transformedPos, _transformedBlock, 0, length);
					_transformedCount -= _transformedPos;
					_transformedPos = 0;
				}

				length = ((count < length) ? count : length);
				if (length > 0) {
					Buffer.BlockCopy (_transformedBlock, _transformedPos, buffer, offset, length);
					_transformedPos += length;

					result += length;
					offset += length;
					count -= length;
				}

				// there may not be enough data in the stream for a 
				// complete block
				if (((length != _transform.InputBlockSize) && (_waitingCount != _transform.InputBlockSize)) || (_endOfStream)) {
					count = 0;	// no more data can be read
				}
			}
			
			return result;
		}

		public override void Write (byte[] buffer, int offset, int count)
		{
			if (_mode != CryptoStreamMode.Write) {
				throw new NotSupportedException (
					Locale.GetText ("not in Write mode"));
			}
			if (offset < 0) { 
				throw new ArgumentOutOfRangeException ("offset", 
					Locale.GetText ("negative"));
			}
			if (count < 0) {
				throw new ArgumentOutOfRangeException ("count", 
					Locale.GetText ("negative"));
			}
			// re-ordered to avoid integer overflow
			if (offset > buffer.Length - count) {
				throw new ArgumentException ("(offset+count)", 
					Locale.GetText ("buffer overflow"));
			}

			// partial block (in progress)
			if ((_partialCount > 0) && (_partialCount != _transform.InputBlockSize)) {
				int remainder = _transform.InputBlockSize - _partialCount;
				remainder = ((count < remainder) ? count : remainder);
				Buffer.BlockCopy (buffer, offset, _workingBlock, _partialCount, remainder);
				_partialCount += remainder;
				offset += remainder;
				count -= remainder;
			}

			int bufferPos = offset;
			while (count > 0) {
				if (_partialCount == _transform.InputBlockSize) {
					// use partial block to avoid (re)allocation
					int len = _transform.TransformBlock (_workingBlock, 0, _partialCount, _currentBlock, 0);
					_stream.Write (_currentBlock, 0, len);
					// reset
					_partialCount = 0;
				}

				if (_transform.CanTransformMultipleBlocks) {
					// transform all except the last block (which may be the last block
					// of the stream and require TransformFinalBlock)
					int numBlock = ((_partialCount + count) / _transform.InputBlockSize);
					int multiSize = (numBlock * _transform.InputBlockSize);
					if (numBlock > 0) {
						byte[] multiBlocks = new byte [multiSize];
						int len = _transform.TransformBlock (buffer, offset, multiSize, multiBlocks, 0);
						_stream.Write (multiBlocks, 0, len); 
						// copy last block into _currentBlock
						_partialCount = count - multiSize;
						Buffer.BlockCopy (buffer, offset + multiSize, _workingBlock, 0, _partialCount);
					}
					else {
						Buffer.BlockCopy (buffer, offset, _workingBlock, _partialCount, count);
						_partialCount += count;
					}
					count = 0; // the last block, if any, is in _workingBlock
				}
				else {
					int len = Math.Min (_transform.InputBlockSize - _partialCount, count);
					Buffer.BlockCopy (buffer, bufferPos, _workingBlock, _partialCount, len);
					bufferPos += len;
					_partialCount += len;
					count -= len;
					// here block may be full, but we wont TransformBlock it until next iteration
					// so that the last block will be called in FlushFinalBlock using TransformFinalBlock
				}
			}
		}

		public override void Flush ()
		{
			if (_stream != null)
				_stream.Flush ();
		}

		public void FlushFinalBlock ()
		{
			if (_flushedFinalBlock) {
				throw new NotSupportedException (
					Locale.GetText ("This method cannot be called twice."));
			}
			if (_mode != CryptoStreamMode.Write) {
				throw new NotSupportedException (
					Locale.GetText ("cannot flush a non-writeable CryptoStream"));
			}

			_flushedFinalBlock = true;
			byte[] finalBuffer = _transform.TransformFinalBlock (_workingBlock, 0, _partialCount);
			if (_stream != null) {
				_stream.Write (finalBuffer, 0, finalBuffer.Length);
				if (_stream is CryptoStream) {
					// for cascading crypto streams
					(_stream as CryptoStream).FlushFinalBlock ();
				}
				_stream.Flush ();
			}
			// zeroize
			Array.Clear (finalBuffer, 0, finalBuffer.Length);
		}

		public override long Seek (long offset, SeekOrigin origin)
		{
			throw new NotSupportedException ("Seek");
		}
		
		// LAMESPEC: Exception NotSupportedException not documented
		public override void SetLength (long value)
		{
			throw new NotSupportedException ("SetLength");
		}

		protected virtual void Dispose (bool disposing) 
		{
			if (!_disposed) {
				_disposed = true;
				// always cleared for security reason
				if (_workingBlock != null)
					Array.Clear (_workingBlock, 0, _workingBlock.Length);
				if (_currentBlock != null)
					Array.Clear (_currentBlock, 0, _currentBlock.Length);
				if (disposing) {
					_stream = null;
					_workingBlock = null;
					_currentBlock = null;
				}
			}
		}
	}
}
