module dnotify;

import std.path;
import std.file;

private {
    import std.string : toStringz;
    import std.conv : to;
    import std.traits : isPointer, isArray;
    import std.variant : Variant;
    import std.array : appender;
    import deimos.notify.notify;
}

public import deimos.notify.notify : NOTIFY_EXPIRES_DEFAULT, NOTIFY_EXPIRES_NEVER,
                                     NotifyUrgency;

version(NoPragma) {
} else {
    pragma(lib, "notify");
    pragma(lib, "gmodule");
    pragma(lib, "glib-2.0");
}

extern (C) {
    private void g_free(void* mem);
    private void g_list_free(GList* glist);
}

version(NoGdk) {
} else {
    version(NoPragma) {
    } else {
        pragma(lib, "gdk_pixbuf");
    }

    private:
    extern (C) {
        GdkPixbuf* gdk_pixbuf_new_from_file(const(char)* filename, GError **error);
    }
}

class NotificationError : Exception {
    string message;
    GError* gerror;
    
    this(GError* gerror) {
        this.message = to!(string)(gerror.message);
        this.gerror = gerror;
        
        super(this.message);
    }

    this(string message) {
        this.message = message;

        super(message);
    }
}

bool check_availability() {
    // notify_init might return without dbus server actually started
    // try to check for running dbus server
    char **ret_name;
    char **ret_vendor;
    char **ret_version;
    char **ret_spec_version;
    bool ret;
    try {
	return notify_get_server_info(ret_name, ret_vendor, ret_version, ret_spec_version);
    } catch (NotificationError e) {
	throw new NotificationError("Cannot find dbus server!");
    }
}

void init(in char[] name) {
    notify_init(name.toStringz());
}

alias notify_is_initted is_initted;
alias notify_uninit uninit;

shared static this() {
	init(baseName(thisExePath()));
}

shared static ~this() {
    uninit();
}

string get_app_name() {
    return to!(string)(notify_get_app_name());
}

void set_app_name(in char[] app_name) {
    notify_set_app_name(app_name.toStringz());
}

string[] get_server_caps() {
    auto result = appender!(string[])();
    
    GList* list = notify_get_server_caps();
    if(list !is null) {
        for(GList* c = list; c !is null; c = c.next) {
            result.put(to!(string)(cast(char*)c.data));
            g_free(c.data);
        }

        g_list_free(list);
    }

    return result.data;
}

struct ServerInfo {
    string name;
    string vendor;
    string version_;
    string spec_version;
}

ServerInfo get_server_info() {
    char* name;
    char* vendor;
    char* version_;
    char* spec_version;
    notify_get_server_info(&name, &vendor, &version_, &spec_version);

    scope(exit) {
        g_free(name);
        g_free(vendor);
        g_free(version_);
        g_free(spec_version);
    }

    return ServerInfo(to!string(name), to!string(vendor), to!string(version_), to!string(spec_version));
}


struct Action {
    const(char[]) id;
    const(char[]) label;
    NotifyActionCallback callback;
    void* user_ptr;
}


class Notification {
    NotifyNotification* notify_notification;
    
    const(char)[] summary;
    const(char)[] body_;
    const(char)[] icon;

    bool closed = true;
    
    private int _timeout = NOTIFY_EXPIRES_DEFAULT;
    const(char)[] _category;
    NotifyUrgency _urgency;
    GdkPixbuf* _image;
    Variant[const(char)[]] _hints;
    const(char)[] _app_name;
    Action[] _actions;

    this(in char[] summary, in char[] body_, in char[] icon="")
        in { assert(is_initted(), "call dnotify.init() before using Notification"); }
        do {
            this.summary = summary;
            this.body_ = body_;
            this.icon = icon;
            notify_notification = notify_notification_new(summary.toStringz(), body_.toStringz(), icon.toStringz());
        }

    bool update(in char[] summary, in char[] body_, in char[] icon="") {
        this.summary = summary;
        this.body_ = body_;
        this.icon = icon;
        return notify_notification_update(notify_notification, summary.toStringz(), body_.toStringz(), icon.toStringz());
    }

    void show() {
        GError* ge;

        if(!notify_notification_show(notify_notification, &ge)) {
            throw new NotificationError(ge);
        }
    }

    @property int timeout() { return _timeout; }
    @property void timeout(int timeout) {
        this._timeout = timeout;
        notify_notification_set_timeout(notify_notification, timeout);
    }

    @property const(char[]) category() { return _category; }
    @property void category(in char[] category) {
        this._category = category;
        notify_notification_set_category(notify_notification, category.toStringz());
    }

    @property NotifyUrgency urgency() { return _urgency; }
    @property void urgency(NotifyUrgency urgency) {
        this._urgency = urgency;
        notify_notification_set_urgency(notify_notification, urgency);
    }


    void set_image(GdkPixbuf* pixbuf) {
        notify_notification_set_image_from_pixbuf(notify_notification, pixbuf);
        //_image = pixbuf;
    }
    
    version(NoGdk) {
    } else {
        void set_image(in char[] filename) { 
            GError* ge;
            // TODO: free pixbuf
            GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(filename.toStringz(), &ge);

            if(pixbuf is null) {
                if(ge is null) {
                    throw new NotificationError("Unable to load file: " ~ filename.idup);
                } else {
                    throw new NotificationError(ge);
                }
            }
            assert(notify_notification !is null);
            notify_notification_set_image_from_pixbuf(notify_notification, pixbuf); // TODO: fix segfault
            //_image = pixbuf;
        }
    }

    @property GdkPixbuf* image() { return _image; }
    
    // using deprecated set_hint_* functions (GVariant is an opaque structure, which needs the glib)
    void set_hint(T)(in char[] key, T value) {
        static if(is(T == int)) {
            notify_notification_set_hint_int32(notify_notification, key, value);
        } else static if(is(T == uint)) {
            notify_notification_set_hint_uint32(notify_notification, key, value);
        } else static if(is(T == double)) {
            notify_notification_set_hint_double(notify_notification, key, value);
        } else static if(is(T : const(char)[])) {
            notify_notification_set_hint_string(notify_notification, key, value.toStringz());
        } else static if(is(T == ubyte)) {
            notify_notification_set_hint_byte(notify_notification, key, value);
        } else static if(is(T == ubyte[])) {
            notify_notification_set_hint_byte_array(notify_notification, key, value.ptr, value.length);
        } else {
            static assert(false, "unsupported value for Notification.set_hint");
        }

        _hints[key] = Variant(value);
    }

    // unset hint?

    Variant get_hint(in char[] key) {
        return _hints[key];
    }

    @property const(char)[] app_name() { return _app_name; }
    @property void app_name(in char[] name) {
        this._app_name = app_name;
        notify_notification_set_app_name(notify_notification, app_name.toStringz());
    }

    void add_action(T)(in char[] action, in char[] label, NotifyActionCallback callback, T user_data) {
        static if(isPointer!T) {
            void* user_ptr = cast(void*)user_data;
        } else static if(isArray!T) {
            void* user_ptr = cast(void*)user_data.ptr;
        } else {
            void* user_ptr = cast(void*)&user_data;
        }

        notify_notification_add_action(notify_notification, action.toStringz(), label.toStringz(),
                                       callback, user_ptr, null);

        _actions ~= Action(action, label, callback, user_ptr);
    }

    void add_action()(Action action) {
        notify_notification_add_action(notify_notification, action.id.toStringz(), action.label.toStringz(),
                                       action.callback, action.user_ptr, null);

        _actions ~= action;
    }

    @property Action[] actions() { return _actions; }
    
    void clear_actions() {
        notify_notification_clear_actions(notify_notification);
    }

    void close() {
        GError* ge;
        
        if(!notify_notification_close(notify_notification, &ge)) {
            throw new NotificationError(ge);
        }
    }

    @property int closed_reason() {
        return notify_notification_get_closed_reason(notify_notification);
    }
}


version(TestMain) {
    import std.stdio;
    
    void main() {
        writeln(get_app_name());
        set_app_name("bla");
        writeln(get_app_name());
        writeln(get_server_caps());
        writeln(get_server_info());
        
        auto n = new Notification("foo", "bar", "notification-message-im");
        n.timeout = 3;
        n.show();
    }
}
