/*
 * This file Copyright (C) 2008-2009 Charles Kerr <charles@transmissionbt.com>
 *
 * This file is licensed by the GPL version 2.  Works owned by the
 * Transmission project are granted a special exemption to clause 2(b)
 * so that the bulk of its code can remain under the MIT license.
 * This exemption does not extend to derived works not owned by
 * the Transmission project.
 *
 * $Id: msgwin.c 8846 2009-07-22 15:55:48Z charles $
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <glib/gi18n.h>
#include <gtk/gtk.h>

#include <libtransmission/transmission.h>

#include "conf.h"
#include "hig.h"
#include "msgwin.h"
#include "tr-core.h"
#include "tr-prefs.h"
#include "util.h"

enum
{
    COL_SEQUENCE,
    COL_NAME,
    COL_MESSAGE,
    COL_TR_MSG,
    N_COLUMNS
};

struct MsgData
{
    TrCore *        core;
    GtkTreeView *   view;
    GtkListStore *  store;
    GtkTreeModel *  filter;
    GtkTreeModel *  sort;
    int             maxLevel;
    gboolean        isPaused;
    guint           refresh_tag;
};

static struct tr_msg_list * myTail = NULL;
static struct tr_msg_list * myHead = NULL;

/***
****
***/

static void
level_combo_changed_cb( GtkWidget * w,
                        gpointer    gdata )
{
    struct MsgData * data = gdata;
    GtkTreeIter      iter;

    if( gtk_combo_box_get_active_iter( GTK_COMBO_BOX( w ), &iter ) )
    {
        int            level = 0;
        GtkTreeModel * m = gtk_combo_box_get_model( GTK_COMBO_BOX( w ) );
        gtk_tree_model_get( m, &iter, 1, &level, -1 );

        tr_setMessageLevel( level );
        tr_core_set_pref_int( data->core, TR_PREFS_KEY_MSGLEVEL, level );
        data->maxLevel = level;
        gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( data->filter ) );
    }
}

static void
doSave( GtkWindow * parent, struct MsgData * data, const char * filename )
{
    FILE * fp = fopen( filename, "w+" );

    if( !fp )
    {
        GtkWidget * w = gtk_message_dialog_new( parent, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _( "Couldn't save \"%s\"" ), filename );
        gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ), "%s", g_strerror( errno ) );
        g_signal_connect_swapped( w, "response", G_CALLBACK( gtk_widget_destroy ), w );
        gtk_widget_show( w );
    }
    else
    {
        GtkTreeIter iter;
        GtkTreeModel * model = GTK_TREE_MODEL( data->sort );
        if( gtk_tree_model_iter_children( model, &iter, NULL ) ) do
        {
            char * date;
            const char * levelStr;
            const struct tr_msg_list * node;

            gtk_tree_model_get( model, &iter, COL_TR_MSG, &node, -1 );
            date = gtr_localtime( node->when );
            switch( node->level ) {
                case TR_MSG_DBG: levelStr = "debug"; break;
                case TR_MSG_ERR: levelStr = "error"; break;
                default:         levelStr = "     "; break;
            }
            fprintf( fp, "%s\t%s\t%s\t%s\n", date, levelStr,
                     ( node->name ? node->name : "" ),
                     ( node->message ? node->message : "" ) );
            g_free( date );
        }
        while( gtk_tree_model_iter_next( model, &iter ) );

        fclose( fp );
    }
}

static void
onSaveDialogResponse( GtkWidget * d, int response, gpointer data )
{
    if( response == GTK_RESPONSE_ACCEPT )
    {
        char * file = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER( d ) );
        doSave( GTK_WINDOW( d ), data, file );
        g_free( file );
    }

    gtk_widget_destroy( d );
}

static void
onSaveRequest( GtkWidget * w,
               gpointer    data )
{
    GtkWindow * window = GTK_WINDOW( gtk_widget_get_toplevel( w ) );
    GtkWidget * d = gtk_file_chooser_dialog_new( _( "Save Log" ), window,
                                                 GTK_FILE_CHOOSER_ACTION_SAVE,
                                                 GTK_STOCK_CANCEL,
                                                 GTK_RESPONSE_CANCEL,
                                                 GTK_STOCK_SAVE,
                                                 GTK_RESPONSE_ACCEPT,
                                                 NULL );

    gtk_dialog_set_alternative_button_order( GTK_DIALOG( d ),
                                             GTK_RESPONSE_ACCEPT,
                                             GTK_RESPONSE_CANCEL,
                                             -1 );
    g_signal_connect( d, "response",
                      G_CALLBACK( onSaveDialogResponse ), data );
    gtk_widget_show( d );
}

static void
onClearRequest( GtkWidget * w UNUSED,
                gpointer      gdata )
{
    struct MsgData * data = gdata;

    gtk_list_store_clear( data->store );
    tr_freeMessageList( myHead );
    myHead = myTail = NULL;
}

static void
onPauseToggled( GtkToggleToolButton * w,
                gpointer              gdata )
{
    struct MsgData * data = gdata;

    data->isPaused = gtk_toggle_tool_button_get_active( w );
}

static struct
{
    const char *  label;
    const char *  pref;
    int           id;
} trLevels[] = {
    { N_( "Error" ),       "error",       TR_MSG_ERR          },
    { N_( "Information" ), "info",        TR_MSG_INF          },
    { N_( "Debug" ),       "debug",       TR_MSG_DBG          },
};

static const char*
getForegroundColor( int msgLevel )
{
    const char * foreground;

    switch( msgLevel )
    {
        case TR_MSG_DBG:
            foreground = "gray"; break;

        case TR_MSG_INF:
            foreground = "black"; break;

        case TR_MSG_ERR:
            foreground = "red"; break;

        default:
            g_assert_not_reached( );
    }
    return foreground;
}

static void
renderText( GtkTreeViewColumn  * column UNUSED,
            GtkCellRenderer *           renderer,
            GtkTreeModel *              tree_model,
            GtkTreeIter *               iter,
            gpointer                    gcol )
{
    const int                  col = GPOINTER_TO_INT( gcol );
    char *                     str = NULL;
    const struct tr_msg_list * node;

    gtk_tree_model_get( tree_model, iter, col, &str, COL_TR_MSG, &node, -1 );
    g_object_set( renderer, "text", str,
                  "foreground", getForegroundColor( node->level ),
                  "ellipsize", PANGO_ELLIPSIZE_END,
                  NULL );
}

static void
renderTime( GtkTreeViewColumn  * column UNUSED,
            GtkCellRenderer *           renderer,
            GtkTreeModel *              tree_model,
            GtkTreeIter *               iter,
            gpointer             data   UNUSED )
{
    struct tm                  tm;
    char                       buf[16];
    const struct tr_msg_list * node;

    gtk_tree_model_get( tree_model, iter, COL_TR_MSG, &node, -1 );
    tm = *localtime( &node->when );
    g_snprintf( buf, sizeof( buf ), "%02d:%02d:%02d", tm.tm_hour, tm.tm_min,
                tm.tm_sec );
    g_object_set ( renderer, "text", buf,
                   "foreground", getForegroundColor( node->level ),
                   NULL );
}

static void
appendColumn( GtkTreeView * view,
              int           col )
{
    GtkCellRenderer *   r;
    GtkTreeViewColumn * c;
    const char *        title = NULL;

    switch( col )
    {
        case COL_SEQUENCE:
            title = _( "Time" ); break;

        /* noun.  column title for a list */
        case COL_NAME:
            title = _( "Name" ); break;

        /* noun.  column title for a list */
        case COL_MESSAGE:
            title = _( "Message" ); break;

        default:
            g_assert_not_reached( );
    }

    switch( col )
    {
        case COL_NAME:
            r = gtk_cell_renderer_text_new( );
            c = gtk_tree_view_column_new_with_attributes( title, r, NULL );
            gtk_tree_view_column_set_cell_data_func( c, r, renderText,
                                                     GINT_TO_POINTER(
                                                         col ), NULL );
            gtk_tree_view_column_set_sizing( c, GTK_TREE_VIEW_COLUMN_FIXED );
            gtk_tree_view_column_set_fixed_width( c, 200 );
            gtk_tree_view_column_set_resizable( c, TRUE );
            break;

        case COL_MESSAGE:
            r = gtk_cell_renderer_text_new( );
            c = gtk_tree_view_column_new_with_attributes( title, r, NULL );
            gtk_tree_view_column_set_cell_data_func( c, r, renderText,
                                                     GINT_TO_POINTER(
                                                         col ), NULL );
            gtk_tree_view_column_set_sizing( c, GTK_TREE_VIEW_COLUMN_FIXED );
            gtk_tree_view_column_set_fixed_width( c, 500 );
            gtk_tree_view_column_set_resizable( c, TRUE );
            break;

        case COL_SEQUENCE:
            r = gtk_cell_renderer_text_new( );
            c = gtk_tree_view_column_new_with_attributes( title, r, NULL );
            gtk_tree_view_column_set_cell_data_func( c, r, renderTime, NULL,
                                                     NULL );
            gtk_tree_view_column_set_resizable( c, TRUE );
            break;

        default:
            g_assert_not_reached( );
            break;
    }

    gtk_tree_view_column_set_sort_column_id( c, col );
    gtk_tree_view_append_column( view, c );
}

static gboolean
isRowVisible( GtkTreeModel * model,
              GtkTreeIter *  iter,
              gpointer       gdata )
{
    const struct MsgData *     data = gdata;
    const struct tr_msg_list * node;

    gtk_tree_model_get( model, iter, COL_TR_MSG, &node, -1 );
    return node->level <= data->maxLevel;
}

static void
onWindowDestroyed( gpointer             gdata,
                   GObject * deadWindow UNUSED )
{
    struct MsgData * data = gdata;

    g_source_remove( data->refresh_tag );
    g_free( data );
}

static tr_msg_list *
addMessages( GtkListStore *       store,
             struct tr_msg_list * head )
{
    const char *        default_name = g_get_application_name( );
    static unsigned int sequence = 1;
    tr_msg_list *       i;

    for( i = head; i; i = i->next )
    {
        GtkTreeIter unused;

        gtk_list_store_insert_with_values( store, &unused, 0,
                                           COL_TR_MSG, i,
                                           COL_NAME,
                                           ( i->name ? i->name :
                                             default_name ),
                                           COL_MESSAGE, i->message,
                                           COL_SEQUENCE, sequence++,
                                           -1 );

        if( !i->next )
            break;
    }

    return i; /* tail */
}

static gboolean
onRefresh( gpointer gdata )
{
    struct MsgData * data = gdata;

    if( !data->isPaused )
    {
        tr_msg_list * msgs = tr_getQueuedMessages( );
        if( msgs )
        {
            /* add the new messages and append them to the end of
             * our persistent list */
            tr_msg_list * tail = addMessages( data->store, msgs );
            if( myTail )
                myTail->next = msgs;
            else
                myHead = msgs;
            myTail = tail;
        }
    }
    return TRUE;
}

static GtkWidget*
debug_level_combo_new( void )
{
    unsigned int      i;
    int               ii;
    int               curlevel;
    GtkWidget *       levels;
    GtkListStore *    store;
    GtkCellRenderer * renderer;

    store = gtk_list_store_new ( 2, G_TYPE_STRING, G_TYPE_INT );

    curlevel = pref_int_get( TR_PREFS_KEY_MSGLEVEL );
    for( i = ii = 0; i < G_N_ELEMENTS( trLevels ); ++i )
    {
        GtkTreeIter iter;
        gtk_list_store_append ( store, &iter );
        gtk_list_store_set ( store, &iter, 0, _( trLevels[i].label ),
                             1, trLevels[i].id,
                             -1 );
        if( trLevels[i].id == curlevel )
            ii = i;
    }
    levels = gtk_combo_box_new_with_model ( GTK_TREE_MODEL( store ) );
    g_object_unref( G_OBJECT( store ) );
    store = NULL;

    renderer = gtk_cell_renderer_text_new ( );
    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( levels ), renderer, TRUE );
    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( levels ), renderer,
                                    "text", 0,
                                    NULL );
    gtk_combo_box_set_active( GTK_COMBO_BOX( levels ), ii );

    return levels;
}

/**
***  Public Functions
**/

GtkWidget *
msgwin_new( TrCore * core )
{
    GtkWidget *      win;
    GtkWidget *      vbox;
    GtkWidget *      toolbar;
    GtkWidget *      w;
    GtkWidget *      view;
    GtkToolItem *    item;
    struct MsgData * data;

    data = g_new0( struct MsgData, 1 );
    data->core = core;

    win = gtk_window_new( GTK_WINDOW_TOPLEVEL );
    gtk_window_set_title( GTK_WINDOW( win ), _( "Message Log" ) );
    gtk_window_set_default_size( GTK_WINDOW( win ), 560, 350 );
    gtk_window_set_role( GTK_WINDOW( win ), "message-log" );
    vbox = gtk_vbox_new( FALSE, 0 );

    /**
    ***  toolbar
    **/

    toolbar = gtk_toolbar_new( );
    gtr_toolbar_set_orientation( GTK_TOOLBAR( toolbar ), GTK_ORIENTATION_HORIZONTAL );
    gtk_toolbar_set_style( GTK_TOOLBAR( toolbar ), GTK_TOOLBAR_BOTH_HORIZ );

    item = gtk_tool_button_new_from_stock( GTK_STOCK_SAVE );
    g_object_set( G_OBJECT( item ), "is-important", TRUE, NULL );
    g_signal_connect( item, "clicked", G_CALLBACK( onSaveRequest ), data );
    gtk_toolbar_insert( GTK_TOOLBAR( toolbar ), item, -1 );

    item = gtk_tool_button_new_from_stock( GTK_STOCK_CLEAR );
    g_object_set( G_OBJECT( item ), "is-important", TRUE, NULL );
    g_signal_connect( item, "clicked", G_CALLBACK( onClearRequest ), data );
    gtk_toolbar_insert( GTK_TOOLBAR( toolbar ), item, -1 );

    item = gtk_separator_tool_item_new( );
    gtk_toolbar_insert( GTK_TOOLBAR( toolbar ), item, -1 );

    item = gtk_toggle_tool_button_new_from_stock( GTK_STOCK_MEDIA_PAUSE );
    g_object_set( G_OBJECT( item ), "is-important", TRUE, NULL );
    g_signal_connect( item, "toggled", G_CALLBACK( onPauseToggled ), data );
    gtk_toolbar_insert( GTK_TOOLBAR( toolbar ), item, -1 );

    item = gtk_separator_tool_item_new( );
    gtk_toolbar_insert( GTK_TOOLBAR( toolbar ), item, -1 );

    w = gtk_label_new( _( "Level" ) );
    gtk_misc_set_padding( GTK_MISC( w ), GUI_PAD, 0 );
    item = gtk_tool_item_new( );
    gtk_container_add( GTK_CONTAINER( item ), w );
    gtk_toolbar_insert( GTK_TOOLBAR( toolbar ), item, -1 );

    w = debug_level_combo_new( );
    g_signal_connect( w, "changed", G_CALLBACK(
                          level_combo_changed_cb ), data );
    item = gtk_tool_item_new( );
    gtk_container_add( GTK_CONTAINER( item ), w );
    gtk_toolbar_insert( GTK_TOOLBAR( toolbar ), item, -1 );

    gtk_box_pack_start( GTK_BOX( vbox ), toolbar, FALSE, FALSE, 0 );

    /**
    ***  messages
    **/

    data->store = gtk_list_store_new( N_COLUMNS,
                                      G_TYPE_UINT,       /* sequence */
                                      G_TYPE_POINTER,    /* category */
                                      G_TYPE_POINTER,    /* message */
                                      G_TYPE_POINTER );   /* struct tr_msg_list
                                                            */

    addMessages( data->store, myHead );
    onRefresh( data ); /* much faster to populate *before* it has listeners */

    data->filter = gtk_tree_model_filter_new( GTK_TREE_MODEL(
                                                  data->store ), NULL );
    data->sort = gtk_tree_model_sort_new_with_model( data->filter );
    gtk_tree_sortable_set_sort_column_id( GTK_TREE_SORTABLE( data->sort ),
                                          COL_SEQUENCE,
                                          GTK_SORT_ASCENDING );
    data->maxLevel = pref_int_get( TR_PREFS_KEY_MSGLEVEL );
    gtk_tree_model_filter_set_visible_func( GTK_TREE_MODEL_FILTER( data->
                                                                   filter ),
                                            isRowVisible, data, NULL );


    view = gtk_tree_view_new_with_model( data->sort );
    g_signal_connect( view, "button-release-event",
                      G_CALLBACK( on_tree_view_button_released ), NULL );
    data->view = GTK_TREE_VIEW( view );
    gtk_tree_view_set_rules_hint( data->view, TRUE );
    appendColumn( data->view, COL_SEQUENCE );
    appendColumn( data->view, COL_NAME );
    appendColumn( data->view, COL_MESSAGE );
    w = gtk_scrolled_window_new( NULL, NULL );
    gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( w ),
                                    GTK_POLICY_AUTOMATIC,
                                    GTK_POLICY_AUTOMATIC );
    gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( w ),
                                         GTK_SHADOW_IN );
    gtk_container_add( GTK_CONTAINER( w ), view );
    gtk_box_pack_start( GTK_BOX( vbox ), w, TRUE, TRUE, 0 );
    gtk_container_add( GTK_CONTAINER( win ), vbox );

    data->refresh_tag = gtr_timeout_add_seconds( 2, onRefresh, data );
    g_object_weak_ref( G_OBJECT( win ), onWindowDestroyed, data );

    gtk_widget_show_all( win );
    return win;
}

