#define PURPLE_PLUGINS

#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <regex.h>
#include <gtk/gtk.h>

#include <glib.h>

#include <notify.h>
#include "plugin.h"
#include "version.h"
#include "debug.h"

#include <gtkplugin.h>

#include "pidgin/gtkconv.h"


#define BUF_LONG 4096

#define PLUGIN_ID "patternlinks"

#define EDIT_PATTERN_TEXT "<Edit Pattern>"
#define EDIT_EXEC_TEXT "<Edit Command>"

#define PREF_PREFIX "/plugins/gtk/" PLUGIN_ID
#define PREF_PATTERNS PREF_PREFIX "patterns"
#define PREF_COMMANDS PREF_PREFIX "commands"

/*
 * Useful links:
 * How to use tree views: http://scentric.net/tutorial/treeview-tutorial.html
 * Pidgin C howto: http://developer.pidgin.im/wiki/CHowTo
 * Pidgin dbus howto: http://developer.pidgin.im/wiki/DbusHowto
 */


/** Old function for URI notification. We delegate to this if we don't 
 * know how to handle the URI.
 */
static PurpleNotifyUiOps old_notify_ops = 
{
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL
};


/** The notify ops we give to libpurple.
 */
static PurpleNotifyUiOps my_notify_ops =
{
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL
};


/** The protocol we're using to differentiate our URIs. Note that we slap a 
 * session id onto the protocol to prevent injection of our urls.
 */
static char * protocol;

/** Length of the protocol field. */
static int protocolLength;

typedef struct {
    /** The regex that we're matching. */
    const char * regex_str;

    /** The template to exec. */
    const char * to_exec;

    /** A compiled version of the regex. */
    regex_t regex;
} RegexEntry;

/** Our list of regexes. */
static GList * regexes;

static gboolean
regex_add(const char * regex_str, const char * to_exec) {
    RegexEntry * entry;
    int errcode;

    entry = malloc(sizeof(RegexEntry));
    if (entry == NULL) {
        return FALSE;
    }

    // Make sure the regex compiles
    if (0 != (errcode = regcomp(&entry->regex, regex_str, REG_EXTENDED))) {
        char buf[1024];
        regerror(errcode, &entry->regex, buf, 1023);
        purple_debug_error(PLUGIN_ID, "Problem compiling regex: %s", buf);
        free(entry);
        return FALSE;
    }

    // Finish initializing
    entry->regex_str = strdup(regex_str);
    entry->to_exec = strdup(to_exec);

    // Store the regex in our list
    regexes = g_list_append(regexes, entry);

    return TRUE;
}

/** Remove the given regex from our regex list, freeing the associated 
 * memory.
 */
static void
regex_remove(RegexEntry * entry) {
    regexes = g_list_remove(regexes, entry);

    free((void *)entry->regex_str);
    free((void *)entry->to_exec);

    regfree(&entry->regex);

    free(entry);
}


static void                        
init_plugin(PurplePlugin *plugin)
{                                  
}


#define SUBSTITUTE_STRING "$1"

/**
 * Append the "uri" for the given match into the target. 
 */
static void
write_uri(char * target, char * source, RegexEntry * entry, regmatch_t * match) {
    char * insert;
    char * start;
    const char * reader;
    const char * encoded;

    start = target + strlen(target);

    // Add the stuff we intend to exec
    reader = entry->to_exec;
    while ( (insert = strstr(reader, SUBSTITUTE_STRING)) ) {
        strncat(start, reader, insert - reader);
        strncat(start, source + match->rm_so, match->rm_eo - match->rm_so);

        reader = insert + strlen(SUBSTITUTE_STRING);
    }

    strcat(start, reader);

    // encode it
    encoded = purple_url_encode(start);
//    encoded = start;

    strcpy(start, encoded);
}



/**
 * Add markup to the given string.
 */
static gboolean
add_markup(PurpleAccount *account, const char *who, char **displaying,
                PurpleConversation *conv, PurpleMessageFlags flags)
{
    GList *regex;

    char * target = NULL;
    char * source = NULL;
    char * targetStart = NULL;

    // Prevent injection of our urls
    if (NULL != strstr(*displaying, protocol)) {
        purple_debug_error(PLUGIN_ID, "Stripping potentially malicious url: %s", *displaying);
        *displaying = purple_markup_strip_html(*displaying);
    }

    

    // See if any of the regexes match
    source = *displaying;
    while (TRUE) {
        gboolean anyMatches = FALSE;
       
        // Check each regex for a match
	    for (regex = regexes; regex != NULL; regex = g_list_next(regex)) {
	        regmatch_t match[2];
            RegexEntry * entry = (RegexEntry *)(regex->data);
	
	        if (0 == regexec(&entry->regex, source, 2, match, 0)) {
	            // Match!
	            anyMatches = TRUE;
	
	            if (target == NULL) {
	                targetStart = target = malloc(BUF_LONG);
                    target[0] = '\0';
	            }
	
                strncat(target, source, match[0].rm_so);

	            // Write the URI to the outgoing message
	            strcat(target, "<A HREF=\"");
                strcat(target, protocol);
                write_uri(target, source, entry, &match[1]);
	            strcat(target, "\">");

	            strncat(target, source + match[0].rm_so, 
                        match[0].rm_eo - match[0].rm_so);

	            strcat(target, "</A>");
	
	            source = source + match[0].rm_eo;
	        }
	    }

        if (!anyMatches) {
            break;
        }
    }

    if (target == NULL) {
        // No matches. Return lamely.
        return FALSE;
    }

    // We had matches. Ensure the whole message is present.
    strcat(target, source);

    free(*displaying);
    *displaying = target;

    return FALSE;
}

/** Given a string, generate an 'argv' from it, removing any '\' escapes. 
 * The content of arg is modified. 
 *
 * @return NULL on error. 
 */
static char **
unescape_argv(char * arg) {
    int reader, writer, argvIndex;
    char ** argv;
    gboolean wroteTerminator;

    // Allocate the argv array.
    {
        int spaceCount;

        spaceCount = 0;

        for (reader = 0; arg[reader]; reader++) {
            if (isspace(arg[reader])) {
                spaceCount++;
            }
        }

        argv = calloc(1 + spaceCount, sizeof(char *));
        if (argv == NULL) {
            return NULL;
        }
    }

    // Split the argument string
    argvIndex = 0;
    wroteTerminator = TRUE;
    for (reader = 0, writer = 0; arg[reader]; reader++, writer++) {
        gboolean escaped = FALSE;

        // Update argv (if need be)
        if (wroteTerminator) {
            argv[argvIndex++] = arg + writer;
            wroteTerminator = FALSE;
        }

        // Transpose the escaped bytes into the unescaped bytes
        if (arg[reader] == '\\') {
            reader++;
            escaped = TRUE;
        }

        if (!escaped && isspace(arg[reader])) {
            // We're unescaped and we have ws. Terminate a string. 
            arg[writer] = '\0';
            wroteTerminator = TRUE;
        }
        else {
            arg[writer] = arg[reader];
        }
    }

    // Make sure we're null terminated
    arg[writer] = '\0'; 
    argv[argvIndex] = NULL;

    return argv;
}

/** Called in our fork'd child process to exec the given URI. */
static void
exec_uri(const char * uri) {
    const char * toExec;
    char ** argv;

    // Find the thing to exec
    toExec = uri + protocolLength;

    // Generate the argument list
    argv = unescape_argv(strdup(purple_url_decode(toExec)));

    execvp(argv[0], argv);
    exit(-1); // We shouldn't get here. But if we do...
}

/** Called when a link is followed. Check for our own schema and run the 
 * command. Alternatively, delegate to the previously installed commands.
 */
static void *
my_notify_uri(const char *uri) {
    if (strncmp(protocol, uri, strlen(protocol)) == 0) {
        pid_t pid = fork();

        if (pid == 0) {
            exec_uri(uri);
        }
        else if (pid < 0) {
            purple_debug_error(PLUGIN_ID, "Failed to fork for %s", uri);
        }
    }
    else {
        if (old_notify_ops.notify_uri) {
            return old_notify_ops.notify_uri(uri);
        }
    }

    return NULL;
}

/** Writes our regexes into our preference. */
static void
regexes_store() {
    GList * list;
    GList * regex;

    // Turf the existing strings
    purple_prefs_remove(PREF_PATTERNS);
    purple_prefs_remove(PREF_COMMANDS);

    // Add the patterns
    list = NULL;
    for (regex = regexes; regex != NULL; regex = g_list_next(regex)) {
        RegexEntry * entry = regex->data;
        list = g_list_append(list, (void *)entry->regex_str);
    }

    purple_prefs_add_string_list(PREF_PATTERNS, list);
    g_list_free(list);
    
    // Add the commands
    list = NULL;
    for (regex = regexes; regex != NULL; regex = g_list_next(regex)) {
        RegexEntry * entry = regex->data;
        list = g_list_append(list, (void *)entry->to_exec);
    }

    purple_prefs_add_string_list(PREF_COMMANDS, list);
    g_list_free(list);
}


static void
regexes_load() {
    GList * patterns;
    GList * commands;

    patterns = purple_prefs_get_string_list(PREF_PATTERNS);
    commands = purple_prefs_get_string_list(PREF_COMMANDS);

    if (g_list_length(patterns) != g_list_length(commands)) {
        purple_debug_error(PLUGIN_ID, "Patterns/commands list length differs. Discarding.");
        return;
    }

    while (TRUE) {
        char * pattern;
        char * command;

        if (patterns == NULL) {
            break;
        }

        pattern = patterns->data;
        command = commands->data;

        patterns = g_list_remove(patterns, pattern);
        commands = g_list_remove(commands, command);

        regex_add(pattern, command);

        g_free(pattern);
        g_free(command);
    }
}


static gboolean
plugin_load(PurplePlugin *plugin) {
    PurpleNotifyUiOps * oldOps;

    // Initialize our regexes
    regexes = NULL;
    regexes_load();


    // Initialize our s3kr1t.
#define PROTOCOL_SIZE 200    
    protocol = malloc(PROTOCOL_SIZE);
    protocolLength = snprintf(protocol, PROTOCOL_SIZE, "extlink-%li:", random());

    // Connect our handlers to put links into messages
    purple_signal_connect_priority(pidgin_conversations_get_handle(),
                    "displaying-im-msg", plugin,
                    PURPLE_CALLBACK(add_markup), NULL, 
                    PURPLE_SIGNAL_PRIORITY_HIGHEST / 2);
    purple_signal_connect_priority(pidgin_conversations_get_handle(),
                    "displaying-chat-msg", plugin,
                    PURPLE_CALLBACK(add_markup), NULL,
                    PURPLE_SIGNAL_PRIORITY_HIGHEST / 2);

    // Add ourselves as a notification handler.
    oldOps = purple_notify_get_ui_ops();

    memcpy(&old_notify_ops, oldOps, sizeof(PurpleNotifyUiOps));

    memcpy(&my_notify_ops, oldOps, sizeof(PurpleNotifyUiOps));

    my_notify_ops.notify_uri = my_notify_uri;
    
    purple_notify_set_ui_ops(&my_notify_ops);


    return TRUE;
}

static gboolean 
plugin_unload(PurplePlugin *plugin) {
    PurpleNotifyUiOps * uiOps;

    // Turf our regexes
    while (regexes) {
        RegexEntry * entry = regexes->data;
        regex_remove(entry);
    }
    

    // Disconnect our notification handler
    uiOps = malloc(sizeof(PurpleNotifyUiOps));
    if (uiOps == NULL) {
        // We're doomed! We can't deregister this notifier.
    }
    else {
        memcpy(uiOps, &old_notify_ops, sizeof(PurpleNotifyUiOps));

        purple_notify_set_ui_ops(uiOps);

        // We leak the uiOps here, but I'm not sure how else to deal with 'em
    }

    // Disconnect our signals
    purple_signal_disconnect(pidgin_conversations_get_handle(), 
                    "displaying-im-msg", plugin, PURPLE_CALLBACK(add_markup));

    purple_signal_disconnect(pidgin_conversations_get_handle(), 
                    "displaying-chat-msg", plugin, PURPLE_CALLBACK(add_markup));

    // Delete our secret protocol
    free(protocol);

    return TRUE;
}

enum {
    /** The editable pattern column in our edit widget. */
    PATTERN_COLUMN,

    /** The editable exec template in our edit widget. */
    EXEC_COLUMN,

    N_COLUMNS
};


struct foreach_has_communicator {
    gboolean found;
    GtkTreeIter * iter;
};

/** Test to see if there's a row containing the EDIT_PATTERN_TEXT or
 * EDIT_EXEC_TEXT. 
 */
static gboolean
foreach_has_unset(GtkTreeModel *store, GtkTreePath *path, GtkTreeIter *iter, void * c)
{
    gchar * pattern;
    gchar * to_exec;
    struct foreach_has_communicator * comm = c;

    gtk_tree_model_get (store, iter,
            PATTERN_COLUMN, &pattern,
            EXEC_COLUMN, &to_exec,
            -1);

    // Look for the text.
    comm->found = strcmp(EDIT_PATTERN_TEXT, pattern) == 0
            || strcmp(EDIT_EXEC_TEXT, to_exec) == 0;
    if (comm->found) {
        *(comm->iter) = *iter;
    }

    // Clean up.
    g_free(pattern); 
    g_free(to_exec); 

    return comm->found;
}

/** Checks to see if there's an 'add' row - ie, one containing 
 * EDIT_PATTERN_TEXT or EDIT_EXEC_TEXT. If so, iter is pointed to it, 
 * otherwise, iter is left unchanged.
 */
static gboolean
hasAdd(GtkTreeView * tree, GtkTreeIter * iter) {
    GtkListStore * store;
    struct foreach_has_communicator comm = {FALSE, iter};

    store = GTK_LIST_STORE(gtk_tree_view_get_model(tree));

    gtk_tree_model_foreach(GTK_TREE_MODEL(store), foreach_has_unset, 
            (void *)&comm);

    return comm.found;
}

/** Called when the "Add" button on the config page is clicked. 
 */
static void 
addClicked(GtkWidget *widget, GtkTreeView * tree) {
    GtkTreeIter iter;
    GtkListStore * store;
    GtkTreePath * path;
    GtkTreeViewColumn * column;

    store = GTK_LIST_STORE(gtk_tree_view_get_model(tree));

    // Check to see if there's already an addable row.
    if (!hasAdd(tree, &iter)) {

        // There isn't. Create a new row.
        gtk_list_store_append(store, &iter);

        gtk_list_store_set(store, &iter,
                PATTERN_COLUMN, EDIT_PATTERN_TEXT, 
                EXEC_COLUMN, EDIT_EXEC_TEXT,
                -1
        );
    }

    // Select the newly created row
    path = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);

    column = gtk_tree_view_get_column(tree, PATTERN_COLUMN);

    gtk_tree_view_set_cursor_on_cell(tree, path, column, NULL, TRUE);

    gtk_tree_path_free(path);
}

/** Delete a row from our list of regexes. Called when the "delete" button 
 * on the config page is clicked. 
 */
static void 
deleteClicked(GtkWidget *widget, GtkTreeView * tree) {
    GtkTreeSelection *selection;
    GtkTreeModel *store;
    GtkTreeIter iter;

    // Find our selection
    store = gtk_tree_view_get_model(tree);
    selection = gtk_tree_view_get_selection(tree);

    // Delete (if appropriate)
    if (gtk_tree_selection_get_selected(selection, &store, &iter)) {
        gtk_list_store_remove(GTK_LIST_STORE(store), &iter);
    }
}

/** Called when a cell in our config page is edited. */
static void
cell_edited(GtkCellRendererText *cell, 
        gchar *path, gchar *new_text, GtkTreeView * tree, int column) {
    GtkListStore * store;
    GtkTreeIter iter;

    store = GTK_LIST_STORE(gtk_tree_view_get_model(tree));

    if (!gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(store), 
            &iter, path)) {
        return;
    }

    gtk_list_store_set(store, &iter,
            column, new_text,
            -1);
}

/** Callback to set the column text when the pattern is edited. */
static void
pattern_edited_cb(GtkCellRendererText *cell, 
        gchar *path, gchar *new_text, GtkTreeView * tree) {
    cell_edited(cell, path, new_text, tree, PATTERN_COLUMN);
}

/** Callback that sets the exec column's text on an edit. */
static void
exec_edited_cb(GtkCellRendererText *cell, 
        gchar *path, gchar *new_text, GtkTreeView * tree) {
    cell_edited(cell, path, new_text, tree, EXEC_COLUMN);
}


static gboolean
foreach_add(GtkTreeModel *store, GtkTreePath *path, GtkTreeIter *iter, void * data)
{
    gchar * pattern;
    gchar * to_exec;

    gtk_tree_model_get (store, iter,
            PATTERN_COLUMN, &pattern,
            EXEC_COLUMN, &to_exec,
            -1);

    if (strcmp(EDIT_PATTERN_TEXT, pattern) != 0
            && strcmp(EDIT_EXEC_TEXT, to_exec) != 0) {
        // The user has edited the regex pattern. Add it to the list.
        regex_add(pattern, to_exec);
    }

    g_free(pattern); 
    g_free(to_exec); 

    return FALSE;
}

/** Called when the config frame is closed. */
static void 
config_frame_closed(GtkWidget *widget, GtkTreeView * tree) {
    GtkListStore * store;

    // Delete the existing regexes
    while (regexes) {
        RegexEntry * entry = regexes->data;
        regex_remove(entry);
    }
    
    // Add the new regexes to the list
    store = GTK_LIST_STORE(gtk_tree_view_get_model(tree));
    gtk_tree_model_foreach(GTK_TREE_MODEL(store), foreach_add, NULL);

    // Store the regexes
    regexes_store();
}

/**
 * Writes the current content of our regex list into the given list. 
 */
static void
populate_pattern_list(GtkListStore * store) {
    GList * regex;

    gtk_list_store_clear(store);

    for (regex = regexes; regex != NULL; regex = g_list_next(regex)) {
        GtkTreeIter iter;
        RegexEntry * entry = regex->data;

        gtk_list_store_append(store, &iter);

        gtk_list_store_set(store, &iter,
                PATTERN_COLUMN, entry->regex_str,
                EXEC_COLUMN, entry->to_exec,
                -1
        );
    }
}

/** 
 * Reset the tree model to our current list o' regexes.
 */
static void 
resetClicked(GtkWidget *widget, GtkTreeView * tree) {
    populate_pattern_list(GTK_LIST_STORE(gtk_tree_view_get_model(tree)));
}


/** Build our config page. */
static GtkWidget *
create_config_frame(PurplePlugin * plugin) {
    GtkWidget *ret;
    GtkListStore *store;
    GtkWidget *tree;
    GtkCellRenderer * patternRenderer;
    GtkCellRenderer * execRenderer;

    ret = gtk_vbox_new(FALSE, PIDGIN_HIG_CAT_SPACE);
    gtk_box_set_homogeneous(GTK_BOX(ret), FALSE);

    gtk_container_set_border_width(GTK_CONTAINER(ret), PIDGIN_HIG_BORDER);
    
    // Add an explanation
    {
        GtkWidget * label;
        label = gtk_label_new("Supply a POSIX regular expression to create links. When a link is selected, the specified command will be run. To copy text from the pattern into the command, use the text '$1'.");

        gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);

        gtk_box_pack_start(GTK_BOX(ret), label, FALSE, FALSE, 0);
    }

    // Create the widget model
    {
        store = gtk_list_store_new (N_COLUMNS,
                G_TYPE_STRING,   /* Pattern */
                G_TYPE_STRING   /* Exec */
        ); 
        
        populate_pattern_list(store);
    }

    // Display the widget
    tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
    {
        GtkTreeViewColumn *column;

        // Configure the pattern renderer
        patternRenderer = gtk_cell_renderer_text_new();
        g_object_set(patternRenderer, "editable", TRUE, NULL);
        gtk_object_set(GTK_OBJECT(patternRenderer), 
                "mode", GTK_CELL_RENDERER_MODE_EDITABLE, 
                NULL);

        // Create the pattern column
        column = gtk_tree_view_column_new_with_attributes("Pattern",
                patternRenderer,
                "text", PATTERN_COLUMN,
                NULL);

        gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);


        // Configure the exec renderer
        execRenderer = gtk_cell_renderer_text_new();
        g_object_set(execRenderer, "editable", TRUE, NULL);
        gtk_object_set(GTK_OBJECT(execRenderer), 
                "mode", GTK_CELL_RENDERER_MODE_EDITABLE, 
                NULL);

        // The exec column
        column = gtk_tree_view_column_new_with_attributes("Execute",
                execRenderer,
                "text", EXEC_COLUMN,
                NULL);

        gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
    }

    g_object_unref(store);

    g_signal_connect(patternRenderer, "edited", (GCallback)pattern_edited_cb, 
            tree);
    g_signal_connect(execRenderer, "edited", (GCallback)exec_edited_cb, tree);

    // Create the scrolling area for the tree
    {
        GtkWidget *scrollArea;

        scrollArea = gtk_scrolled_window_new(NULL, NULL);
        gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollArea), 
                GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

        gtk_widget_set_size_request(scrollArea, 322, 200);

        gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrollArea), 
                tree);
        
        gtk_box_pack_start(GTK_BOX(ret), scrollArea, TRUE, TRUE, 0);
    }
    

    // Display add/delete buttons
    {
        GtkWidget * resetButton;
        GtkWidget * addButton;
        GtkWidget * deleteButton;
        GtkWidget * hbox;

        // Create the buttons
        addButton = gtk_button_new_from_stock(GTK_STOCK_NEW);
        deleteButton = gtk_button_new_from_stock(GTK_STOCK_DELETE);
        resetButton = gtk_button_new_from_stock(GTK_STOCK_REVERT_TO_SAVED);

        // Hook up calls
        g_signal_connect (G_OBJECT(addButton), "clicked",
                G_CALLBACK(addClicked), tree);

        g_signal_connect (G_OBJECT(deleteButton), "clicked",
                G_CALLBACK(deleteClicked), tree);

        g_signal_connect (G_OBJECT(resetButton), "clicked",
                G_CALLBACK(resetClicked), tree);


        // Pack the buttons
        hbox = gtk_hbox_new(TRUE, PIDGIN_HIG_CAT_SPACE);

        gtk_container_add(GTK_CONTAINER(hbox), addButton);
        gtk_container_add(GTK_CONTAINER(hbox), deleteButton);
        gtk_container_add(GTK_CONTAINER(hbox), resetButton);
        
        gtk_box_pack_start(GTK_BOX(ret), hbox, FALSE, FALSE, 0);
    }

    // Hook up the close signals
    g_signal_connect(ret, "destroy", G_CALLBACK(config_frame_closed), tree);


    gtk_widget_show_all(ret);

    return ret;
}


/** Defines our UI callbacks. */
static PidginPluginUiInfo config_ui =
{
    create_config_frame,
    0,

    NULL,
    NULL,
    NULL,
    NULL
};

/** Plugin definition. */
static PurplePluginInfo info = {
    PURPLE_PLUGIN_MAGIC,
    PURPLE_MAJOR_VERSION,
    PURPLE_MINOR_VERSION,
    PURPLE_PLUGIN_STANDARD,
    PIDGIN_PLUGIN_TYPE,
    0,
    NULL,
    PURPLE_PRIORITY_DEFAULT,

    PLUGIN_ID,
    "Pattern Links",
    "1.0.0",

    "Create links in message text.",          
    "Create arbitrary hyperlinks in message text. Hyperlinks are handled by external programs.", 
    "",                          
    NULL,     
    
    plugin_load,                   
    plugin_unload,                          
    NULL,                          
                                   
    &config_ui,                          
    NULL,                          
    NULL,                        
    NULL,                   
    NULL,                          
    NULL,                          
    NULL,                          
    NULL                           
};                               



PURPLE_INIT_PLUGIN(hello_world, init_plugin, info)
