/*
 * Copyright (C) 2002-2006 the xine-project
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 *
 * log window implementation
 */

#include "globals.h"

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

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

#include "log_window.h"
#include "utils.h"

#define MAX_TEXT 16

typedef struct {
  GtkTextView *view;
  GtkAdjustment *scroll;
  GtkTextMark *mark;
} log_t;

static GtkWidget *win, *tabs;
static log_t texts[MAX_TEXT + 1];
static int sections;
static gboolean is_visible = FALSE;
static gboolean have_log_cb;

static GAsyncQueue *log_queue;

#define CONSOLE_TEXT	sections
#define console		texts[CONSOLE_TEXT].view
#define console_scroll	texts[CONSOLE_TEXT].scroll

static inline void scroll_to_bottom (int section)
{
  gtk_text_view_scroll_mark_onscreen (texts[section].view, texts[section].mark);
}

static void adj_changed_cb (GtkAdjustment *adj, gpointer data)
{
  int section = (int)(intptr_t)data;
  GtkAllocation  allocation = { 0 };
  GtkWidget     *widget = GTK_WIDGET (texts[section].view);

  gtk_widget_get_allocation (widget, &allocation);

  if (!gtk_widget_get_realized (widget)
      || gtk_adjustment_get_value(adj) + allocation.height >= gtk_adjustment_get_upper(adj) - 6)
    scroll_to_bottom (section);
}

#define CROP_MARK "…\n"
#define BUFFER_LIMIT (4 * 1024 * 1024 - sizeof (CROP_MARK))

static void log_clip (GtkTextBuffer *buffer)
{
  gint count = gtk_text_buffer_get_char_count (buffer);
  if (count > BUFFER_LIMIT)
  {
    GtkTextIter start, end;
    gtk_text_buffer_get_start_iter (buffer, &start);
    gtk_text_buffer_get_iter_at_offset (buffer, &end, count - BUFFER_LIMIT);
    gint line = gtk_text_iter_get_line (&end);
    gtk_text_buffer_get_iter_at_line (buffer, &end, line + 1);
    gtk_text_buffer_delete (buffer, &start, &end);
    gtk_text_buffer_insert (buffer, &start, CROP_MARK, -1);
  }
}

static void log_do_insert (GtkTextBuffer *buffer, GtkTextIter *pos,
			   const char *text, ssize_t length, const char *tag)
{
  if (tag)
    gtk_text_buffer_insert_with_tags_by_name (buffer, pos, text, length, tag, NULL);
  else
    gtk_text_buffer_insert (buffer, pos, text, length);
}

static void log_insert (GtkTextBuffer *buffer, GtkTextIter *pos,
			const char *text, const char *tag)
{
  const gchar *end = text;
  while (*end)
  {
    text = end;
    g_utf8_validate (end, -1, &end);
    log_do_insert (buffer, pos, text, end - text, tag);
    if (*end)
    {
      log_do_insert (buffer, pos, "�", -1, tag);
      while (*end && (*++end & 0x7F) < 0x40)
	/* skip 0x80..0xBF */;
    }
  }
}

static void add_log_line (GtkTextBuffer *buffer, GtkTextIter *pos,
			  const char *text, gint length)
{
  const char *colon = strstr (text, ": ");
  if (colon)
    gtk_text_buffer_insert_with_tags_by_name
      (buffer, pos, text, ++colon - text, "datestamp", NULL);
  else
    colon = text;
  log_insert (buffer, pos, colon, NULL);
}

typedef struct {
  GtkTextBuffer *buffer;
  int section;
  char text[0];
} update_log_t;

static void * __attribute__ ((noreturn))
log_update_thread (void *data)
{
  for (;;)
  {
    update_log_t *item = g_async_queue_pop (log_queue);
    GtkTextIter end;
    gdk_threads_enter ();
    gtk_text_buffer_get_end_iter (item->buffer, &end);
    add_log_line (item->buffer, &end, item->text, -1);
    log_clip (item->buffer);
    gdk_threads_leave ();
    free (item);
  }
  return NULL; /* shut up, gcc */
}

static void update_log_cb (void *data, int section)
{
  GtkTextBuffer *buffer = gtk_text_view_get_buffer (texts[section].view);
  const char *const *log = (const char * const *)xine_get_log (xine, section);

  if (log && log[0])
  {
    update_log_t *cb = malloc (sizeof (update_log_t) + strlen (log[0]) + 1);
    cb->buffer = buffer;
    cb->section = section;
    strcpy (cb->text, log[0]);
    g_async_queue_push (log_queue, cb);
  }
}

static inline void update_log_pane (int section)
{
  GtkTextBuffer *buffer = gtk_text_view_get_buffer (texts[section].view);
  GtkTextIter start, end;

  gtk_text_buffer_get_start_iter (buffer, &start);
  gtk_text_buffer_get_end_iter (buffer, &end);
  gtk_text_buffer_delete (buffer, &start, &end);

  const char *const *log = (const char * const *)xine_get_log (xine, section);

  if (log)
  {
    int j = 0;
    while (log[j])
      ++j;
    for (--j; j >= 0; --j)
      add_log_line (buffer, &end, log[j], -1);
    scroll_to_bottom (section);
  }
}

static void update_log_window (void)
{
  int i;
  for (i = 0; i < sections; ++i)
    update_log_pane (i);
}

static void response_cb (GtkDialog *dbox, int response, gpointer data)
{
  switch (response)
  {
  case GTK_RESPONSE_NO:
    {
      guint page = gtk_notebook_get_current_page ((GtkNotebook *)tabs);
      GtkTextBuffer *buffer = gtk_text_view_get_buffer (texts[page].view);
      GtkTextIter start, end;
      gtk_text_buffer_get_start_iter (buffer, &start);
      gtk_text_buffer_get_end_iter (buffer, &end);
      gtk_text_buffer_delete (buffer, &start, &end);
    }
    break;
  case GTK_RESPONSE_YES:
    update_log_window ();
    break;
  default:
    is_visible = FALSE;
    gtk_widget_hide (win);
  }
}

static void switch_cb (GtkNotebook *notebook, gpointer *page,
		       guint page_no, gpointer data)
{
  gboolean is_cp = (page_no == (guint)(intptr_t)data);
  gtk_dialog_set_response_sensitive ((GtkDialog *) win, GTK_RESPONSE_NO, is_cp);
  gtk_dialog_set_response_sensitive ((GtkDialog *) win, GTK_RESPONSE_YES, !is_cp);
}

static JSBool js_log_show (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall ("log_show");
  log_window_show ();
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

struct console_buffer_s {
  struct console_buffer_s *next;
  int err;
  char line[0];
};
typedef struct console_buffer_s console_buffer_t;

static console_buffer_t *console_buffer = NULL, *console_buffer_tail = NULL;
static pthread_mutex_t log_console_lock = PTHREAD_MUTEX_INITIALIZER;

static void log_console_text_internal (const char *line, int err)
{
  GtkTextBuffer *buffer = gtk_text_view_get_buffer (console);
  GtkTextIter end;

  gdk_threads_enter ();
  gtk_text_buffer_get_end_iter (buffer, &end);
  log_insert (buffer, &end, line, "error");
  log_clip (buffer);
  gdk_threads_leave ();
}

void log_console_text (const char *line, int err)
{
  pthread_mutex_lock (&log_console_lock);
  if (console)
    log_console_text_internal (line, err);
  else
  {
    console_buffer_t *cb = malloc (sizeof (console_buffer_t) + strlen (line) + 1);
    cb->next = NULL;
    cb->err = err;
    strcpy (cb->line, line);
    if (console_buffer_tail)
      console_buffer_tail->next = cb;
    console_buffer_tail = cb;
    if (!console_buffer)
      console_buffer = cb;
  }
  pthread_mutex_unlock (&log_console_lock);
}

static GtkWidget *log_buffer_new (int section)
{
  log_t *const ret = &texts[section];
  GtkTextView *view = ret->view = GTK_TEXT_VIEW(gtk_text_view_new ());
  gtk_text_view_set_editable (view, FALSE);
  gtk_text_view_set_cursor_visible (view, FALSE);
  gtk_text_view_set_left_margin (view, 6);
  gtk_text_view_set_right_margin (view, 2);
  gtk_text_view_set_pixels_above_lines (view, 1);
  gtk_text_view_set_pixels_below_lines (view, 1);
  gtk_text_view_set_pixels_inside_wrap (view, 0);
  gtk_text_view_set_indent (view, -4);
  gtk_text_view_set_wrap_mode (view, GTK_WRAP_WORD_CHAR);

  GtkWidget *scroller = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroller),
				  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET(view));

  ret->scroll = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scroller));
  g_signal_connect ((GObject *)ret->scroll, "changed",
		    (GCallback)adj_changed_cb, (gpointer)(intptr_t)section);

  GtkTextIter end;
  GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);
  gtk_text_buffer_get_end_iter (buffer, &end);
  ret->mark = gtk_text_buffer_create_mark (buffer, NULL, &end, FALSE);

  return scroller;
}

void log_window_init (void)
{
  have_log_cb = xine_check_version (1, 1, 13);

  /* window */

  win = gtk_dialog_new_with_buttons (_("xine engine log output"), NULL, 0,
				GTK_STOCK_CLEAR, GTK_RESPONSE_NO,
				NULL);
  if (have_log_cb)
    xine_register_log_cb (xine, update_log_cb, NULL);
  else
    gtk_dialog_add_button (GTK_DIALOG (win), GTK_STOCK_REFRESH, GTK_RESPONSE_YES);
  gtk_dialog_add_button (GTK_DIALOG (win), GTK_STOCK_CLOSE, GTK_RESPONSE_DELETE_EVENT);

  g_signal_connect (G_OBJECT(win), "response", G_CALLBACK (response_cb), NULL);
  gtk_window_set_default_size (GTK_WINDOW (win), 640, 400);
  hide_on_delete (win, &is_visible);

  /* notebook */

  sections = xine_get_log_section_count (xine);

  tabs = gtk_notebook_new();
  if (!have_log_cb)
    g_signal_connect (G_OBJECT(tabs), "switch-page",
		      G_CALLBACK (switch_cb), (gpointer)(intptr_t)sections);

  const char *const *names = xine_get_log_names (xine);
  int i;
  for (i = 0; i < sections; ++i)
  {
    gtk_notebook_append_page (GTK_NOTEBOOK (tabs), log_buffer_new (i),
			      gtk_label_new (names[i]));
    gtk_text_buffer_create_tag (gtk_text_view_get_buffer (texts[i].view),
				"datestamp",
				"family", "mono",
				"weight", PANGO_WEIGHT_BOLD,
				NULL);
  }

  pthread_mutex_lock (&log_console_lock);
  gtk_notebook_append_page (GTK_NOTEBOOK (tabs), log_buffer_new (CONSOLE_TEXT),
			    gtk_label_new (_("console")));
  gtk_text_buffer_create_tag (gtk_text_view_get_buffer (console),
			      "error",
			      "style", PANGO_STYLE_ITALIC,
			      NULL);
  console_buffer_t *cb = console_buffer;
  while (cb)
  {
    console_buffer_t *next = cb->next;
    log_console_text_internal (cb->line, cb->err);
    free (cb);
    cb = next;
  }
  pthread_mutex_unlock (&log_console_lock);

  update_log_window (); /* just in case */

  if (have_log_cb)
  {
    pthread_t thread;
    pthread_attr_t attr;
    log_queue = g_async_queue_new ();
    pthread_attr_init (&attr);
    pthread_attr_setstacksize (&attr, 128 * 1024);
    if (pthread_create (&thread, &attr, log_update_thread, NULL))
    {
      perror ("gxine: logging thread failed");
      exit (2);
    }
  }

  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (win))),
                      tabs, TRUE, TRUE, 2);

  /* script engine functions */

  se_defun (gse, NULL, "log_show", js_log_show, 0, JSFUN_FAST_NATIVE,
	    SE_GROUP_DIALOGUE, NULL, NULL);
}

void log_window_show (void)
{
  if (is_visible)
  {
    is_visible = FALSE;
    gtk_widget_hide (win);
  }
  else
  {
    is_visible = TRUE;
    window_show (win, NULL);
    if (!have_log_cb)
      update_log_window ();
  }
}
