/*
 * gedit plugin for GTK+ CSS
 *
 * ©2011 Carlos Garnacho <carlosg@gnome.org>
 *
 * 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "config.h"

#include "cossa-previewer.h"
#include "cossa-style-provider.h"
#include <cairo-svg.h>
#include <gtk/gtk.h>

#define BORDER_WIDTH 10
#define PREVIEW_PADDING 10
#define SHADOW_OFFSET 5

typedef struct _CossaPreviewerPrivate CossaPreviewerPrivate;
typedef struct _SampleData SampleData;

enum {
  INVALIDATE_NONE  = 0,
  INVALIDATE_STYLE = 1 << 0,
  INVALIDATE_IMAGE = 1 << 1
};

struct _SampleData
{
  GtkWidget *widget;
  cairo_surface_t *surface;
  guint invalidation;
};

struct _CossaPreviewerPrivate
{
  CossaZoomLevel zoom_level;
  GList *samples;
  GList *cur_sample;

  GtkCssProvider *provider;
};

static void     cossa_previewer_finalize                       (GObject   *object);

static gboolean cossa_previewer_draw                           (GtkWidget *widget,
                                                                cairo_t   *cr);

static GtkSizeRequestMode cossa_previewer_get_request_mode     (GtkWidget *widget);

static void     cossa_previewer_get_preferred_width            (GtkWidget *widget,
                                                                gint      *minimum_width,
                                                                gint      *natural_width);
static void     cossa_previewer_get_preferred_height_for_width (GtkWidget *widget,
                                                                gint       width,
                                                                gint      *minimum_height,
                                                                gint      *natural_height);


G_DEFINE_TYPE (CossaPreviewer, cossa_previewer, GTK_TYPE_WIDGET)

static void
cossa_previewer_class_init (CossaPreviewerClass *klass)
{
  GtkWidgetClass *widget_class;
  GObjectClass *object_class;

  object_class = G_OBJECT_CLASS (klass);
  widget_class = GTK_WIDGET_CLASS (klass);

  object_class->finalize = cossa_previewer_finalize;

  widget_class->draw = cossa_previewer_draw;
  widget_class->get_request_mode = cossa_previewer_get_request_mode;
  widget_class->get_preferred_width = cossa_previewer_get_preferred_width;
  widget_class->get_preferred_height_for_width = cossa_previewer_get_preferred_height_for_width;

  g_type_class_add_private (klass, sizeof (CossaPreviewerPrivate));
}

static void
cossa_previewer_init (CossaPreviewer *previewer)
{
  CossaPreviewerPrivate *priv;
  GtkCssProvider *provider;
  GtkStyleContext *context;

  previewer->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (previewer,
                                                        COSSA_TYPE_PREVIEWER,
                                                        CossaPreviewerPrivate);

  gtk_widget_set_has_window (GTK_WIDGET (previewer), FALSE);
  context = gtk_widget_get_style_context (GTK_WIDGET (previewer));

  provider = gtk_css_provider_new ();
  gtk_css_provider_load_from_data (provider,
                                   ".previewer-background {\n"
                                   "  background-color: shade (@bg_color, 0.85);\n"
                                   "  border-radius: 0;\n"
                                   "}\n"
                                   "\n"
                                   ".fake-background {\n"
                                   "  background-color: @bg_color;\n"
                                   "  border-radius: 9;\n"
                                   "}\n"
                                   "\n"
                                   ".fake-shadow {\n"
                                   "  border-radius: 9;\n"
                                   "  background-image: -gtk-gradient (radial,\n"
                                   "                                   center center, 0,\n"
                                   "                                   center center, 1,\n"
                                   "                                   from (darker (@bg_color)),\n"
                                   "                                   to (alpha (darker (@bg_color), 0.6)));\n"
                                   "}\n",
                                   -1, NULL);

  gtk_style_context_add_provider (context,
                                  GTK_STYLE_PROVIDER (provider),
                                  GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  g_object_unref (provider);

  priv->zoom_level = COSSA_ZOOM_1_1;
  priv->provider = GTK_CSS_PROVIDER (cossa_style_provider_new ());
}

static SampleData *
sample_data_new (GtkWidget *widget)
{
  SampleData *data;

  data = g_slice_new0 (SampleData);
  data->widget = g_object_ref (widget);
  data->invalidation = INVALIDATE_IMAGE;

  return data;
}

static void
sample_data_free (SampleData *data)
{
  g_object_unref (data->widget);

  if (data->surface)
    cairo_surface_destroy (data->surface);

  g_slice_free (SampleData, data);
}

static void
cossa_previewer_finalize (GObject *object)
{
  CossaPreviewerPrivate *priv;

  priv = COSSA_PREVIEWER (object)->priv;
  g_object_unref (priv->provider);

  g_list_free_full (priv->samples, (GDestroyNotify) sample_data_free);

  G_OBJECT_CLASS (cossa_previewer_parent_class)->finalize (object);
}

static gboolean
cossa_previewer_draw (GtkWidget *widget,
                      cairo_t   *cr)
{
  CossaPreviewer *previewer = (CossaPreviewer *) widget;
  CossaPreviewerPrivate *priv = previewer->priv;
  GtkStyleContext *context;
  GtkAllocation allocation;
  GtkAllocation child_alloc;
  SampleData *sample;
  gint w, h;

  context = gtk_widget_get_style_context (widget);

  if (!priv->cur_sample)
    return FALSE;

  gtk_widget_get_allocation (widget, &allocation);

  sample = priv->cur_sample->data;
  gtk_widget_get_allocation (sample->widget, &child_alloc);

  cairo_save (cr);

  w = child_alloc.width * priv->zoom_level;
  h = child_alloc.height * priv->zoom_level;

  gtk_style_context_save (context);
  gtk_style_context_add_class (context, "previewer-background");
  gtk_render_background (context, cr, 0, 0, (gdouble) allocation.width, (gdouble) allocation.height);
  gtk_style_context_restore (context);

  cairo_translate (cr,
                   allocation.width / 2 - w / 2,
                   allocation.height / 2 - h / 2);

  /* Render shadow first */
  gtk_style_context_save (context);
  gtk_style_context_add_class (context, "fake-shadow");
  gtk_render_background (context, cr,
                         SHADOW_OFFSET, SHADOW_OFFSET,
                         w, h);
  gtk_style_context_restore (context);

  /* And then background */
  gtk_style_context_save (context);
  gtk_style_context_add_class (context, "fake-background");
  gtk_render_background (context, cr, 0, 0, w, h);
  gtk_style_context_restore (context);

  cairo_rectangle (cr, 0, 0, w, h);
  cairo_clip (cr);

  cairo_rectangle (cr, 0, 0, w, h);
  cairo_set_source_surface (cr, sample->surface, 0, 0);
  cairo_fill (cr);

  cairo_restore (cr);

  return FALSE;
}

static GtkSizeRequestMode
cossa_previewer_get_request_mode (GtkWidget *widget)
{
  return GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
}

static void
cossa_previewer_get_preferred_width (GtkWidget *widget,
                                     gint      *minimum_width,
                                     gint      *natural_width)
{
  CossaPreviewer *previewer = (CossaPreviewer *) widget;
  CossaPreviewerPrivate *priv = previewer->priv;

  if (priv->cur_sample)
    {
      SampleData *data = priv->cur_sample->data;

      *minimum_width = *natural_width = cairo_image_surface_get_width (data->surface);
    }
}

static void
cossa_previewer_get_preferred_height_for_width (GtkWidget *widget,
                                                gint       width,
                                                gint      *minimum_height,
                                                gint      *natural_height)
{
  CossaPreviewer *previewer = (CossaPreviewer *) widget;
  CossaPreviewerPrivate *priv = previewer->priv;

  if (priv->cur_sample)
    {
      SampleData *data = priv->cur_sample->data;

      *minimum_height = *natural_height = cairo_image_surface_get_height (data->surface);
    }
}

GtkWidget *
cossa_previewer_new (void)
{
  return g_object_new (COSSA_TYPE_PREVIEWER, NULL);
}

static void
update_sample_surface (SampleData     *sample,
                       CossaZoomLevel  zoom)
{
  GtkAllocation allocation = { 0 };
  GtkRequisition req;
  cairo_t *cr;

  if (sample->invalidation == INVALIDATE_NONE)
    return;

  if (sample->surface)
    cairo_surface_destroy (sample->surface);

  if (sample->invalidation & INVALIDATE_STYLE)
    gtk_widget_reset_style (sample->widget);

  gtk_widget_get_preferred_size (sample->widget, NULL, &req);

  allocation.width = req.width;
  allocation.height = req.height;
  gtk_widget_size_allocate (sample->widget, &allocation);

  sample->surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                req.width * zoom,
                                                req.height * zoom);

  cr = cairo_create (sample->surface);
  cairo_scale (cr, zoom, zoom);
  gtk_widget_draw (sample->widget, cr);
  cairo_destroy (cr);

  sample->invalidation = INVALIDATE_NONE;
}

static void
update_sample_style (GtkWidget        *widget,
                     GtkStyleProvider *provider)
{
  GtkStyleContext *context;

  context = gtk_widget_get_style_context (widget);

  gtk_style_context_add_provider (context, provider,
                                  GTK_STYLE_PROVIDER_PRIORITY_USER + 100);

  if (GTK_IS_CONTAINER (widget))
    gtk_container_forall (GTK_CONTAINER (widget),
                          (GtkCallback) update_sample_style,
                          provider);
}

void
cossa_previewer_add_sample (CossaPreviewer *previewer,
                            GtkWidget      *widget)
{
  CossaPreviewerPrivate *priv;
  SampleData *sample;
  GtkRequisition req;
  GtkAllocation allocation;

  g_return_if_fail (COSSA_IS_PREVIEWER (previewer));
  g_return_if_fail (GTK_IS_WIDGET (widget));

  priv = previewer->priv;
  gtk_widget_realize (widget);
  gtk_widget_show (widget);

  update_sample_style (widget, GTK_STYLE_PROVIDER (priv->provider));

  gtk_widget_get_preferred_size (widget, NULL, &req);

  allocation.x = allocation.y = 0;
  allocation.width = req.width;
  allocation.height = req.height;
  gtk_widget_size_allocate (widget, &allocation);

  sample = sample_data_new (widget);
  update_sample_surface (sample, priv->zoom_level);

  priv->samples = g_list_prepend (priv->samples, sample);

  if (!priv->cur_sample)
    {
      priv->cur_sample = priv->samples;

      if (gtk_widget_is_drawable (GTK_WIDGET (previewer)))
        gtk_widget_queue_draw (GTK_WIDGET (previewer));
    }
}

void
cossa_previewer_select_sample (CossaPreviewer *previewer,
                               GtkWidget      *sample)
{
  CossaPreviewerPrivate *priv;
  GtkWidget *widget;
  GList *samples;

  g_return_if_fail (COSSA_IS_PREVIEWER (previewer));
  g_return_if_fail (GTK_IS_WIDGET (sample));

  widget = GTK_WIDGET (previewer);
  priv = previewer->priv;
  samples = priv->samples;

  while (samples)
    {
      SampleData *data;

      data = samples->data;

      if (data->widget == sample)
        {
          priv->cur_sample = samples;
          update_sample_surface (priv->cur_sample->data, priv->zoom_level);

          if (gtk_widget_is_drawable (widget))
            gtk_widget_queue_resize (widget);

          break;
        }
      else
        samples = samples->next;
    }

  gtk_widget_queue_draw (GTK_WIDGET (previewer));
}

static void
cossa_previewer_invalidate_samples (CossaPreviewer *previewer,
				    guint           invalidation)
{
  CossaPreviewerPrivate *priv;
  GtkWidget *widget;
  GList *l;

  priv = previewer->priv;
  widget = GTK_WIDGET (previewer);

  for (l = priv->samples; l; l = l->next)
    {
      SampleData *data = l->data;

      data->invalidation |= invalidation;
    }

  if (priv->cur_sample)
    {
      update_sample_surface (priv->cur_sample->data, priv->zoom_level);
      gtk_widget_queue_resize (widget);
    }
}

void
cossa_previewer_set_zoom_level (CossaPreviewer *previewer,
                                CossaZoomLevel  zoom)
{
  CossaPreviewerPrivate *priv;

  g_return_if_fail (COSSA_IS_PREVIEWER (previewer));

  priv = previewer->priv;

  if (priv->zoom_level == zoom)
    return;

  priv->zoom_level = zoom;
  cossa_previewer_invalidate_samples (previewer, INVALIDATE_IMAGE);
}

CossaZoomLevel
cossa_previewer_get_zoom_level (CossaPreviewer *previewer)
{
  CossaPreviewerPrivate *priv;

  g_return_val_if_fail (COSSA_IS_PREVIEWER (previewer), COSSA_ZOOM_1_1);

  priv = previewer->priv;
  return priv->zoom_level;
}

void
cossa_previewer_update_samples (CossaPreviewer *previewer)
{
  g_return_if_fail (COSSA_IS_PREVIEWER (previewer));

  cossa_previewer_invalidate_samples (previewer, INVALIDATE_STYLE);
}


GtkCssProvider *
cossa_previewer_get_style (CossaPreviewer *previewer)
{
  CossaPreviewerPrivate *priv;

  g_return_val_if_fail (COSSA_IS_PREVIEWER (previewer), NULL);

  priv = previewer->priv;
  return priv->provider;
}
