#include <config.h>

#include <glibtop/procmap.h>
#include <glibtop/mountlist.h>
#include <sys/stat.h>
#include <glib/gi18n.h>

#include <string>
#include <map>
#include <sstream>
#include <iomanip>
#include <stdexcept>

using std::string;


#include "procman.h"
#include "memmaps.h"
#include "proctable.h"
#include "util.h"


/* be careful with this enum, you could break the column names */
enum
{
    MMAP_COL_FILENAME,
    MMAP_COL_VMSTART,
    MMAP_COL_VMEND,
    MMAP_COL_VMSZ,
    MMAP_COL_FLAGS,
    MMAP_COL_VMOFFSET,
    MMAP_COL_PRIVATE_CLEAN,
    MMAP_COL_PRIVATE_DIRTY,
    MMAP_COL_SHARED_CLEAN,
    MMAP_COL_SHARED_DIRTY,
    MMAP_COL_DEVICE,
    MMAP_COL_INODE,
    MMAP_COL_MAX
};


namespace
{
    class OffsetFormater
    {
        string format;

    public:

        void set(const glibtop_map_entry &last_map)
        {
            this->format = (last_map.end <= G_MAXUINT32) ? "%08" G_GINT64_MODIFIER "x" : "%016" G_GINT64_MODIFIER "x";
        }

        string operator()(guint64 v) const
        {
            char buffer[17];
            g_snprintf(buffer, sizeof buffer, this->format.c_str(), v);
            return buffer;
        }
    };


    class InodeDevices
    {
        typedef std::map<guint16, string> Map;
        Map devices;

    public:

        void update()
        {
            this->devices.clear();

            glibtop_mountlist list;
            glibtop_mountentry *entries = glibtop_get_mountlist(&list, 1);

            for (unsigned i = 0; i != list.number; ++i) {
                struct stat buf;

                if (stat(entries[i].devname, &buf) != -1)
                    this->devices[buf.st_rdev] = entries[i].devname;
            }

            g_free(entries);
        }

        string get(guint64 dev64)
        {
            if (dev64 == 0)
                return "";

            guint16 dev = dev64 & 0xffff;

            if (dev != dev64)
                g_warning("weird device %" G_GINT64_MODIFIER "x", dev64);

                Map::iterator it(this->devices.find(dev));

                if (it != this->devices.end())
                    return it->second;

                guint8 major, minor;
                major = dev >> 8;
                minor = dev;

                std::ostringstream out;
                out << std::hex
                    << std::setfill('0')
                    << std::setw(2) << unsigned(major)
                    << ':'
                    << std::setw(2) << unsigned(minor);

                this->devices[dev] = out.str();
                return out.str();
        }
    };


    class MemMapsData
    {
    public:
        guint timer;
        GtkWidget *tree;
        GSettings *settings;
        ProcInfo *info;
        OffsetFormater format;
        mutable InodeDevices devices;
        const char * const schema;

        MemMapsData(GtkWidget *a_tree, GSettings *a_settings)
            : tree(a_tree),
            settings(a_settings),
            schema("memmapstree")
        {
            procman_get_tree_state(this->settings, this->tree, this->schema);
        }

        ~MemMapsData()
        {
            procman_save_tree_state(this->settings, this->tree, this->schema);
        }
    };
}


struct glibtop_map_entry_cmp
{
    bool operator()(const glibtop_map_entry &a, const guint64 start) const
    {
        return a.start < start;
    }

    bool operator()(const guint64 &start, const glibtop_map_entry &a) const
    {
        return start < a.start;
    }

};


static void
update_row(GtkTreeModel *model, GtkTreeIter &row, const MemMapsData &mm, const glibtop_map_entry *memmaps)
{
    guint64 size;
    string filename, device;
    string vmstart, vmend, vmoffset;
    char flags[5] = "----";

    size = memmaps->end - memmaps->start;

    if(memmaps->perm & GLIBTOP_MAP_PERM_READ)    flags [0] = 'r';
    if(memmaps->perm & GLIBTOP_MAP_PERM_WRITE)   flags [1] = 'w';
    if(memmaps->perm & GLIBTOP_MAP_PERM_EXECUTE) flags [2] = 'x';
    if(memmaps->perm & GLIBTOP_MAP_PERM_SHARED)  flags [3] = 's';
    if(memmaps->perm & GLIBTOP_MAP_PERM_PRIVATE) flags [3] = 'p';

    if (memmaps->flags & (1 << GLIBTOP_MAP_ENTRY_FILENAME))
      filename = memmaps->filename;

    vmstart  = mm.format(memmaps->start);
    vmend    = mm.format(memmaps->end);
    vmoffset = mm.format(memmaps->offset);
    device   = mm.devices.get(memmaps->device);

    gtk_list_store_set (GTK_LIST_STORE (model), &row,
                        MMAP_COL_FILENAME, filename.c_str(),
                        MMAP_COL_VMSTART, vmstart.c_str(),
                        MMAP_COL_VMEND, vmend.c_str(),
                        MMAP_COL_VMSZ, size,
                        MMAP_COL_FLAGS, flags,
                        MMAP_COL_VMOFFSET, vmoffset.c_str(),
                        MMAP_COL_PRIVATE_CLEAN, memmaps->private_clean,
                        MMAP_COL_PRIVATE_DIRTY, memmaps->private_dirty,
                        MMAP_COL_SHARED_CLEAN, memmaps->shared_clean,
                        MMAP_COL_SHARED_DIRTY, memmaps->shared_dirty,
                        MMAP_COL_DEVICE, device.c_str(),
                        MMAP_COL_INODE, memmaps->inode,
                        -1);
}




static void
update_memmaps_dialog (MemMapsData *mmdata)
{
    GtkTreeModel *model;
    glibtop_map_entry *memmaps;
    glibtop_proc_map procmap;

    memmaps = glibtop_get_proc_map (&procmap, mmdata->info->pid);
    /* process has disappeared */
    if(!memmaps or procmap.number == 0) return;

    mmdata->format.set(memmaps[procmap.number - 1]);

    model = gtk_tree_view_get_model (GTK_TREE_VIEW (mmdata->tree));

    GtkTreeIter iter;

    typedef std::map<guint64, GtkTreeIter> IterCache;
    IterCache iter_cache;

    /*
      removes the old maps and
      also fills a cache of start -> iter in order to speed
      up add
    */

    if (gtk_tree_model_get_iter_first(model, &iter)) {
        while (true) {
            char *vmstart = 0;
            guint64 start;
            gtk_tree_model_get(model, &iter,
                               MMAP_COL_VMSTART, &vmstart,
                               -1);

            try {
                std::istringstream(vmstart) >> std::hex >> start;
            } catch (std::logic_error &e) {
                g_warning("Could not parse %s", vmstart);
                start = 0;
            }

            g_free(vmstart);

            bool found = std::binary_search(memmaps, memmaps + procmap.number,
                                            start, glibtop_map_entry_cmp());

            if (found) {
                iter_cache[start] = iter;
                if (!gtk_tree_model_iter_next(model, &iter))
                    break;
            } else {
                if (!gtk_list_store_remove(GTK_LIST_STORE(model), &iter))
                    break;
            }
        }
    }

    mmdata->devices.update();

    /*
      add the new maps
    */

    for (guint i = 0; i != procmap.number; i++) {
        GtkTreeIter iter;
        IterCache::iterator it(iter_cache.find(memmaps[i].start));

        if (it != iter_cache.end())
            iter = it->second;
        else
            gtk_list_store_prepend(GTK_LIST_STORE(model), &iter);

        update_row(model, iter, *mmdata, &memmaps[i]);
    }

    g_free (memmaps);
}



static void
dialog_response (GtkDialog * dialog, gint response_id, gpointer data)
{
    MemMapsData * const mmdata = static_cast<MemMapsData*>(data);

    g_source_remove (mmdata->timer);

    delete mmdata;
    gtk_widget_destroy (GTK_WIDGET (dialog));
}


static MemMapsData*
create_memmapsdata (ProcData *procdata)
{
    GtkWidget *tree;
    GtkListStore *model;
    guint i;

    const gchar * const titles[] = {
        N_("Filename"),
        // xgettext: virtual memory start
        N_("VM Start"),
        // xgettext: virtual memory end
        N_("VM End"),
        // xgettext: virtual memory syze
        N_("VM Size"),
        N_("Flags"),
        // xgettext: virtual memory offset
        N_("VM Offset"),
        // xgettext: memory that has not been modified since
        // it has been allocated
        N_("Private clean"),
        // xgettext: memory that has been modified since it
        // has been allocated
        N_("Private dirty"),
        // xgettext: shared memory that has not been modified
        // since it has been allocated
        N_("Shared clean"),
        // xgettext: shared memory that has been modified
        // since it has been allocated
        N_("Shared dirty"),
        N_("Device"),
        N_("Inode")
    };

    model = gtk_list_store_new (MMAP_COL_MAX,
                                G_TYPE_STRING, /* MMAP_COL_FILENAME  */
                                G_TYPE_STRING, /* MMAP_COL_VMSTART     */
                                G_TYPE_STRING, /* MMAP_COL_VMEND     */
                                G_TYPE_UINT64, /* MMAP_COL_VMSZ     */
                                G_TYPE_STRING, /* MMAP_COL_FLAGS     */
                                G_TYPE_STRING, /* MMAP_COL_VMOFFSET  */
                                G_TYPE_UINT64, /* MMAP_COL_PRIVATE_CLEAN */
                                G_TYPE_UINT64, /* MMAP_COL_PRIVATE_DIRTY */
                                G_TYPE_UINT64, /* MMAP_COL_SHARED_CLEAN */
                                G_TYPE_UINT64, /* MMAP_COL_SHARED_DIRTY */
                                G_TYPE_STRING, /* MMAP_COL_DEVICE     */
                                G_TYPE_UINT64 /* MMAP_COL_INODE     */
                                );

    tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (model));
    g_object_unref (G_OBJECT (model));

    for (i = 0; i < MMAP_COL_MAX; i++) {
        GtkCellRenderer *cell;
        GtkTreeViewColumn *col;

        cell = gtk_cell_renderer_text_new();
        col = gtk_tree_view_column_new();
        gtk_tree_view_column_pack_start(col, cell, TRUE);
        gtk_tree_view_column_set_title(col, _(titles[i]));
        gtk_tree_view_column_set_resizable(col, TRUE);
        gtk_tree_view_column_set_sort_column_id(col, i);
        gtk_tree_view_column_set_reorderable(col, TRUE);
        gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);

        switch (i) {
            case MMAP_COL_PRIVATE_CLEAN:
            case MMAP_COL_PRIVATE_DIRTY:
            case MMAP_COL_SHARED_CLEAN:
            case MMAP_COL_SHARED_DIRTY:
            case MMAP_COL_VMSZ:
                gtk_tree_view_column_set_cell_data_func(col, cell,
                                                         &procman::size_cell_data_func,
                                                        GUINT_TO_POINTER(i),
                                                        NULL);

                g_object_set(cell, "xalign", 1.0f, NULL);
                break;

            default:
                gtk_tree_view_column_set_attributes(col, cell, "text", i, NULL);
                break;
        }


        switch (i) {
            case MMAP_COL_VMSTART:
            case MMAP_COL_VMEND:
            case MMAP_COL_FLAGS:
            case MMAP_COL_VMOFFSET:
            case MMAP_COL_DEVICE:
                g_object_set(cell, "family", "monospace", NULL);
                break;
        }
    }

    return new MemMapsData(tree, procdata->settings);
}


static gboolean
memmaps_timer (gpointer data)
{
    MemMapsData * const mmdata = static_cast<MemMapsData*>(data);
    GtkTreeModel *model;

    model = gtk_tree_view_get_model (GTK_TREE_VIEW (mmdata->tree));
    g_assert(model);

    update_memmaps_dialog (mmdata);

    return TRUE;
}


static void
create_single_memmaps_dialog (GtkTreeModel *model, GtkTreePath *path,
                              GtkTreeIter *iter, gpointer data)
{
    ProcData * const procdata = static_cast<ProcData*>(data);
    MemMapsData *mmdata;
    GtkWidget *memmapsdialog;
    GtkWidget *dialog_vbox;
    GtkWidget *label;
    GtkWidget *scrolled;
    ProcInfo *info;

    gtk_tree_model_get (model, iter, COL_POINTER, &info, -1);

    if (!info)
        return;

    mmdata = create_memmapsdata (procdata);
    mmdata->info = info;

    memmapsdialog = gtk_dialog_new_with_buttons (_("Memory Maps"), GTK_WINDOW (procdata->app),
                                                 GTK_DIALOG_DESTROY_WITH_PARENT,
                                                 GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
                                                 NULL);
    gtk_window_set_resizable(GTK_WINDOW(memmapsdialog), TRUE);
    gtk_window_set_default_size(GTK_WINDOW(memmapsdialog), 620, 400);
    gtk_container_set_border_width(GTK_CONTAINER(memmapsdialog), 5);

    dialog_vbox = gtk_dialog_get_content_area (GTK_DIALOG(memmapsdialog));
    gtk_container_set_border_width (GTK_CONTAINER (dialog_vbox), 5);

    label = procman_make_label_for_mmaps_or_ofiles (
        _("_Memory maps for process \"%s\" (PID %u):"),
        info->name,
        info->pid);

    gtk_box_pack_start (GTK_BOX (dialog_vbox), label, FALSE, TRUE, 0);


    scrolled = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
                                    GTK_POLICY_AUTOMATIC,
                                    GTK_POLICY_AUTOMATIC);
    gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled),
                                         GTK_SHADOW_IN);

    gtk_container_add (GTK_CONTAINER (scrolled), mmdata->tree);
    gtk_label_set_mnemonic_widget (GTK_LABEL (label), mmdata->tree);

    gtk_box_pack_start (GTK_BOX (dialog_vbox), scrolled, TRUE, TRUE, 0);

    g_signal_connect(G_OBJECT(memmapsdialog), "response",
                              G_CALLBACK(dialog_response), mmdata);

    gtk_widget_show_all (memmapsdialog);

    mmdata->timer = g_timeout_add_seconds (5, memmaps_timer, mmdata);

    update_memmaps_dialog (mmdata);
}


void
create_memmaps_dialog (ProcData *procdata)
{
    /* TODO: do we really want to open multiple dialogs ? */
    gtk_tree_selection_selected_foreach (procdata->selection, create_single_memmaps_dialog,
                         procdata);
}