//
// X11NotificationArea.cs
//
// Authors:
//   Todd Berman <tberman@off.net>
//   Ed Catmur <ed@catmur.co.uk>
//   Miguel de Icaza <miguel@novell.com>
//   Aaron Bockover <abockover@novell.com>
//
// Copyright (C) 2005 Todd Berman
// Copyright (C) 2005 Ed Catmur
// Copyright (C) 2005-2008 Novell, Inc.
//
// 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.
//

// NOTE: throughout IntPtr is used for the Xlib long type, as this tends to
// have the correct width and does not require any configure checks.

using System;
using System.Runtime.InteropServices;

using Gtk;
using Gdk;

#pragma warning disable 0169, 0414

public class X11NotificationArea : Plug
{
    private uint stamp;
    private Orientation orientation;
    private Visual visual;
    private bool visual_is_rgba;

    private IntPtr selection_atom;
    private IntPtr manager_atom;
    private IntPtr system_tray_opcode_atom;
    private IntPtr orientation_atom;
    private IntPtr visual_atom;
    private IntPtr message_data_atom;
    private IntPtr manager_window;
    private FilterFunc filter;

    public X11NotificationArea (string name)
    {
        Title = name;
        Init ();
    }

    public X11NotificationArea (string name, Gdk.Screen screen)
    {
        Title = name;
        Screen = screen;
        Init ();
    }

    [DllImport ("libc")]
    private static extern IntPtr memcpy (ref XClientMessageEvent.DataUnion dest, IntPtr src, IntPtr len);

    public uint SendMessage (uint timeout, string message)
    {
        if (manager_window == IntPtr.Zero) {
            return 0;
        }

        byte[] arr = System.Text.Encoding.UTF8.GetBytes (message);
        IntPtr unmanaged_arr = Marshal.AllocHGlobal (arr.Length);
        Marshal.Copy (arr, 0, unmanaged_arr, arr.Length);

        SendManagerMessage (SystemTrayMessage.BeginMessage, (IntPtr) Id, timeout, (uint) arr.Length, ++stamp);

        gdk_error_trap_push ();

        for (int index = 0; index < message.Length; index += 20) {
            XClientMessageEvent ev = new XClientMessageEvent ();

            IntPtr display = gdk_x11_display_get_xdisplay (Display.Handle);

            ev.type = XEventName.ClientMessage;
            ev.window = (IntPtr) Id;
            ev.format = 8;
            ev.message_type = message_data_atom;

            int len = Math.Min (arr.Length - index, 20);
            memcpy (ref ev.data, (IntPtr)((int)unmanaged_arr + index), (IntPtr)len);

            XSendEvent (display, manager_window, false, (IntPtr) EventMask.StructureNotifyMask, ref ev);
            XSync (display, false);
        }

        gdk_error_trap_pop ();

        return stamp;
    }

    public void CancelMessage (uint id)
    {
        if (id == 0) {
            return;
        }

        SendManagerMessage (SystemTrayMessage.CancelMessage, (IntPtr) Id, id, 0, 0);
    }

    private void Init ()
    {
        stamp = 1;
        orientation = Orientation.Horizontal;
        AddEvents ((int)EventMask.PropertyChangeMask);
        filter = new FilterFunc (ManagerFilter);

        Display display = Screen.Display;
        IntPtr xdisplay = gdk_x11_display_get_xdisplay (display.Handle);
        selection_atom = XInternAtom (xdisplay, "_NET_SYSTEM_TRAY_S" + Screen.Number.ToString (), false);
        manager_atom = XInternAtom (xdisplay, "MANAGER", false);
        system_tray_opcode_atom = XInternAtom (xdisplay, "_NET_SYSTEM_TRAY_OPCODE", false);
        orientation_atom = XInternAtom (xdisplay, "_NET_SYSTEM_TRAY_ORIENTATION", false);
        visual_atom = XInternAtom (xdisplay, "_NET_SYSTEM_TRAY_VISUAL", false);
        message_data_atom = XInternAtom (xdisplay, "_NET_SYSTEM_TRAY_MESSAGE_DATA", false);

        Screen.RootWindow.AddFilter (filter);
        UpdateManagerWindow (false);
    }

    [GLib.ConnectBefore]
    private void TransparentExposeEvent (object obj, Gtk.ExposeEventArgs args)
    {
        Gtk.Widget widget = (Gtk.Widget)obj;
        Gdk.Rectangle area = args.Event.Area;

        if (visual_is_rgba) {
            Cairo.Context cr = Gdk.CairoHelper.Create (widget.GdkWindow);
            cr.SetSourceRGBA (0, 0, 0, 0);
            cr.Operator = Cairo.Operator.Source;
            Gdk.CairoHelper.Region (cr, args.Event.Region);
            cr.Fill ();

            ((IDisposable)cr.Target).Dispose ();
            ((IDisposable)cr).Dispose ();
        } else {
            widget.GdkWindow.ClearArea (area.X, area.Y, area.Width, area.Height);
        }
    }

    private void MakeTransparentAgain (object obj, Gtk.StyleSetArgs args)
    {
        Gtk.Widget widget = (Gtk.Widget)obj;

        if (visual_is_rgba) {
            widget.GdkWindow.Background = Gdk.Color.Zero;
        } else {
            widget.GdkWindow.SetBackPixmap (null, true);
        }
    }

    private void MakeTransparent (object obj, EventArgs args)
    {
        Gtk.Widget widget = (Gtk.Widget)obj;
        if (widget.IsNoWindow || widget.IsAppPaintable)
            return;

        widget.AppPaintable = true;
        if (visual_is_rgba) {
            widget.GdkWindow.Background = Gdk.Color.Zero;
        } else {
            widget.DoubleBuffered = false;
            widget.GdkWindow.SetBackPixmap (null, true);
        }
        widget.ExposeEvent += TransparentExposeEvent;
        widget.StyleSet += MakeTransparentAgain;
    }

    private void SetColormap ()
    {
        Gtk.Widget widget = (Gtk.Widget)this;

        if (visual == null || visual == Screen.SystemVisual) {
            widget.Colormap = Screen.SystemColormap;
        } else if (visual == Screen.RgbVisual) {
            widget.Colormap = Screen.RgbColormap;
        } else if (visual == Screen.RgbaVisual) {
            widget.Colormap = Screen.RgbaColormap;
        } else {
            widget.Colormap = new Colormap (visual, false);
        }
    }

    protected override void OnRealized ()
    {
        SetColormap ();
        base.OnRealized ();
        MakeTransparent (this, EventArgs.Empty);
        SendDockRequest ();
    }

    protected override void OnAdded (Gtk.Widget child)
    {
        child.Realized += MakeTransparent;
        base.OnAdded (child);
    }

    protected override void OnUnrealized ()
    {
        if (manager_window != IntPtr.Zero) {
            Gdk.Window gdkwin = Gdk.Window.ForeignNewForDisplay (Display, (uint)manager_window);
            if (gdkwin != null) {
                gdkwin.RemoveFilter (filter);
            }
        }

        Screen.RootWindow.RemoveFilter (filter);
        base.OnUnrealized ();
    }

    private void UpdateManagerWindow (bool dock_if_realized)
    {
        if(Handle == IntPtr.Zero || Display == null || Display.Handle == IntPtr.Zero) {
            return;
        }

        IntPtr xdisplay = gdk_x11_display_get_xdisplay (Display.Handle);

        if (manager_window != IntPtr.Zero) {
            return;
        }

        XGrabServer (xdisplay);

        manager_window = XGetSelectionOwner (xdisplay, selection_atom);
        if (manager_window != IntPtr.Zero) {
            XSelectInput (xdisplay, manager_window, (IntPtr) (EventMask.StructureNotifyMask | EventMask.PropertyChangeMask));
        }

        XUngrabServer (xdisplay);
        XFlush (xdisplay);

        if (manager_window != IntPtr.Zero) {
            Gdk.Window gdkwin = Gdk.Window.ForeignNewForDisplay (Display, (uint)manager_window);
            if (gdkwin != null) {
                gdkwin.AddFilter (filter);
            }

            GetOrientationProperty ();
            GetVisualProperty ();

            if (IsRealized) {
                if ((visual == null && Visual == Screen.SystemVisual)
                    || visual == Visual) {
                    // Already have the right visual, can just dock
                    if (dock_if_realized) {
                        SendDockRequest ();
                    }
                } else {
                    // Need to re-realize the widget to get the right visual
                    Hide ();
                    Unrealize ();
                    Show ();
                }
            }
        }
    }

    private void SendDockRequest ()
    {
        SendManagerMessage (SystemTrayMessage.RequestDock, manager_window, Id, 0, 0);
    }

    private void SendManagerMessage (SystemTrayMessage message, IntPtr window, uint data1, uint data2, uint data3)
    {
        XClientMessageEvent ev = new XClientMessageEvent ();
        IntPtr display;

        ev.type = XEventName.ClientMessage;
        ev.window = window;
        ev.message_type = system_tray_opcode_atom;
        ev.format = 32;
        ev.data.ptr1 = (IntPtr)gdk_x11_get_server_time (GdkWindow.Handle);
        ev.data.ptr2 = (IntPtr)message;
        ev.data.ptr3 = (IntPtr)data1;
        ev.data.ptr4 = (IntPtr)data2;
        ev.data.ptr5 = (IntPtr)data3;

        display = gdk_x11_display_get_xdisplay (Display.Handle);
        gdk_error_trap_push ();
        XSendEvent (display, manager_window, false, (IntPtr) EventMask.NoEventMask, ref ev);
        XSync (display, false);
        gdk_error_trap_pop ();
    }

    private FilterReturn ManagerFilter (IntPtr xevent, Event evnt)
    {
        XAnyEvent xev = (XAnyEvent) Marshal.PtrToStructure (xevent, typeof(XAnyEvent));

        if (xev.type == XEventName.ClientMessage){
            XClientMessageEvent xclient = (XClientMessageEvent) Marshal.PtrToStructure (xevent, typeof(XClientMessageEvent));

            if (xclient.message_type == manager_atom && xclient.data.ptr2 == selection_atom) {
                UpdateManagerWindow (true);
                return FilterReturn.Continue;
            }
        }

        if (xev.window == manager_window) {
            if (xev.type == XEventName.PropertyNotify){
                XPropertyEvent xproperty = (XPropertyEvent) Marshal.PtrToStructure (xevent, typeof(XPropertyEvent));
                if (xproperty.atom == orientation_atom) {
                    GetOrientationProperty();
                    return FilterReturn.Continue;
                }
            }

            if (xev.type == XEventName.DestroyNotify) {
                ManagerWindowDestroyed();
            }
        }

        return FilterReturn.Continue;
    }

    private void ManagerWindowDestroyed ()
    {
        if (manager_window != IntPtr.Zero) {
            Gdk.Window gdkwin = Gdk.Window.ForeignNewForDisplay (Display, (uint) manager_window);

            if (gdkwin != null) {
                gdkwin.RemoveFilter (filter);
            }

            manager_window = IntPtr.Zero;
            UpdateManagerWindow (true);
        }
    }

    private void GetOrientationProperty ()
    {
        IntPtr display;
        IntPtr type;
        int format;
        IntPtr prop_return;
        IntPtr nitems, bytes_after;
        int error, result;

        if (manager_window == IntPtr.Zero) {
            return;
        }

        display = gdk_x11_display_get_xdisplay (Display.Handle);

        gdk_error_trap_push ();
        type = IntPtr.Zero;

        result = XGetWindowProperty (display, manager_window, orientation_atom, (IntPtr) 0,
                         (IntPtr) System.Int32.MaxValue, false, (IntPtr) XAtom.Cardinal, out type, out format,
                         out nitems, out bytes_after, out prop_return);

        error = gdk_error_trap_pop ();

        if (error != 0 || result != 0) {
            return;
        }

        if (type == (IntPtr) XAtom.Cardinal) {
            orientation = ((SystemTrayOrientation) Marshal.ReadInt32 (prop_return) == SystemTrayOrientation.Horz)
                ? Orientation.Horizontal
                : Orientation.Vertical;
        }

        if (prop_return != IntPtr.Zero) {
            XFree (prop_return);
        }
    }

    private void GetVisualProperty ()
    {
        IntPtr display;
        IntPtr type;
        int format;
        IntPtr prop_return;
        IntPtr nitems, bytes_after;
        int error, result;

        if (manager_window == IntPtr.Zero) {
            return;
        }

        display = gdk_x11_display_get_xdisplay (Display.Handle);

        gdk_error_trap_push ();
        type = IntPtr.Zero;

        result = XGetWindowProperty (display, manager_window, visual_atom, (IntPtr) 0,
                         (IntPtr) System.Int32.MaxValue, false, (IntPtr) XAtom.VisualId, out type, out format,
                         out nitems, out bytes_after, out prop_return);

        error = gdk_error_trap_pop ();

        if (error != 0 || result != 0) {
            return;
        }

        if (type == (IntPtr) XAtom.VisualId) {
            int visual_id = Marshal.ReadInt32 (prop_return);
            IntPtr raw_ret = gdk_x11_screen_lookup_visual (Screen.Handle, visual_id);
            visual = GLib.Object.GetObject (raw_ret) as Gdk.Visual;
        }

        // TODO the proper check is (visual->red_prec + visual->blue_prec + visual->green_prec < visual->depth)
        // but this seems to work
        visual_is_rgba = visual != null && visual == Screen.RgbaVisual;

        // we can't be double-buffered when we aren't using a real RGBA visual
        DoubleBuffered = visual_is_rgba;

        if (prop_return != IntPtr.Zero) {
            XFree (prop_return);
        }
    }

    [DllImport ("libgdk-x11-2.0.so.0")]
    private static extern IntPtr gdk_x11_screen_lookup_visual (IntPtr screen, int visual_id);

    [DllImport ("libgdk-x11-2.0.so.0")]
    private static extern IntPtr gdk_x11_display_get_xdisplay (IntPtr display);

    [DllImport ("libgdk-x11-2.0.so.0")]
    private static extern int gdk_x11_get_server_time (IntPtr window);

    [DllImport ("libgdk-x11-2.0.so.0")]
    private static extern void gdk_error_trap_push ();

    [DllImport ("libgdk-x11-2.0.so.0")]
    private static extern int gdk_error_trap_pop ();

    [DllImport ("libX11.so.6")]
    private extern static IntPtr XInternAtom(IntPtr display, string atom_name, bool only_if_exists);

    [DllImport ("libX11.so.6")]
    private extern static void XGrabServer (IntPtr display);

    [DllImport ("libX11.so.6")]
    private extern static void XUngrabServer (IntPtr display);

    [DllImport ("libX11.so.6")]
    private extern static int XFlush (IntPtr display);

    [DllImport ("libX11.so.6")]
    private extern static int XSync (IntPtr display, bool discard);

    [DllImport ("libX11.so.6")]
    private extern static int XFree (IntPtr display);

    [DllImport ("libX11.so.6")]
    private extern static IntPtr XGetSelectionOwner (IntPtr display, IntPtr atom);

    [DllImport ("libX11.so.6")]
    private extern static IntPtr XSelectInput (IntPtr display, IntPtr window, IntPtr mask);

    [DllImport ("libX11.so.6")]
    private extern static int XSendEvent (IntPtr display, IntPtr window, bool propagate, IntPtr event_mask,
                         ref XClientMessageEvent send_event);

    [DllImport ("libX11.so.6")]
    private extern static int XGetWindowProperty (IntPtr display, IntPtr w, IntPtr property, IntPtr long_offset,
                             IntPtr long_length, bool deleteProp, IntPtr req_type,
                             out IntPtr actual_type_return, out int actual_format_return,
                             out IntPtr nitems_return, out IntPtr bytes_after_return,
                             out IntPtr prop_return);

    [Flags]
    private enum EventMask {
        NoEventMask              = 0,
        KeyPressMask             = 1 << 0,
        KeyReleaseMask           = 1 << 1,
        ButtonPressMask          = 1 << 2,
        ButtonReleaseMask        = 1 << 3,
        EnterWindowMask          = 1 << 4,
        LeaveWindowMask          = 1 << 5,
        PointerMotionMask        = 1 << 6,
        PointerMotionHintMask    = 1 << 7,
        Button1MotionMask        = 1 << 8,
        Button2MotionMask        = 1 << 9,
        Button3MotionMask        = 1 << 10,
        Button4MotionMask        = 1 << 11,
        Button5MotionMask        = 1 << 12,
        ButtonMotionMask         = 1 << 13,
        KeymapStateMask          = 1 << 14,
        ExposureMask             = 1 << 15,
        VisibilityChangeMask     = 1 << 16,
        StructureNotifyMask      = 1 << 17,
        ResizeRedirectMask       = 1 << 18,
        SubstructureNotifyMask   = 1 << 19,
        SubstructureRedirectMask = 1 << 20,
        FocusChangeMask          = 1 << 21,
        PropertyChangeMask       = 1 << 22,
        ColormapChangeMask       = 1 << 23,
        OwnerGrabButtonMask      = 1 << 24
    }

    private enum SystemTrayMessage {
        RequestDock = 0,
        BeginMessage = 1,
        CancelMessage = 2
    }

    private enum SystemTrayOrientation {
        Horz = 0,
        Vert = 1
    }

    private enum XEventName {
        KeyPress                = 2,
        KeyRelease              = 3,
        ButtonPress             = 4,
        ButtonRelease           = 5,
        MotionNotify            = 6,
        EnterNotify             = 7,
        LeaveNotify             = 8,
        FocusIn                 = 9,
        FocusOut                = 10,
        KeymapNotify            = 11,
        Expose                  = 12,
        GraphicsExpose          = 13,
        NoExpose                = 14,
        VisibilityNotify        = 15,
        CreateNotify            = 16,
        DestroyNotify           = 17,
        UnmapNotify             = 18,
        MapNotify               = 19,
        MapRequest              = 20,
        ReparentNotify          = 21,
        ConfigureNotify         = 22,
        ConfigureRequest        = 23,
        GravityNotify           = 24,
        ResizeRequest           = 25,
        CirculateNotify         = 26,
        CirculateRequest        = 27,
        PropertyNotify          = 28,
        SelectionClear          = 29,
        SelectionRequest        = 30,
        SelectionNotify         = 31,
        ColormapNotify          = 32,
        ClientMessage           = 33,
        MappingNotify           = 34,
        TimerNotify             = 100,
        LASTEvent
    }

    private enum XAtom {
        Cardinal                = 6,
        VisualId                = 32,
        LASTAtom
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct XAnyEvent
    {
        internal XEventName    type;
        internal IntPtr        serial;
        internal bool          send_event;
        internal IntPtr        display;
        internal IntPtr        window;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct XPropertyEvent
    {
        internal XEventName    type;
        internal IntPtr        serial;
        internal bool          send_event;
        internal IntPtr        display;
        internal IntPtr        window;
        internal IntPtr        atom;
        internal IntPtr        time;
        internal int           state;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct XClientMessageEvent
    {
        internal XEventName     type;
        internal IntPtr         serial;
        internal bool           send_event;
        internal IntPtr         display;
        internal IntPtr         window;
        internal IntPtr         message_type;
        internal int            format;

        [StructLayout(LayoutKind.Sequential)]
        internal struct DataUnion
        {
            internal IntPtr ptr1;
            internal IntPtr ptr2;
            internal IntPtr ptr3;
            internal IntPtr ptr4;
            internal IntPtr ptr5;
        }

        internal DataUnion      data;
    }
}

#pragma warning restore 0169, 0414

