// Copyright 2006 Alp Toker <alp@atoker.com>
// This software is made available under the MIT License
// See COPYING for details

using System;
using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace NDesk.DBus
{
	class MessageWriter
	{
		protected EndianFlag endianness;
		protected MemoryStream stream;

		public Connection connection;

		//a default constructor is a bad idea for now as we want to make sure the header and content-type match
		public MessageWriter () : this (Connection.NativeEndianness) {}

		public MessageWriter (EndianFlag endianness)
		{
			this.endianness = endianness;
			stream = new MemoryStream ();
		}

		public byte[] ToArray ()
		{
			//TODO: mark the writer locked or something here
			return stream.ToArray ();
		}

		public void CloseWrite ()
		{
			int needed = Protocol.PadNeeded ((int)stream.Position, 8);
			for (int i = 0 ; i != needed ; i++)
				stream.WriteByte (0);
		}

		public void Write (byte val)
		{
			stream.WriteByte (val);
		}

		public void Write (bool val)
		{
			Write ((uint) (val ? 1 : 0));
		}

		unsafe protected void MarshalUShort (byte *data)
		{
			WritePad (2);
			byte[] dst = new byte[2];

			if (endianness == Connection.NativeEndianness) {
				dst[0] = data[0];
				dst[1] = data[1];
			} else {
				dst[0] = data[1];
				dst[1] = data[0];
			}

			stream.Write (dst, 0, 2);
		}

		unsafe public void Write (short val)
		{
			MarshalUShort ((byte*)&val);
		}

		unsafe public void Write (ushort val)
		{
			MarshalUShort ((byte*)&val);
		}

		unsafe protected void MarshalUInt (byte *data)
		{
			WritePad (4);
			byte[] dst = new byte[4];

			if (endianness == Connection.NativeEndianness) {
				dst[0] = data[0];
				dst[1] = data[1];
				dst[2] = data[2];
				dst[3] = data[3];
			} else {
				dst[0] = data[3];
				dst[1] = data[2];
				dst[2] = data[1];
				dst[3] = data[0];
			}

			stream.Write (dst, 0, 4);
		}

		unsafe public void Write (int val)
		{
			MarshalUInt ((byte*)&val);
		}

		unsafe public void Write (uint val)
		{
			MarshalUInt ((byte*)&val);
		}

		unsafe protected void MarshalULong (byte *data)
		{
			WritePad (8);
			byte[] dst = new byte[8];

			if (endianness == Connection.NativeEndianness) {
				for (int i = 0; i < 8; ++i)
					dst[i] = data[i];
			} else {
				for (int i = 0; i < 8; ++i)
					dst[i] = data[7 - i];
			}

			stream.Write (dst, 0, 8);
		}

		unsafe public void Write (long val)
		{
			MarshalULong ((byte*)&val);
		}

		unsafe public void Write (ulong val)
		{
			MarshalULong ((byte*)&val);
		}

#if !DISABLE_SINGLE
		unsafe public void Write (float val)
		{
			MarshalUInt ((byte*)&val);
		}
#endif

		unsafe public void Write (double val)
		{
			MarshalULong ((byte*)&val);
		}

		public void Write (string val)
		{
			byte[] utf8_data = Encoding.UTF8.GetBytes (val);
			Write ((uint)utf8_data.Length);
			stream.Write (utf8_data, 0, utf8_data.Length);
			WriteNull ();
		}

		public void Write (ObjectPath val)
		{
			Write (val.Value);
		}

		public void Write (Signature val)
		{
			byte[] ascii_data = val.GetBuffer ();

			if (ascii_data.Length > Protocol.MaxSignatureLength)
				throw new Exception ("Signature length " + ascii_data.Length + " exceeds maximum allowed " + Protocol.MaxSignatureLength + " bytes");

			Write ((byte)ascii_data.Length);
			stream.Write (ascii_data, 0, ascii_data.Length);
			WriteNull ();
		}

		public void WriteComplex (object val, Type type)
		{
			if (type == typeof (void))
				return;

			if (type.IsArray) {
				WriteArray (val, type.GetElementType ());
			} else if (type.IsGenericType && (type.GetGenericTypeDefinition () == typeof (IDictionary<,>) || type.GetGenericTypeDefinition () == typeof (Dictionary<,>))) {
				Type[] genArgs = type.GetGenericArguments ();
				System.Collections.IDictionary idict = (System.Collections.IDictionary)val;
				WriteFromDict (genArgs[0], genArgs[1], idict);
			} else if (Mapper.IsPublic (type)) {
				WriteObject (type, val);
			} else if (!type.IsPrimitive && !type.IsEnum) {
				WriteValueType (val, type);
				/*
			} else if (type.IsGenericType && type.GetGenericTypeDefinition () == typeof (Nullable<>)) {
				//is it possible to support nullable types?
				Type[] genArgs = type.GetGenericArguments ();
				WriteVariant (genArgs[0], val);
				*/
			} else {
				throw new Exception ("Can't write");
			}
		}

		public void Write (Type type, object val)
		{
			if (type == typeof (void))
				return;

			if (type.IsArray) {
				WriteArray (val, type.GetElementType ());
			} else if (type == typeof (ObjectPath)) {
				Write ((ObjectPath)val);
			} else if (type == typeof (Signature)) {
				Write ((Signature)val);
			} else if (type == typeof (object)) {
				Write (val);
			} else if (type == typeof (string)) {
				Write ((string)val);
			} else if (type.IsGenericType && (type.GetGenericTypeDefinition () == typeof (IDictionary<,>) || type.GetGenericTypeDefinition () == typeof (Dictionary<,>))) {
				Type[] genArgs = type.GetGenericArguments ();
				System.Collections.IDictionary idict = (System.Collections.IDictionary)val;
				WriteFromDict (genArgs[0], genArgs[1], idict);
			} else if (Mapper.IsPublic (type)) {
				WriteObject (type, val);
			} else if (!type.IsPrimitive && !type.IsEnum) {
				WriteValueType (val, type);
			} else {
				Write (Signature.TypeToDType (type), val);
			}
		}

		//helper method, should not be used as it boxes needlessly
		public void Write (DType dtype, object val)
		{
			switch (dtype)
			{
				case DType.Byte:
				{
					Write ((byte)val);
				}
				break;
				case DType.Boolean:
				{
					Write ((bool)val);
				}
				break;
				case DType.Int16:
				{
					Write ((short)val);
				}
				break;
				case DType.UInt16:
				{
					Write ((ushort)val);
				}
				break;
				case DType.Int32:
				{
					Write ((int)val);
				}
				break;
				case DType.UInt32:
				{
					Write ((uint)val);
				}
				break;
				case DType.Int64:
				{
					Write ((long)val);
				}
				break;
				case DType.UInt64:
				{
					Write ((ulong)val);
				}
				break;
#if !DISABLE_SINGLE
				case DType.Single:
				{
					Write ((float)val);
				}
				break;
#endif
				case DType.Double:
				{
					Write ((double)val);
				}
				break;
				case DType.String:
				{
					Write ((string)val);
				}
				break;
				case DType.ObjectPath:
				{
					Write ((ObjectPath)val);
				}
				break;
				case DType.Signature:
				{
					Write ((Signature)val);
				}
				break;
				case DType.Variant:
				{
					Write ((object)val);
				}
				break;
				default:
				throw new Exception ("Unhandled D-Bus type: " + dtype);
			}
		}

		public void WriteObject (Type type, object val)
		{
			ObjectPath path;

			BusObject bobj = val as BusObject;

			if (bobj == null && val is MarshalByRefObject) {
				bobj = ((MarshalByRefObject)val).GetLifetimeService () as BusObject;
			}

			if (bobj == null)
				throw new Exception ("No object reference to write");

			path = bobj.Path;

			Write (path);
		}

		//variant
		public void Write (object val)
		{
			//TODO: maybe support sending null variants

			if (val == null)
				throw new NotSupportedException ("Cannot send null variant");

			Type type = val.GetType ();

			WriteVariant (type, val);
		}

		public void WriteVariant (Type type, object val)
		{
			Signature sig = Signature.GetSig (type);

			Write (sig);
			Write (type, val);
		}

		//this requires a seekable stream for now
		public void WriteArray (object obj, Type elemType)
		{
			Array val = (Array)obj;

			//TODO: more fast paths for primitive arrays
			if (elemType == typeof (byte)) {
				if (val.Length > Protocol.MaxArrayLength)
					throw new Exception ("Array length " + val.Length + " exceeds maximum allowed " + Protocol.MaxArrayLength + " bytes");

				Write ((uint)val.Length);
				stream.Write ((byte[])val, 0, val.Length);
				return;
			}

			long origPos = stream.Position;
			Write ((uint)0);

			//advance to the alignment of the element
			WritePad (Protocol.GetAlignment (Signature.TypeToDType (elemType)));

			long startPos = stream.Position;

			foreach (object elem in val)
				Write (elemType, elem);

			long endPos = stream.Position;
			uint ln = (uint)(endPos - startPos);
			stream.Position = origPos;

			if (ln > Protocol.MaxArrayLength)
				throw new Exception ("Array length " + ln + " exceeds maximum allowed " + Protocol.MaxArrayLength + " bytes");

			Write (ln);
			stream.Position = endPos;
		}

		public void WriteFromDict (Type keyType, Type valType, System.Collections.IDictionary val)
		{
			long origPos = stream.Position;
			Write ((uint)0);

			//advance to the alignment of the element
			//WritePad (Protocol.GetAlignment (Signature.TypeToDType (type)));
			WritePad (8);

			long startPos = stream.Position;

			foreach (System.Collections.DictionaryEntry entry in val)
			{
				WritePad (8);

				Write (keyType, entry.Key);
				Write (valType, entry.Value);
			}

			long endPos = stream.Position;
			uint ln = (uint)(endPos - startPos);
			stream.Position = origPos;

			if (ln > Protocol.MaxArrayLength)
				throw new Exception ("Dict length " + ln + " exceeds maximum allowed " + Protocol.MaxArrayLength + " bytes");

			Write (ln);
			stream.Position = endPos;
		}

		public void WriteValueType (object val, Type type)
		{
			MethodInfo mi = TypeImplementer.GetWriteMethod (type);
			mi.Invoke (null, new object[] {this, val});
			//mi.Invoke (this, new object[] {val});
		}

		/*
		public void WriteValueTypeOld (object val, Type type)
		{
			WritePad (8);

			if (type.IsGenericType && type.GetGenericTypeDefinition () == typeof (KeyValuePair<,>)) {
				System.Reflection.PropertyInfo key_prop = type.GetProperty ("Key");
				Write (key_prop.PropertyType, key_prop.GetValue (val, null));

				System.Reflection.PropertyInfo val_prop = type.GetProperty ("Value");
				Write (val_prop.PropertyType, val_prop.GetValue (val, null));

				return;
			}

			FieldInfo[] fis = type.GetFields (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

			foreach (System.Reflection.FieldInfo fi in fis) {
				object elem;
				elem = fi.GetValue (val);
				Write (fi.FieldType, elem);
			}
		}
		*/

		public void WriteNull ()
		{
			stream.WriteByte (0);
		}

		public void WritePad (int alignment)
		{
			stream.Position = Protocol.Padded ((int)stream.Position, alignment);
		}
	}
}
