/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * st-theme-node-transition.c: Theme node transitions for StWidget.
 *
 * Copyright 2010 Florian Müllner
 * Copyright 2010 Adel Gadllah
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <math.h>

#include "st-theme-node-transition.h"

enum {
  COMPLETED,
  NEW_FRAME,
  LAST_SIGNAL
};

typedef struct _StThemeNodeTransitionPrivate StThemeNodeTransitionPrivate;

struct _StThemeNodeTransition {
  GObject parent;

  StThemeNodeTransitionPrivate *priv;
};

struct _StThemeNodeTransitionPrivate {
  StThemeNode *old_theme_node;
  StThemeNode *new_theme_node;

  StThemeNodePaintState old_paint_state;
  StThemeNodePaintState new_paint_state;

  CoglTexture *old_texture;
  CoglTexture *new_texture;

  CoglFramebuffer *old_offscreen;
  CoglFramebuffer *new_offscreen;

  CoglPipeline *material;

  ClutterTimeline *timeline;

  gulong timeline_completed_id;
  gulong timeline_new_frame_id;

  ClutterActorBox last_allocation;
  ClutterActorBox offscreen_box;

  gboolean needs_setup;
};

static guint signals[LAST_SIGNAL] = { 0 };

G_DEFINE_TYPE_WITH_PRIVATE (StThemeNodeTransition, st_theme_node_transition, G_TYPE_OBJECT);


static void
on_timeline_completed (ClutterTimeline       *timeline,
                       StThemeNodeTransition *transition)
{
  g_signal_emit (transition, signals[COMPLETED], 0);
}

static void
on_timeline_new_frame (ClutterTimeline       *timeline,
                       gint                   frame_num,
                       StThemeNodeTransition *transition)
{
  g_signal_emit (transition, signals[NEW_FRAME], 0);
}

StThemeNodeTransition *
st_theme_node_transition_new (ClutterActor          *actor,
                              StThemeNode           *from_node,
                              StThemeNode           *to_node,
                              StThemeNodePaintState *old_paint_state,
                              unsigned int           duration)
{
  StThemeNodeTransition *transition;
  g_return_val_if_fail (ST_IS_THEME_NODE (from_node), NULL);
  g_return_val_if_fail (ST_IS_THEME_NODE (to_node), NULL);

  duration = st_theme_node_get_transition_duration (to_node);

  transition = g_object_new (ST_TYPE_THEME_NODE_TRANSITION, NULL);

  transition->priv->old_theme_node = g_object_ref (from_node);
  transition->priv->new_theme_node = g_object_ref (to_node);

  st_theme_node_paint_state_copy (&transition->priv->old_paint_state,
                                  old_paint_state);

  transition->priv->timeline = clutter_timeline_new_for_actor (actor, duration);

  transition->priv->timeline_completed_id =
    g_signal_connect (transition->priv->timeline, "completed",
                      G_CALLBACK (on_timeline_completed), transition);
  transition->priv->timeline_new_frame_id =
    g_signal_connect (transition->priv->timeline, "new-frame",
                      G_CALLBACK (on_timeline_new_frame), transition);

  clutter_timeline_set_progress_mode (transition->priv->timeline, CLUTTER_EASE_IN_OUT_QUAD);

  clutter_timeline_start (transition->priv->timeline);

  return transition;
}

/**
 * st_theme_node_transition_get_new_paint_state: (skip)
 *
 */
StThemeNodePaintState *
st_theme_node_transition_get_new_paint_state (StThemeNodeTransition *transition)
{
  return &transition->priv->new_paint_state;
}

void
st_theme_node_transition_update (StThemeNodeTransition *transition,
                                 StThemeNode           *new_node)
{
  StThemeNodeTransitionPrivate *priv;
  StThemeNode *old_node;
  ClutterTimelineDirection direction;

  g_return_if_fail (ST_IS_THEME_NODE_TRANSITION (transition));
  g_return_if_fail (ST_IS_THEME_NODE (new_node));

  priv = transition->priv;
  direction = clutter_timeline_get_direction (priv->timeline);
  old_node = (direction == CLUTTER_TIMELINE_FORWARD) ? priv->old_theme_node
                                                     : priv->new_theme_node;

  /* If the update is the reversal of the current transition,
   * we reverse the timeline.
   * Otherwise, we should initiate a new transition from the
   * current state to the new one; this is hard to do if the
   * transition is in an intermediate state, so we just cancel
   * the ongoing transition in that case.
   * Note that reversing a timeline before any time elapsed
   * results in the timeline's time position being set to the
   * full duration - this is not what we want, so we cancel the
   * transition as well in that case.
   */
  if (st_theme_node_equal (new_node, old_node))
    {
      {
        StThemeNodePaintState tmp;

        st_theme_node_paint_state_init (&tmp);
        st_theme_node_paint_state_copy (&tmp, &priv->old_paint_state);
        st_theme_node_paint_state_copy (&priv->old_paint_state, &priv->new_paint_state);
        st_theme_node_paint_state_copy (&priv->new_paint_state, &tmp);
        st_theme_node_paint_state_free (&tmp);
      }

      if (clutter_timeline_get_elapsed_time (priv->timeline) > 0)
        {
          if (direction == CLUTTER_TIMELINE_FORWARD)
            clutter_timeline_set_direction (priv->timeline,
                                            CLUTTER_TIMELINE_BACKWARD);
          else
            clutter_timeline_set_direction (priv->timeline,
                                            CLUTTER_TIMELINE_FORWARD);
        }
      else
        {
          clutter_timeline_stop (priv->timeline);
          g_signal_emit (transition, signals[COMPLETED], 0);
        }
    }
  else
    {
      if (clutter_timeline_get_elapsed_time (priv->timeline) > 0)
        {
          clutter_timeline_stop (priv->timeline);
          g_signal_emit (transition, signals[COMPLETED], 0);
        }
      else
        {
          guint new_duration = st_theme_node_get_transition_duration (new_node);

          clutter_timeline_set_duration (priv->timeline, new_duration);

          g_object_unref (priv->new_theme_node);
          priv->new_theme_node = g_object_ref (new_node);

          st_theme_node_paint_state_invalidate (&priv->new_paint_state);
        }
    }
}

static void
calculate_offscreen_box (StThemeNodeTransition *transition,
                         const ClutterActorBox *allocation)
{
  ClutterActorBox paint_box;

  st_theme_node_transition_get_paint_box (transition,
                                          allocation,
                                          &paint_box);
  transition->priv->offscreen_box.x1 = paint_box.x1 - allocation->x1;
  transition->priv->offscreen_box.y1 = paint_box.y1 - allocation->y1;
  transition->priv->offscreen_box.x2 = paint_box.x2 - allocation->x1;
  transition->priv->offscreen_box.y2 = paint_box.y2 - allocation->y1;
}

void
st_theme_node_transition_get_paint_box (StThemeNodeTransition *transition,
                                        const ClutterActorBox *allocation,
                                        ClutterActorBox       *paint_box)
{
  StThemeNodeTransitionPrivate *priv = transition->priv;
  ClutterActorBox old_node_box, new_node_box;

  st_theme_node_get_paint_box (priv->old_theme_node,
                               allocation,
                               &old_node_box);

  st_theme_node_get_paint_box (priv->new_theme_node,
                               allocation,
                               &new_node_box);

  paint_box->x1 = MIN (old_node_box.x1, new_node_box.x1);
  paint_box->y1 = MIN (old_node_box.y1, new_node_box.y1);
  paint_box->x2 = MAX (old_node_box.x2, new_node_box.x2);
  paint_box->y2 = MAX (old_node_box.y2, new_node_box.y2);
}

static gboolean
setup_framebuffers (StThemeNodeTransition *transition,
                    const ClutterActorBox *allocation,
                    float                  resource_scale)
{
  StThemeNodeTransitionPrivate *priv = transition->priv;
  CoglContext *ctx;
  guint width, height;
  GError *catch_error = NULL;

  /* template material to avoid unnecessary shader compilation */
  static CoglPipeline *material_template = NULL;

  ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ());
  width  = ceilf ((priv->offscreen_box.x2 - priv->offscreen_box.x1) * resource_scale);
  height = ceilf ((priv->offscreen_box.y2 - priv->offscreen_box.y1) * resource_scale);

  g_return_val_if_fail (width  > 0, FALSE);
  g_return_val_if_fail (height > 0, FALSE);

  cogl_clear_object (&priv->old_texture);
  priv->old_texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx, width, height));

  cogl_clear_object (&priv->new_texture);
  priv->new_texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx, width, height));

  if (priv->old_texture == NULL)
    return FALSE;

  if (priv->new_texture == NULL)
    return FALSE;

  cogl_clear_object (&priv->old_offscreen);
  priv->old_offscreen = COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (priv->old_texture));
  if (!cogl_framebuffer_allocate (priv->old_offscreen, &catch_error))
    {
      g_error_free (catch_error);
      cogl_clear_object (&priv->old_offscreen);
      return FALSE;
    }

  cogl_clear_object (&priv->new_offscreen);
  priv->new_offscreen = COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (priv->new_texture));
  if (!cogl_framebuffer_allocate (priv->new_offscreen, &catch_error))
    {
      g_error_free (catch_error);
      cogl_clear_object (&priv->new_offscreen);
      return FALSE;
    }

  if (priv->material == NULL)
    {
      if (G_UNLIKELY (material_template == NULL))
        {
          CoglContext *ctx =
            clutter_backend_get_cogl_context (clutter_get_default_backend ());
          material_template = cogl_pipeline_new (ctx);

          cogl_pipeline_set_layer_combine (material_template, 0,
                                           "RGBA = REPLACE (TEXTURE)",
                                           NULL);
          cogl_pipeline_set_layer_combine (material_template, 1,
                                           "RGBA = INTERPOLATE (PREVIOUS, "
                                                               "TEXTURE, "
                                                               "CONSTANT[A])",
                                           NULL);
          cogl_pipeline_set_layer_combine (material_template, 2,
                                           "RGBA = MODULATE (PREVIOUS, "
                                                            "PRIMARY)",
                                           NULL);
        }
      priv->material = cogl_pipeline_copy (material_template);
    }

  cogl_pipeline_set_layer_texture (priv->material, 0, priv->new_texture);
  cogl_pipeline_set_layer_texture (priv->material, 1, priv->old_texture);

  cogl_framebuffer_clear4f (priv->old_offscreen, COGL_BUFFER_BIT_COLOR,
                            0, 0, 0, 0);
  cogl_framebuffer_orthographic (priv->old_offscreen,
                                 priv->offscreen_box.x1,
                                 priv->offscreen_box.y1,
                                 priv->offscreen_box.x2,
                                 priv->offscreen_box.y2, 0.0, 1.0);

  st_theme_node_paint (priv->old_theme_node, &priv->old_paint_state,
                       priv->old_offscreen, allocation, 255, resource_scale);

  cogl_framebuffer_clear4f (priv->new_offscreen, COGL_BUFFER_BIT_COLOR,
                            0, 0, 0, 0);
  cogl_framebuffer_orthographic (priv->new_offscreen,
                                 priv->offscreen_box.x1,
                                 priv->offscreen_box.y1,
                                 priv->offscreen_box.x2,
                                 priv->offscreen_box.y2, 0.0, 1.0);
  st_theme_node_paint (priv->new_theme_node, &priv->new_paint_state,
                       priv->new_offscreen, allocation, 255, resource_scale);

  return TRUE;
}

void
st_theme_node_transition_paint (StThemeNodeTransition *transition,
                                CoglFramebuffer       *framebuffer,
                                ClutterActorBox       *allocation,
                                guint8                 paint_opacity,
                                float                  resource_scale)
{
  StThemeNodeTransitionPrivate *priv = transition->priv;

  CoglColor constant;
  float tex_coords[] = {
    0.0, 0.0, 1.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
  };

  g_return_if_fail (ST_IS_THEME_NODE (priv->old_theme_node));
  g_return_if_fail (ST_IS_THEME_NODE (priv->new_theme_node));

  if (!clutter_actor_box_equal (allocation, &priv->last_allocation))
    priv->needs_setup = TRUE;

  if (priv->needs_setup)
    {
      priv->last_allocation = *allocation;

      calculate_offscreen_box (transition, allocation);
      priv->needs_setup = !setup_framebuffers (transition, allocation,
                                               resource_scale);

      if (priv->needs_setup) /* setting up framebuffers failed */
        return;
    }

  cogl_color_init_from_4f (&constant, 0., 0., 0.,
                           clutter_timeline_get_progress (priv->timeline));
  cogl_pipeline_set_layer_combine_constant (priv->material, 1, &constant);

  cogl_pipeline_set_color4ub (priv->material,
                              paint_opacity, paint_opacity,
                              paint_opacity, paint_opacity);

  cogl_framebuffer_draw_multitextured_rectangle (framebuffer,
                                                 priv->material,
                                                 priv->offscreen_box.x1,
                                                 priv->offscreen_box.y1,
                                                 priv->offscreen_box.x2,
                                                 priv->offscreen_box.y2,
                                                 tex_coords, 8);
}

static void
st_theme_node_transition_dispose (GObject *object)
{
  StThemeNodeTransitionPrivate *priv = ST_THEME_NODE_TRANSITION (object)->priv;

  g_clear_object (&priv->old_theme_node);
  g_clear_object (&priv->new_theme_node);

  cogl_clear_object (&priv->old_texture);
  cogl_clear_object (&priv->new_texture);

  cogl_clear_object (&priv->old_offscreen);
  cogl_clear_object (&priv->new_offscreen);

  cogl_clear_object (&priv->material);

  if (priv->timeline)
    {
      g_clear_signal_handler (&priv->timeline_completed_id, priv->timeline);
      g_clear_signal_handler (&priv->timeline_new_frame_id, priv->timeline);

      g_clear_object (&priv->timeline);
    }

  priv->timeline_completed_id = 0;
  priv->timeline_new_frame_id = 0;

  st_theme_node_paint_state_free (&priv->old_paint_state);
  st_theme_node_paint_state_free (&priv->new_paint_state);

  G_OBJECT_CLASS (st_theme_node_transition_parent_class)->dispose (object);
}

static void
st_theme_node_transition_init (StThemeNodeTransition *transition)
{
  transition->priv = st_theme_node_transition_get_instance_private (transition);

  transition->priv->old_theme_node = NULL;
  transition->priv->new_theme_node = NULL;

  transition->priv->old_texture = NULL;
  transition->priv->new_texture = NULL;

  transition->priv->old_offscreen = NULL;
  transition->priv->new_offscreen = NULL;

  st_theme_node_paint_state_init (&transition->priv->old_paint_state);
  st_theme_node_paint_state_init (&transition->priv->new_paint_state);

  transition->priv->needs_setup = TRUE;
}

static void
st_theme_node_transition_class_init (StThemeNodeTransitionClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = st_theme_node_transition_dispose;

  signals[COMPLETED] =
    g_signal_new ("completed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0, NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  signals[NEW_FRAME] =
    g_signal_new ("new-frame",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0, NULL, NULL, NULL,
                  G_TYPE_NONE, 0);
}
