//
// OciDefineHandle.cs
//
// Part of managed C#/.NET library System.Data.OracleClient.dll
//
// Part of the Mono class libraries at
// mcs/class/System.Data.OracleClient/System.Data.OracleClient.Oci
//
// Assembly: System.Data.OracleClient.dll
// Namespace: System.Data.OracleClient.Oci
//
// Authors:
//     Tim Coleman <tim@timcoleman.com>
//     Daniel Morgan <danielmorgan@verizon.net>
//
// Copyright (C) Tim Coleman, 2003
// Copyright (C) Daniel Morgan, 2004
//

using System;
using System.Data.OracleClient;
using System.Runtime.InteropServices;
using System.Text;

namespace System.Data.OracleClient.Oci
{
	internal sealed class OciDefineHandle : OciHandle, IDisposable
	{
		#region Fields

		bool disposed = false;

		IntPtr handle = IntPtr.Zero;
		IntPtr value;
		short indicator;
		//OracleType type;
		OciDataType ociType;
		OciDataType definedType;
		int definedSize;
		short rlenp = 0;
		//short precision;
		short scale;
		Type fieldType;
		//string name;

		// Oracle defines the LONG VARCHAR have a size of 2 to the 31 power - 5
		// maybe this should settable via a config file for System.Data.OracleClient.dll
		// see DefineLong.  Or if the a truncate sql error occurs, then retry?
		// or maybe allocate the memory yourself and then let oracle return error more data,
		// then try to get some more data
		internal static int LongVarCharMaxValue = (int) Int16.MaxValue - 5;

		OciErrorHandle errorHandle;

		OciLobLocator lobLocator;
		OciDateTimeDescriptor dateTimeDesc;
		//byte[] date;

		#endregion // Fields

		#region Constructors

		public OciDefineHandle (OciHandle parent, IntPtr newHandle)
			: base (OciHandleType.Define, parent, newHandle)
		{
		}

		public void DefineByPosition (int position)
		{
			OciParameterDescriptor parameter = ((OciStatementHandle) Parent).GetParameter (position);

			//name = parameter.GetName ();
			definedType = parameter.GetDataType ();
			definedSize = parameter.GetDataSize ();
			//precision = parameter.GetPrecision ();
			scale = parameter.GetScale ();

			Define (position);

			parameter.Dispose ();
		}

		#endregion // Constructors

		#region Properties

		public OciDataType DataType {
			get { return definedType; }
		}

		public Type FieldType {
			get { return fieldType; }
		}

		public int DefinedSize {
			get { return definedSize; }
		}

		public OciErrorHandle ErrorHandle {
			get { return errorHandle; }
			set { errorHandle = value; }
		}

		public bool IsNull {
			get { return (indicator == -1); }
		}

		public short Scale {
			get { return scale; }
		}

		public short Size {
			get { return rlenp; }
		}

		public IntPtr Value {
			get { return value; }
		}

		#endregion

		#region Methods

		void Define (int position)
		{
			switch (definedType) {
			case OciDataType.Date:
				DefineDate (position);
				return;
			case OciDataType.TimeStamp:
				DefineTimeStamp (position);
				return;
			case OciDataType.Clob:
			case OciDataType.Blob:
				DefineLob (position, definedType);
				return;
			case OciDataType.Raw:
				DefineRaw( position);
				return;
			case OciDataType.RowIdDescriptor:
				definedSize = 10;
				DefineChar (position);
				return;
			case OciDataType.Integer:
			case OciDataType.Number:
			case OciDataType.Float:
				DefineNumber (position);
				return;
			case OciDataType.Long:
			case OciDataType.LongVarChar:
				DefineLong (position);
				return;
			default:
				DefineChar (position); // HANDLE ALL OTHERS AS CHAR FOR NOW
				return;
			}
		}

		void DefineTimeStamp (int position)
		{
			definedSize = -1;
			ociType = OciDataType.TimeStamp;
			fieldType = typeof(System.DateTime);

			dateTimeDesc = (OciDateTimeDescriptor) Parent.Parent.Allocate (OciHandleType.TimeStamp);
			if (dateTimeDesc == null) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}

			value = dateTimeDesc.Handle;
			dateTimeDesc.ErrorHandle = ErrorHandle;

			int status = 0;

			status = OciCalls.OCIDefineByPosPtr (Parent,
				out handle,
				ErrorHandle,
				position + 1,
				ref value,
				definedSize,
				ociType,
				ref indicator,
				ref rlenp,
				IntPtr.Zero,
				0);

			definedSize = 11;

			if (status != 0) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}
		}

		void DefineDate (int position)
		{
			definedSize = 7;
			ociType = OciDataType.Date;
			fieldType = typeof(System.DateTime);

			value = OciCalls.AllocateClear (definedSize);

			int status = 0;

			status = OciCalls.OCIDefineByPos (Parent,
						ref handle,
						ErrorHandle,
						position + 1,
						value,
						definedSize,
						ociType,
						ref indicator,
						ref rlenp,
						IntPtr.Zero,
						0);

			if (status != 0) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}
		}

		void DefineLong (int position)
		{
			fieldType = typeof (System.String);

			// LONG VARCHAR max length is 2 to the 31 power - 5
			// the first 4 bytes of a LONG VARCHAR value contains the length
			// Int32.MaxValue - 5 causes out of memory in mono on win32
			// because I do not have 2GB of memory available
			// so Int16.MaxValue - 5 is used instead.
			// LAMESPEC for Oracle OCI - you can not get the length of the LONG VARCHAR value
			// until after you get the value.  This could be why Oracle deprecated LONG VARCHAR.
			// If you specify a definedSize less then the length of the column value,
			// then you will get an OCI_ERROR ORA-01406: fetched column value was truncated
			definedSize = LongVarCharMaxValue;

			value = OciCalls.AllocateClear (definedSize);
			ociType = OciDataType.LongVarChar;

			int status = 0;
			status = OciCalls.OCIDefineByPos (Parent,
				ref handle,
				ErrorHandle,
				position + 1,
				value,
				definedSize,
				ociType,
				ref indicator,
				ref rlenp,
				IntPtr.Zero, 0);

			rlenp = (short) definedSize;

			if (status != 0) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}
		}

		void DefineChar (int position)
		{
			fieldType = typeof (System.String);

			int maxByteCount = Encoding.UTF8.GetMaxByteCount (definedSize);
			value = OciCalls.AllocateClear (maxByteCount);

			ociType = OciDataType.Char;

			int status = 0;

			status = OciCalls.OCIDefineByPos (Parent,
						ref handle,
						ErrorHandle,
						position + 1,
						value,
						maxByteCount,
						ociType,
						ref indicator,
						ref rlenp,
						IntPtr.Zero,
						0);

			if (status != 0) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}
		}

		void DefineNumber (int position)
		{
			fieldType = typeof (System.Decimal);
			value = OciCalls.AllocateClear (definedSize);

			ociType = OciDataType.Char;

			int status = 0;

			status = OciCalls.OCIDefineByPos (Parent,
				ref handle,
				ErrorHandle,
				position + 1,
				value,
				definedSize * 2,
				ociType,
				ref indicator,
				ref rlenp,
				IntPtr.Zero,
				0);

			if (status != 0) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}
		}

		void DefineLob (int position, OciDataType type)
		{
			ociType = type;

			if (ociType == OciDataType.Clob)
				fieldType = typeof(System.String);
			else if (ociType == OciDataType.Blob)
				fieldType = typeof(byte[]);

			int status = 0;

			definedSize = -1;

			lobLocator = (OciLobLocator) Parent.Parent.Allocate (OciHandleType.LobLocator);

			if (lobLocator == null) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}

			value = lobLocator.Handle;
			lobLocator.ErrorHandle = ErrorHandle;
			lobLocator.Service = ((OciStatementHandle) Parent).Service;

			status = OciCalls.OCIDefineByPosPtr (Parent,
							out handle,
							ErrorHandle,
							position + 1,
							ref value,
							definedSize,
							ociType,
							ref indicator,
							ref rlenp,
							IntPtr.Zero,
							0);

			definedSize = Int32.MaxValue;

			if (status != 0) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}
		}

		void DefineRaw (int position)
		{
			ociType = OciDataType.Raw;
			fieldType = typeof (byte[]);

			value = OciCalls.AllocateClear (definedSize);

			int status = 0;

			status = OciCalls.OCIDefineByPos (Parent,
							ref handle,
							ErrorHandle,
							position + 1,
							value,
							definedSize * 2,
							ociType,
							ref indicator,
							ref rlenp,
							IntPtr.Zero, 0);

			if (status != 0) {
				OciErrorInfo info = ErrorHandle.HandleError ();
				throw new OracleException (info.ErrorCode, info.ErrorMessage);
			}
		}

		protected override void Dispose (bool disposing)
		{
			if (!disposed) {
				try {
					switch (definedType) {
					case OciDataType.Clob:
					case OciDataType.Blob:
					case OciDataType.TimeStamp:
						break;
					default:
						Marshal.FreeHGlobal (value);
						break;
					}
					disposed = true;
				} finally {
					base.Dispose (disposing);
					value = IntPtr.Zero;
				}
			}
		}

		public OracleLob GetOracleLob ()
		{
			return new OracleLob (lobLocator, ociType);
		}

                public object GetValue (IFormatProvider formatProvider)
		{
			object tmp;

			byte [] buffer = null;

			switch (DataType) {
			case OciDataType.VarChar2:
			case OciDataType.String:
			case OciDataType.VarChar:
			case OciDataType.Char:
			case OciDataType.CharZ:
			case OciDataType.OciString:
			case OciDataType.RowIdDescriptor:
				buffer = new byte [Size];
				Marshal.Copy (Value, buffer, 0, Size);

				// Get length of returned string
				int 	rsize = 0;
				IntPtr	env = Parent.Parent;	// Parent is statement, grandparent is environment
				OciCalls.OCICharSetToUnicode (env, null, buffer, out rsize);

				// Get string
				StringBuilder ret = new StringBuilder(rsize);
				OciCalls.OCICharSetToUnicode (env, ret, buffer, out rsize);

				return ret.ToString ();
			case OciDataType.LongVarChar:
			case OciDataType.Long:
				buffer = new byte [LongVarCharMaxValue];
				Marshal.Copy (Value, buffer, 0, buffer.Length);

				int longSize = 0;
				if (BitConverter.IsLittleEndian)
					longSize = BitConverter.ToInt32 (new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, 0);
				else
					longSize = BitConverter.ToInt32 (new byte[]{buffer[3], buffer[2], buffer[1], buffer[0]}, 0);

				ASCIIEncoding encoding = new ASCIIEncoding ();
				string e = encoding.GetString (buffer, 4, longSize);
				return e;
			case OciDataType.Integer:
			case OciDataType.Number:
			case OciDataType.Float:
				tmp = Marshal.PtrToStringAnsi (Value, Size);
				if (tmp != null)
                                        return Decimal.Parse (String.Copy ((string) tmp), formatProvider);
				break;
			case OciDataType.TimeStamp:
				return dateTimeDesc.GetDateTime (Parent.Parent, dateTimeDesc.ErrorHandle);
			case OciDataType.Date:
				return UnpackDate ();
			case OciDataType.Raw:
				byte [] raw_buffer = new byte [Size];
				Marshal.Copy (Value, raw_buffer, 0, Size);
				return raw_buffer;
			case OciDataType.Blob:
			case OciDataType.Clob:
				return GetOracleLob ();
			}

			return DBNull.Value;
		}

                internal object GetOracleValue (IFormatProvider formatProvider)
		{
                        object ovalue = GetValue (formatProvider);

			switch (DataType) {
			case OciDataType.Raw:
				return new OracleBinary ((byte[]) ovalue);
			case OciDataType.Date:
				return new OracleDateTime ((DateTime) ovalue);
			case OciDataType.Blob:
			case OciDataType.Clob:
				OracleLob lob = (OracleLob) ovalue;
				return lob;
			case OciDataType.Integer:
			case OciDataType.Number:
			case OciDataType.Float:
				return new OracleNumber ((decimal) ovalue);
			case OciDataType.VarChar2:
			case OciDataType.String:
			case OciDataType.VarChar:
			case OciDataType.Char:
			case OciDataType.CharZ:
			case OciDataType.OciString:
			case OciDataType.LongVarChar:
			case OciDataType.Long:
			case OciDataType.RowIdDescriptor:
				return new OracleString ((string) ovalue);
			default:
				// TODO: do other types
				throw new NotImplementedException ();
			}
		}

		[MonoTODO ("Be able to handle negative dates... i.e. BCE.")]
		internal DateTime UnpackDate ()
		{
			byte century = Marshal.ReadByte (value, 0);
			byte year = Marshal.ReadByte (value, 1);
			byte month = Marshal.ReadByte (value, 2);
			byte day = Marshal.ReadByte (value, 3);
			byte hour = Marshal.ReadByte (value, 4);
			byte minute = Marshal.ReadByte (value, 5);
			byte second = Marshal.ReadByte (value, 6);

			if (hour == 0)
				hour ++;
			if (minute == 0)
				minute ++;
			if (second == 0)
				second ++;

			return new DateTime ((century - 100) * 100 + (year - 100),
						month,
						day,
						hour - 1,
						minute - 1,
						second - 1);

		}

		#endregion // Methods
	}
}
