/*
 * 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
#define SAMPLES_PER_ROW 3
#define SAMPLE_OFFSET 10

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 void
draw_sample (GtkWidget  *widget,
             SampleData *sample,
             cairo_t    *cr)
{
  CossaPreviewer *previewer = COSSA_PREVIEWER (widget);
  CossaPreviewerPrivate *priv = previewer->priv;
  GtkAllocation child_alloc;
  GtkStyleContext *context;
  gint w, h;

  context = gtk_widget_get_style_context (widget);

  gtk_widget_get_allocation (sample->widget, &child_alloc);
  w = child_alloc.width * priv->zoom_level;
  h = child_alloc.height * priv->zoom_level;

  cairo_save (cr);

  /* 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);
}

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);

  gtk_widget_get_allocation (widget, &allocation);

  cairo_save (cr);

  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);

  if (!priv->cur_sample)
    {
      GList *l;
      gint y;

      y = SAMPLE_OFFSET;
      l = priv->samples;
      while (l)
        {
          gint i;
          gint x;

          h = 0;
          x = SAMPLE_OFFSET;

          for (i = 0; i < SAMPLES_PER_ROW; i++)
            {
              sample = l->data;

              gtk_widget_get_allocation (sample->widget, &child_alloc);
              w = child_alloc.width * priv->zoom_level;
              h = MAX (h, child_alloc.height * priv->zoom_level);

              cairo_save (cr);
              cairo_translate (cr, x, y);
              draw_sample (widget, sample, cr);
              cairo_restore (cr);

              x += w + SAMPLE_OFFSET;
              l = l->next;
              if (!l)
                goto end_draw;
            }

          y += h + SAMPLE_OFFSET;
          l = l->next;
        }
    }
  else
    {
      sample = priv->cur_sample->data;

      gtk_widget_get_allocation (sample->widget, &child_alloc);
      w = child_alloc.width * priv->zoom_level;
      h = child_alloc.height * priv->zoom_level;

      cairo_translate (cr,
                       allocation.width / 2 - w / 2,
                       allocation.height / 2 - h / 2);
      draw_sample (widget, sample, cr);
    }

end_draw:
  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)
    {
      GList *sample;
      gint width = 0;
      gint row_width = 0;

      sample = priv->samples;
      while (sample)
        {
          gint i;

          row_width = 0;

          for (i = 0; i < SAMPLES_PER_ROW; i++)
            {
              SampleData *data = sample->data;

              row_width += cairo_image_surface_get_width (data->surface) + SAMPLE_OFFSET;
              sample = sample->next;
              if (!sample)
                goto end_width;
            }

          width = MAX (width, row_width);
          sample = sample->next;
        }

end_width:
      width = MAX (width, row_width);
      *minimum_width = *natural_width = width;
    }
  else
    {
      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)
    {
      GList *sample;
      gint height = 0;
      gint row_height = 0;

      sample = priv->samples;
      while (sample)
        {
          gint i;

          row_height = 0;

          for (i = 0; i < SAMPLES_PER_ROW; i++)
            {
              SampleData *data = sample->data;

              row_height = MAX (row_height, cairo_image_surface_get_height (data->surface));
              sample = sample->next;
              if (!sample)
                goto end_height;
            }

          height += row_height;
          sample = sample->next;
        }

end_height:
      height += row_height;
      *minimum_height = *natural_height = height;
    }
  else
    {
      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 && gtk_widget_is_drawable (GTK_WIDGET (previewer)))
    gtk_widget_queue_draw (GTK_WIDGET (previewer));
}

/* if @sample is %NULL all samples are shown */
void
cossa_previewer_select_sample (CossaPreviewer *previewer,
                               GtkWidget      *sample)
{
  CossaPreviewerPrivate *priv;
  GtkWidget *widget;
  GList *samples;

  g_return_if_fail (COSSA_IS_PREVIEWER (previewer));

  priv = previewer->priv;

  if (sample == NULL)
    {
      priv->cur_sample = NULL;
      gtk_widget_queue_resize (GTK_WIDGET (previewer));

      return;
    }

  widget = GTK_WIDGET (previewer);
  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 (data, priv->zoom_level);
    }

  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;
}
