namespace DBus 
{
  
  using System;
  using System.Runtime.InteropServices;
  using System.Diagnostics;
  using System.Collections;
  
  public class Message 
  {
    private static Stack stack = new Stack ();
	  
    static public Message Current {
      get 
	{
	  return stack.Count > 0 ? (Message) stack.Peek () : null;
	}
    }
    static internal void Push (Message message)
    {
      stack.Push (message);
    }
    static internal void Pop ()
    {
      stack.Pop ();
    }
	  
    
    /// 
    /// A pointer to the underlying Message structure
    /// 
    private IntPtr rawMessage;
    
    /// 
    /// The current slot number
    /// 
    private static int slot = -1;
    
    // Keep in sync with C
    public enum MessageType 
    {
      Invalid = 0,
      MethodCall = 1,
      MethodReturn = 2,
      Error = 3,
      Signal = 4
    }
    private Arguments arguments = null;
    protected Service service = null;
    protected string pathName = null;
    protected string interfaceName = null;
    protected string name = null;    
    private string key= null;
    protected Message()
    {
      // An empty constructor for the sake of sub-classes which know how to construct theirselves.
    }
    
    protected Message(IntPtr rawMessage, Service service)
    {
      RawMessage = rawMessage;
      this.service = service;
    }
    
    protected Message(MessageType messageType) 
    {
      // the assignment bumps the refcount
      RawMessage = dbus_message_new((int) messageType);
      
      if (RawMessage == IntPtr.Zero) {
	throw new OutOfMemoryException();
      }
      
      dbus_message_unref(RawMessage);
    }
    
    protected Message(MessageType messageType, Service service) : this(messageType) 
    {
      this.service = service;
    }
    
    ~Message() 
    {
      RawMessage = IntPtr.Zero; // free the native object
    }
    
    public static Message Wrap(IntPtr rawMessage, Service service) 
    {
      if (slot > -1) {
	// If we already have a Message object associated with this rawMessage then return it
	IntPtr rawThis = dbus_message_get_data(rawMessage, slot);
	if (rawThis != IntPtr.Zero)
	  return (DBus.Message) ((GCHandle)rawThis).Target;
      } 
      // If it doesn't exist then create a new Message around it
      Message message = null;
      MessageType messageType = (MessageType) dbus_message_get_type(rawMessage);
      
      switch (messageType) {
      case MessageType.Signal:
	message = new Signal(rawMessage, service);
	break;
      case MessageType.MethodCall:
	message = new MethodCall(rawMessage, service);
	break;
      case MessageType.MethodReturn:
	message = new MethodReturn(rawMessage, service);
	break;
      case MessageType.Error:
	message = new ErrorMessage(rawMessage, service);
	break;
      default:
	throw new ApplicationException("Unknown message type to wrap: " + messageType);
      }
      return message;
    }
    
    internal IntPtr RawMessage 
    {
      get 
	{
	  return rawMessage;
	}
      set 
	{
	  if (value == rawMessage) 
	    return;
	  
	  if (rawMessage != IntPtr.Zero) 
	    {
	      // Get the reference to this
	      IntPtr rawThis = dbus_message_get_data(rawMessage, Slot);
	      Debug.Assert (rawThis != IntPtr.Zero);
	      
	      // Blank over the reference
	      dbus_message_set_data(rawMessage, Slot, IntPtr.Zero, IntPtr.Zero);
	      
	      // Free the reference
	      ((GCHandle) rawThis).Free();
	      
	      // Unref the connection
	      dbus_message_unref(rawMessage);
	    }
	  
	  this.rawMessage = value;
	  
	  if (rawMessage != IntPtr.Zero) 
	    {
	      GCHandle rawThis;
	      
	      dbus_message_ref(rawMessage);
	      
	      // We store a weak reference to the C# object on the C object
	      rawThis = GCHandle.Alloc(this, GCHandleType.WeakTrackResurrection);
	      
	      dbus_message_set_data(rawMessage, Slot, (IntPtr) rawThis, IntPtr.Zero);
	    }
	}
    }
    
    public void Send(ref int serial) 
    {
      if (!dbus_connection_send (Service.Connection.RawConnection, RawMessage, ref serial))
	throw new OutOfMemoryException ();
      Service.Connection.Flush();
    }
    
    public void Send() 
    {
      int ignored = 0;
      Send(ref ignored);
    }
    public void SendWithReply() 
    {
      IntPtr rawPendingCall = IntPtr.Zero;
      
      if (!dbus_connection_send_with_reply (Service.Connection.RawConnection, RawMessage, rawPendingCall, Service.Connection.Timeout))
	throw new OutOfMemoryException();
    }
     
    public MethodReturn SendWithReplyAndBlock()
    {
      Error error = new Error();
      error.Init();
      IntPtr rawMessage = dbus_connection_send_with_reply_and_block(Service.Connection.RawConnection, 
								    RawMessage, 
								    Service.Connection.Timeout, 
								    ref error);
      if (rawMessage != IntPtr.Zero) {
	MethodReturn methodReturn = new MethodReturn(rawMessage, Service);
	return methodReturn;
      } else {
	throw new DBusException(error);
      }
    }
    public MessageType Type
    {
      get 
	{
	  return (MessageType) dbus_message_get_type(RawMessage);
	}
    }
    
    public Service Service
    {
      set 
	{
	  if (this.service != null && (value.Name != this.service.Name)) {
	    if (!dbus_message_set_destination(RawMessage, value.Name)) {
	      throw new OutOfMemoryException();
	    }
	  }
	  
	  this.service = value;
	}
      get 
	{
	  return this.service;
	}
    }
    
    protected virtual string PathName
    {
      set 
	{
	  if (value != this.pathName) 
	    {
	      if (!dbus_message_set_path(RawMessage, value)) {
		throw new OutOfMemoryException();
	      }
	      
	      this.pathName = value;
	    }
	}
      get 
	{
	  if (this.pathName == null) {
	    this.pathName = Marshal.PtrToStringAnsi(dbus_message_get_path(RawMessage));
	  }
	  
	  return this.pathName;
	}
    }
    
    protected virtual string InterfaceName
    {
      set 
	{
	  if (value != this.interfaceName)
	    {
	      dbus_message_set_interface (RawMessage, value);
	      this.interfaceName = value;
	    }
	}
      get 
	{
	  if (this.interfaceName == null) {
	    this.interfaceName = Marshal.PtrToStringAnsi(dbus_message_get_interface(RawMessage));
	  }
	  return this.interfaceName;
	}
    }
    
    protected virtual string Name
    {
      set {
	if (value != this.name) {
	  dbus_message_set_member(RawMessage, value);
	  this.name = value;
	}
      }
      get {
	if (this.name == null) {
	  this.name = Marshal.PtrToStringAnsi(dbus_message_get_member(RawMessage));
	}
	return this.name;
      }
    }
    public string Key
    {
      get {
	if (this.key == null) {
	  this.key = Name + " " + Arguments;
	}
	
	return this.key;
      }
    }
    public Arguments Arguments
    {
      get 
	{
	  if (this.arguments == null) {
	    this.arguments = new Arguments(this);
	  }
	  
	  return this.arguments;
	}
    }
    public string Sender 
    {
      get
	{
	  return Marshal.PtrToStringAnsi(dbus_message_get_sender(RawMessage));
	}
    }
    public string Destination
    {
      get
	{
	  return Marshal.PtrToStringAnsi(dbus_message_get_destination(RawMessage));
	}
    }
	    
    protected int Slot
    {
      get 
	{
	  if (slot == -1) 
	    {
	      // We need to initialize the slot
	      if (!dbus_message_allocate_data_slot (ref slot))
		throw new OutOfMemoryException ();
	      
	      Debug.Assert (slot >= 0);
	    }
	  
	  return slot;
	}
    }
    
    [DllImport ("dbus-1", EntryPoint="dbus_message_new")]
    protected extern static IntPtr dbus_message_new (int messageType);
    
    [DllImport ("dbus-1", EntryPoint="dbus_message_unref")]
    protected extern static void dbus_message_unref (IntPtr ptr);
    
    [DllImport ("dbus-1", EntryPoint="dbus_message_ref")]
    protected extern static void dbus_message_ref (IntPtr ptr);
    
    [DllImport ("dbus-1", EntryPoint="dbus_message_allocate_data_slot")]
    protected extern static bool dbus_message_allocate_data_slot (ref int slot);
    
    [DllImport ("dbus-1", EntryPoint="dbus_message_free_data_slot")]
    protected extern static void dbus_message_free_data_slot (ref int slot);
    
    [DllImport ("dbus-1", EntryPoint="dbus_message_set_data")]
    protected extern static bool dbus_message_set_data (IntPtr ptr,
							int    slot,
							IntPtr data,
							IntPtr free_data_func);
    
    [DllImport ("dbus-1", EntryPoint="dbus_message_get_data")]
    protected extern static IntPtr dbus_message_get_data (IntPtr ptr,
							  int    slot);
    
    [DllImport ("dbus-1", EntryPoint="dbus_connection_send")]
    private extern static bool dbus_connection_send (IntPtr  ptr,
						     IntPtr  message,
						     ref int client_serial);
    [DllImport ("dbus-1", EntryPoint="dbus_connection_send_with_reply")]
    private extern static bool dbus_connection_send_with_reply (IntPtr rawConnection, IntPtr rawMessage, IntPtr rawPendingCall, int timeout);
    [DllImport ("dbus-1", EntryPoint="dbus_connection_send_with_reply_and_block")]
    private extern static IntPtr dbus_connection_send_with_reply_and_block (IntPtr rawConnection, IntPtr  message, int timeout, ref Error error);
    [DllImport("dbus-1")]
    private extern static int dbus_message_get_type(IntPtr rawMessage);
    [DllImport("dbus-1")]
    private extern static bool dbus_message_set_path(IntPtr rawMessage, string pathName);
    [DllImport("dbus-1")]
    private extern static IntPtr dbus_message_get_path(IntPtr rawMessage);
    
    [DllImport("dbus-1")]
    private extern static bool dbus_message_set_interface (IntPtr rawMessage, string interfaceName);
    [DllImport("dbus-1")]
    private extern static IntPtr dbus_message_get_interface(IntPtr rawMessage);
    
    [DllImport("dbus-1")]
    private extern static bool dbus_message_set_member(IntPtr rawMessage, string name);
    [DllImport("dbus-1")]
    private extern static IntPtr dbus_message_get_member(IntPtr rawMessage);
    [DllImport("dbus-1")]
    private extern static bool dbus_message_set_destination(IntPtr rawMessage, string serviceName);
    [DllImport("dbus-1")]
    private extern static IntPtr dbus_message_get_destination(IntPtr rawMessage);
    [DllImport("dbus-1")]
    private extern static IntPtr dbus_message_get_sender(IntPtr rawMessage);
  }
}