using System;
using System.Data;
using DotNetMock;

namespace DotNetMock.Framework.Data
{
	/// <summary>
	/// This Mock Object implements the IDataReader interface.
	/// </summary>
	public class MockDataReader : MockObject, IDataReader
	{
		private int _recordsAffectedCount = -1;
		private int _expectedDepth = 0;
		private bool _isClosed = false;

		private ExpectationCounter _closeCalls = new ExpectationCounter("MockDataReader.CloseCalls");
		private ExpectationCounter _readCalls = new ExpectationCounter("MockDataReader.ReadCalls");
		private ExpectationCounter _nextResultCalls = new ExpectationCounter("MockDataReader.NextResultCalls");

		private IDbConnection _currentConnection = null;
		private bool _shouldCloseConnectionOnReaderClose = false;

		private DataTable _schemaTable = null;
		private object[,] _rows = new object[0,0];
		private int _currentRow = -1;
		private Exception _getException = null;

		/// <summary>
		/// Create mock implementation of <see cref="IDataReader"/>.
		/// </summary>
		public MockDataReader() {}
		/// <summary>
		/// Create named mock implementation of <see cref="IDataReader"/>.
		/// </summary>
		public MockDataReader( string mockName ) : base(mockName)
		{
		}
		
		#region Mock Methods
		/// <summary>
		/// Sets the expected number of NextResult calls that will be made on the object.
		/// </summary>
		/// <param name="calls"></param>
		public void SetExpectedNextResultCalls( int calls )
		{
			_nextResultCalls.Expected = calls;
		}
		/// <summary>
		/// Sets the expected value to return for the Depth property.
		/// </summary>
		/// <param name="depth">Depth to return</param>
		public void SetExpectedDepth( int depth )
		{
			_expectedDepth = depth;
		}
		/// <summary>
		/// Set records affected count to return from RecordsAffected property
		/// </summary>
		/// <param name="count">Count to expect</param>
		public void SetRecordsAffectedCount(int count) 
		{
			_recordsAffectedCount = count;
		}
		/// <summary>
		/// Set number of Close() calls to expect
		/// </summary>
		/// <param name="calls">Count to expect</param>
		public void SetExpectedCloseCalls(int calls)
		{
			 _closeCalls.Expected = calls;
		}
		/// <summary>
		/// Sets Schema Table to used 
		/// </summary>
		/// <param name="schemaTable">DataTable to describe columns</param>
		public void SetSchemaTable(DataTable schemaTable)
		{
			_schemaTable = schemaTable;
		}
		/// <summary>
		/// Set number of Read() calls to expect
		/// </summary>
		/// <param name="readCalls">Count to expect</param>
		public void SetExpectedReadCalls(int readCalls)
		{
			_readCalls.Expected = readCalls;
		}
		/// <summary>
		/// Set value of IsClosed property
		/// </summary>
		/// <param name="isClosed">True/False</param>
		public void SetIsClosedValue(bool isClosed)
		{
			_isClosed = isClosed;
		}
		/// <summary>
		/// Set data to use for all get calls
		/// </summary>
		/// <param name="rows">Two-Dimensional array to read data from.  Format: [Row,Column]</param>
		public void SetRows(object[,] rows) 
		{
			_rows = rows;
		}
		/// <summary>
		/// Set exception to throw on any GetXXX() calls
		/// </summary>
		/// <param name="exception">Exception to throw</param>
		public void SetGetException(Exception exception)
		{
			_getException = exception;
		}
		#endregion

		#region Internal Properties
		internal IDbConnection Connection
		{
			set { _currentConnection = value; }
		}
		internal bool ShouldCloseConnectionOnReaderClose
		{
			set { _shouldCloseConnectionOnReaderClose = value; }
		}

		#endregion

		#region Implementation of IDataReader
		/// <summary>
		/// <see cref="IDataReader.Close"/>
		/// </summary>
		public void Close()
		{
			_closeCalls.Inc();
			_isClosed = true;
			if ( _shouldCloseConnectionOnReaderClose )
			{
				if ( _currentConnection != null )
				{
					_currentConnection.Close();
				}
			}
		}
		/// <summary>
		/// Gets SchemaTable
		/// </summary>
		/// <returns></returns>
		public System.Data.DataTable GetSchemaTable()
		{
			return _schemaTable;
		}
		/// <summary>
		/// Advances the data reader to the next result, when reading the result batch SQL statements
		/// </summary>
		/// <returns>Flag indicating if there are more rows or not.</returns>
		public bool NextResult()
		{
			_nextResultCalls.Inc();
			return innerExecute();
		}
		/// <summary>
		/// Advances the IDataReader to the next record.
		/// </summary>
		/// <returns>Flag indicating if there are more rows to read</returns>
		public bool Read()
		{
			_readCalls.Inc();
			return innerExecute();
		}
		/// <summary>
		/// Gets a value indicating the depth of nesting for the current row.  
		/// </summary>
		public int Depth
		{
			get
			{	
				return _expectedDepth;
			}
		}
		/// <summary>
		/// Gets a value indicating whether the data reader is closed
		/// </summary>
		public bool IsClosed
		{
			get
			{
				return _isClosed;
			}
		}
		/// <summary>
		/// Gets the number of rows changed, inserted, or deleted by executing the SQL statement
		/// </summary>
		public int RecordsAffected
		{
			get
			{
				return _recordsAffectedCount;
			}
		}

		#region Implementation of IDataRecord
		/// <summary>
		/// Gets the 32-bit signed integer value of the specified field
		/// </summary>
		/// <param name="i">Index of the field to find</param>
		/// <returns>The 32-bit signed integer value of the specified field.</returns>
		public int GetInt32(int i)
		{
			return Convert.ToInt32(GetValue(i));
		}
		/// <summary>
		/// Returns the value of the specified field
		/// </summary>
		/// <param name="i">Index of the field to find</param>
		/// <returns>The Object which will contain the field value upon return.</returns>
		public object GetValue(int i)
		{
			if (_getException != null) 
			{
				throw _getException;
			}
			return _rows[_currentRow, i];
		}
		/// <summary>
		/// Return whether the specified field is set to null
		/// </summary>
		/// <param name="i">Index of the field to find</param>
		/// <returns>true if the specified field is set to null, otherwise false.</returns>
		public bool IsDBNull(int i)
		{
			if (GetValue(i).GetType().Equals(typeof(DBNull))) 
			{
				return true;
			}
			return false;
		}
		/// <summary>
		/// Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer offset. Currently Not Implemented
		/// </summary>
		/// <param name="i">The zero-based column ordinal. </param>
		/// <param name="fieldOffset">The index within the field from which to begin the read operation. </param>
		/// <param name="buffer">The buffer into which to read the stream of bytes.</param>
		/// <param name="bufferoffset">The index for buffer to begin the read operation.</param>
		/// <param name="length">The number of bytes to read.</param>
		/// <returns>The actual number of bytes read.</returns>
		public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length)
		{
			this.NotImplemented("MockDataReader.GetBytes");
			return 0;
		}
		/// <summary>
		/// Gets the 8-bit unsigned integer value of the specified column.
		/// </summary>
		/// <param name="i">The zero-based column ordinal.</param>
		/// <returns>The 8-bit unsigned integer value of the specified column.</returns>
		public byte GetByte(int i)
		{
			return Convert.ToByte(GetValue(i));
		}
		/// <summary>
		/// Gets the Type information corresponding to the type of Object that would be returned from GetValue. Currently Not Implemented
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The Type information corresponding to the type of Object that would be returned from GetValue.</returns>
		public System.Type GetFieldType(int i)
		{
			if ( _schemaTable == null ) 
			{
				throw new DotNetMockException( "To use the GetFieldType method, a SchemaTable of type DataTable must be provided.  Create a DataTable and use the SetSchemaTable method.");
			}
			return _schemaTable.Columns[i].DataType;
		}
		/// <summary>
		/// Gets the fixed-position numeric value of the specified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The fixed-position numeric value of the specified field.</returns>
		public decimal GetDecimal(int i)
		{
			return Convert.ToDecimal(GetValue(i));
		}
		/// <summary>
		/// Gets all the attribute fields in the collection for the current record.
		/// </summary>
		/// <param name="values">An array of Object to copy the attribute fields into. </param>
		/// <returns>The number of instances of Object in the array.</returns>
		public int GetValues(object[] values)
		{
			int objectCounter = 0;
			for ( int i = 0; i < values.GetLength(0); i++ ) 
			{
				if ( i < _rows.GetLength(1) )
				{
					objectCounter++;
					values[i] = _rows[_currentRow, i];
				}
			}
			return objectCounter;
		}
		/// <summary>
		/// Gets the name for the field to find.  Currently Not Implemented
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The name of the field or the empty string (""), if there is no value to return.</returns>
		public string GetName(int i)
		{
			if ( _schemaTable == null ) 
			{
				throw new AssertionException( "Cannot process DataRecord with first setting schema using SetSchemaTable()" );
			}
			if ( i + 1 > FieldCount ) 
			{
				throw new IndexOutOfRangeException( "Index supplied: " + i + " was out of range for this DataRecord. FieldCount: " + FieldCount );
			}
			return _schemaTable.Columns[i].ColumnName;
		}
		/// <summary>
		/// Gets the 64-bit signed integer value of the specified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The 64-bit signed integer value of the specified field.</returns>
		public long GetInt64(int i)
		{
			return Convert.ToInt64(GetValue(i));
		}
		/// <summary>
		/// Gets the double-precision floating point number of the specified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The double-precision floating point number of the specified field.</returns>
		public double GetDouble(int i)
		{
			return Convert.ToDouble(GetValue(i));
		}
		/// <summary>
		/// Gets the value of the specified column as a Boolean.
		/// </summary>
		/// <param name="i">The zero-based column ordinal. </param>
		/// <returns>The value of the column.</returns>
		public bool GetBoolean(int i)
		{	
			return Convert.ToBoolean(GetValue(i));
		}
		/// <summary>
		/// Returns the guid value of the specified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The guid value of the specified field.</returns>
		public System.Guid GetGuid(int i)
		{
			return (Guid) GetValue(i);
		}
		/// <summary>
		/// Gets the date and time data value of the spcified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The date and time data value of the spcified field.</returns>
		public System.DateTime GetDateTime(int i)
		{
			return Convert.ToDateTime(GetValue(i));
		}
		/// <summary>
		/// Return the index of the named field.
		/// </summary>
		/// <param name="name">The name of the field to find. </param>
		/// <returns>The index of the named field.</returns>
		public int GetOrdinal(string name)
		{
			if ( _schemaTable == null ) 
			{
				throw new AssertionException( "Cannot process DataRecord with first setting schema using SetSchemaTable()" );
			}
			for ( int counterCaseSensitive = 0; counterCaseSensitive < _schemaTable.Columns.Count; counterCaseSensitive++ )
			{
				if ( _schemaTable.Columns[counterCaseSensitive].ColumnName.Equals( name ) ) 
				{
					return counterCaseSensitive;
				}
			}
			for ( int counterCaseInSensitive = 0; counterCaseInSensitive < _schemaTable.Columns.Count; counterCaseInSensitive++ )
			{
				if ( _schemaTable.Columns[counterCaseInSensitive].ColumnName.ToLower().Equals( name.ToLower() ) ) 
				{
					return counterCaseInSensitive;
				}
			}
			throw new IndexOutOfRangeException( "Cannot find column " + name );
		}
		/// <summary>
		/// Gets the data type information for the specified field.  Currently Not Implemented
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The data type information for the specified field.</returns>
		public string GetDataTypeName(int i)
		{
			this.NotImplemented("MockDataReader.GetDataTypeName");
			return null;
		}
		/// <summary>
		/// Gets the single-precision floating point number of the specified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The single-precision floating point number of the specified field.</returns>
		public float GetFloat(int i)
		{	
			return (float) GetValue(i);
		}
		/// <summary>
		/// Gets an IDataReader to be used when the field points to more remote structured data.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>An IDataReader to be used when the field points to more remote structured data.</returns>
		public System.Data.IDataReader GetData(int i)
		{
			return (IDataReader) GetValue(i);
		}
		/// <summary>
		/// Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given buffer offset.
		/// </summary>
		/// <param name="i">The zero-based column ordinal. </param>
		/// <param name="fieldoffset">The index within the row from which to begin the read operation. </param>
		/// <param name="buffer">The buffer into which to read the stream of bytes. </param>
		/// <param name="bufferoffset">The index for buffer to begin the read operation. </param>
		/// <param name="length">The number of bytes to read. </param>
		/// <returns></returns>
		public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length)
		{
			this.NotImplemented("MockDataReader.GetChars");
			return 0;
		}
		/// <summary>
		/// Gets the string value of the specified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The string value of the specified field.</returns>
		public string GetString(int i)
		{
			return Convert.ToString(GetValue(i));
		}
		/// <summary>
		/// Gets the character value of the specified column.
		/// </summary>
		/// <param name="i">The zero-based column ordinal. </param>
		/// <returns>The character value of the specified column.</returns>
		public char GetChar(int i)
		{
			return Convert.ToChar(GetValue(i));
		}
		/// <summary>
		/// Gets the 16-bit signed integer value of the specified field.
		/// </summary>
		/// <param name="i">The index of the field to find. </param>
		/// <returns>The 16-bit signed integer value of the specified field.</returns>
		public short GetInt16(int i)
		{
			return Convert.ToInt16(GetValue(i));
		}
		/// <summary>
		/// Gets the specified column by name.
		/// </summary>
		public object this[string name]
		{
			get
			{
				return this[findColumnIndexForName(name)];
			}
		}
		/// <summary>
		/// Gets the specified column by index
		/// </summary>
		public object this[int i]
		{
			get
			{
				return GetValue(i);
			}
		}
		/// <summary>
		/// Gets the number of columns in the current row.
		/// </summary>
		public int FieldCount
		{
			get
			{
				if ( _schemaTable == null ) 
				{
					return -1;
				}
				return _schemaTable.Columns.Count;
			}
		}
		#endregion
		#endregion

		#region Implementation of IDisposable
		/// <summary>
		/// <see cref="IDisposable.Dispose"/>
		/// </summary>
		public void Dispose()
		{
			if ( !IsClosed ) 
			{
				Close();
			}
		}
		#endregion
		
		#region Private Methods
		/// <summary>
		/// Finds the index of the given name
		/// </summary>
		/// <param name="name">Name of the column to find</param>
		/// <returns>Column index of the given name</returns>
		private int findColumnIndexForName(string name) 
		{
			if (_schemaTable != null) 
			{
				for (int i = 0; i < _schemaTable.Columns.Count; ++i)
				{
					if (_schemaTable.Columns[i].ColumnName.Equals(name))
					{			  
						return i;	
					}
				}
			}
			throw new IndexOutOfRangeException("Column name: " + name + " not found");
		}
		/// <summary>
		/// Helper method that determines if there are more rows to be read or not.
		/// </summary>
		/// <returns>true if there are more rows; false otherwise</returns>
		private bool innerExecute()
		{
			if (_currentRow + 1  >= _rows.GetLength(0)) 
			{
				return false;
			}
			_currentRow++;
			return true;
		}
		#endregion
	}
}
