#include "gskglrenderopsprivate.h"

static inline void
rgba_to_float (const GdkRGBA *c,
               float         *f)
{
  f[0] = c->red;
  f[1] = c->green;
  f[2] = c->blue;
  f[3] = c->alpha;
}

float
ops_get_scale (const RenderOpBuilder *builder)
{
  const graphene_matrix_t *mv = &builder->current_modelview;

  return MAX (graphene_matrix_get_x_scale (mv),
              graphene_matrix_get_y_scale (mv));
}

void
ops_set_program (RenderOpBuilder *builder,
                 const Program   *program)
{
  /* The tricky part about this is that we want to initialize all uniforms of a program
   * to the current value from the builder, but only once. */
  static const GskRoundedRect empty_clip;
  static const graphene_matrix_t empty_matrix;
  static const graphene_rect_t empty_rect;
  RenderOp op;

  if (builder->current_program == program)
    return;

  op.op = OP_CHANGE_PROGRAM;
  op.program = program;
  g_array_append_val (builder->render_ops, op);
  builder->current_program = program;

  /* If the projection is not yet set for this program, we use the current one. */
  if (memcmp (&empty_matrix, &builder->program_state[program->index].projection, sizeof (graphene_matrix_t)) == 0 ||
      memcmp (&builder->current_projection, &builder->program_state[program->index].projection, sizeof (graphene_matrix_t)) != 0)
    {
      op.op = OP_CHANGE_PROJECTION;
      op.projection = builder->current_projection;
      g_array_append_val (builder->render_ops, op);
      builder->program_state[program->index].projection = builder->current_projection;
    }

  if (memcmp (&empty_matrix, &builder->program_state[program->index].modelview, sizeof (graphene_matrix_t)) == 0 ||
      memcmp (&builder->current_modelview, &builder->program_state[program->index].modelview, sizeof (graphene_matrix_t)) != 0)
    {
      op.op = OP_CHANGE_MODELVIEW;
      op.modelview = builder->current_modelview;
      g_array_append_val (builder->render_ops, op);
      builder->program_state[program->index].modelview = builder->current_modelview;
    }

  if (memcmp (&empty_rect, &builder->program_state[program->index].viewport, sizeof (graphene_rect_t)) == 0 ||
      memcmp (&builder->current_viewport, &builder->program_state[program->index].viewport, sizeof (graphene_rect_t)) != 0)
    {
      op.op = OP_CHANGE_VIEWPORT;
      op.viewport = builder->current_viewport;
      g_array_append_val (builder->render_ops, op);
      builder->program_state[program->index].viewport = builder->current_viewport;
    }

  if (memcmp (&empty_clip, &builder->program_state[program->index].clip, sizeof (GskRoundedRect)) == 0 ||
      memcmp (&builder->current_clip, &builder->program_state[program->index].clip, sizeof (GskRoundedRect)) != 0)
    {
      op.op = OP_CHANGE_CLIP;
      op.clip = builder->current_clip;
      g_array_append_val (builder->render_ops, op);
      builder->program_state[program->index].clip = builder->current_clip;
    }

  if (builder->program_state[program->index].opacity != builder->current_opacity)
    {
      op.op = OP_CHANGE_OPACITY;
      op.opacity = builder->current_opacity;
      g_array_append_val (builder->render_ops, op);
      builder->program_state[program->index].opacity = builder->current_opacity;
    }
}

GskRoundedRect
ops_set_clip (RenderOpBuilder      *builder,
              const GskRoundedRect *clip)
{
  RenderOp *last_op;
  GskRoundedRect prev_clip;

  if (builder->render_ops->len > 0)
    {
      last_op = &g_array_index (builder->render_ops, RenderOp, builder->render_ops->len - 1);

      if (last_op->op == OP_CHANGE_CLIP)
        {
          last_op->clip = *clip;
        }
      else
        {
          RenderOp op;

          op.op = OP_CHANGE_CLIP;
          op.clip = *clip;
          g_array_append_val (builder->render_ops, op);
        }
    }

  if (builder->current_program != NULL)
    builder->program_state[builder->current_program->index].clip = *clip;

  prev_clip = builder->current_clip;
  builder->current_clip = *clip;

  return prev_clip;
}

graphene_matrix_t
ops_set_modelview (RenderOpBuilder         *builder,
                   const graphene_matrix_t *modelview)
{
  RenderOp op;
  graphene_matrix_t prev_mv;

  if (builder->current_program &&
      memcmp (&builder->program_state[builder->current_program->index].modelview, modelview,
              sizeof (graphene_matrix_t)) == 0)
    return *modelview;

  if (builder->render_ops->len > 0)
    {
      RenderOp *last_op = &g_array_index (builder->render_ops, RenderOp, builder->render_ops->len - 1);
      if (last_op->op == OP_CHANGE_MODELVIEW)
        {
          last_op->modelview = *modelview;
        }
      else
        {
          op.op = OP_CHANGE_MODELVIEW;
          op.modelview = *modelview;
          g_array_append_val (builder->render_ops, op);
        }
    }
  else
    {
      op.op = OP_CHANGE_MODELVIEW;
      op.modelview = *modelview;
      g_array_append_val (builder->render_ops, op);
    }

  if (builder->current_program != NULL)
    builder->program_state[builder->current_program->index].modelview = *modelview;

  prev_mv = builder->current_modelview;
  builder->current_modelview = *modelview;

  return prev_mv;
}

graphene_matrix_t
ops_set_projection (RenderOpBuilder         *builder,
                    const graphene_matrix_t *projection)
{
  RenderOp op;
  graphene_matrix_t prev_mv;

  if (builder->render_ops->len > 0)
    {
      RenderOp *last_op = &g_array_index (builder->render_ops, RenderOp, builder->render_ops->len - 1);
      if (last_op->op == OP_CHANGE_PROJECTION)
        {
          last_op->projection = *projection;
        }
      else
        {
          op.op = OP_CHANGE_PROJECTION;
          op.projection = *projection;
          g_array_append_val (builder->render_ops, op);
        }
    }
  else
    {
      op.op = OP_CHANGE_PROJECTION;
      op.projection = *projection;
      g_array_append_val (builder->render_ops, op);
    }

  if (builder->current_program != NULL)
    builder->program_state[builder->current_program->index].projection = *projection;

  prev_mv = builder->current_projection;
  builder->current_projection = *projection;

  return prev_mv;
}

graphene_rect_t
ops_set_viewport (RenderOpBuilder       *builder,
                  const graphene_rect_t *viewport)
{
  RenderOp op;
  graphene_rect_t prev_viewport;

  op.op = OP_CHANGE_VIEWPORT;
  op.viewport = *viewport;
  g_array_append_val (builder->render_ops, op);

  if (builder->current_program != NULL)
    builder->program_state[builder->current_program->index].viewport = *viewport;

  prev_viewport = builder->current_viewport;
  builder->current_viewport = *viewport;

  return prev_viewport;
}

void
ops_set_texture (RenderOpBuilder *builder,
                 int              texture_id)
{
  RenderOp op;

  if (builder->current_texture == texture_id)
    return;

  op.op = OP_CHANGE_SOURCE_TEXTURE;
  op.texture_id = texture_id;
  g_array_append_val (builder->render_ops, op);
  builder->current_texture = texture_id;
}

int
ops_set_render_target (RenderOpBuilder *builder,
                       int              render_target_id)
{
  RenderOp op;
  int prev_render_target;

  if (builder->current_render_target == render_target_id)
    return render_target_id;

  prev_render_target = builder->current_render_target;
  op.op = OP_CHANGE_RENDER_TARGET;
  op.render_target_id = render_target_id;
  g_array_append_val (builder->render_ops, op);
  builder->current_render_target = render_target_id;

  return prev_render_target;
}

float
ops_set_opacity (RenderOpBuilder *builder,
                 float            opacity)
{
  RenderOp op;
  float prev_opacity;
  RenderOp *last_op;

  if (builder->current_opacity == opacity)
    return opacity;

  if (builder->render_ops->len > 0)
    {
      last_op = &g_array_index (builder->render_ops, RenderOp, builder->render_ops->len - 1);

      if (last_op->op == OP_CHANGE_OPACITY)
        {
          last_op->opacity = opacity;
        }
      else
        {
          op.op = OP_CHANGE_OPACITY;
          op.opacity = opacity;
          g_array_append_val (builder->render_ops, op);
        }
    }
  else
    {
      op.op = OP_CHANGE_OPACITY;
      op.opacity = opacity;
      g_array_append_val (builder->render_ops, op);
    }

  prev_opacity = builder->current_opacity;
  builder->current_opacity = opacity;

  if (builder->current_program != NULL)
    builder->program_state[builder->current_program->index].opacity = opacity;

  return prev_opacity;
}

void
ops_set_color (RenderOpBuilder *builder,
               const GdkRGBA   *color)
{
  RenderOp op;

  if (gdk_rgba_equal (color, &builder->program_state[builder->current_program->index].color))
    return;

  builder->program_state[builder->current_program->index].color = *color;

  op.op = OP_CHANGE_COLOR;
  op.color = *color;
  g_array_append_val (builder->render_ops, op);
}

void
ops_set_color_matrix (RenderOpBuilder         *builder,
                      const graphene_matrix_t *matrix,
                      const graphene_vec4_t   *offset)
{
  RenderOp op;

  if (memcmp (matrix,
              &builder->program_state[builder->current_program->index].color_matrix.matrix,
              sizeof (graphene_matrix_t)) == 0 &&
      memcmp (offset,
              &builder->program_state[builder->current_program->index].color_matrix.offset,
              sizeof (graphene_vec4_t)) == 0)
    return;

  builder->program_state[builder->current_program->index].color_matrix.matrix = *matrix;
  builder->program_state[builder->current_program->index].color_matrix.offset = *offset;

  op.op = OP_CHANGE_COLOR_MATRIX;
  op.color_matrix.matrix = *matrix;
  op.color_matrix.offset = *offset;
  g_array_append_val (builder->render_ops, op);
}

void
ops_set_border (RenderOpBuilder      *builder,
                const float          *widths,
                const GskRoundedRect *outline)
{
  RenderOp op;

  /* TODO: Assert that current_program == border program? */

  if (memcmp (&builder->program_state[builder->current_program->index].border.widths,
              widths, sizeof (float) * 4) == 0 &&
      memcmp (&builder->program_state[builder->current_program->index].border.outline,
              outline, sizeof (GskRoundedRect)) == 0)
    return;

  memcpy (&builder->program_state[builder->current_program->index].border.widths,
          widths, sizeof (float) * 4);

  builder->program_state[builder->current_program->index].border.outline = *outline;

  op.op = OP_CHANGE_BORDER;
  op.border.widths[0] = widths[0];
  op.border.widths[1] = widths[1];
  op.border.widths[2] = widths[2];
  op.border.widths[3] = widths[3];
  op.border.outline = *outline;
  g_array_append_val (builder->render_ops, op);
}

void
ops_set_border_color (RenderOpBuilder *builder,
                      const GdkRGBA   *color)
{
  RenderOp op;
  op.op = OP_CHANGE_BORDER_COLOR;
  rgba_to_float (color, op.border.color);

  if (memcmp (&op.border.color, &builder->program_state[builder->current_program->index].border.color,
              sizeof (float) * 4) == 0)
    return;

  rgba_to_float (color, builder->program_state[builder->current_program->index].border.color);

  g_array_append_val (builder->render_ops, op);
}

void
ops_draw (RenderOpBuilder     *builder,
          const GskQuadVertex  vertex_data[GL_N_VERTICES])
{
  RenderOp *last_op;

  last_op = &g_array_index (builder->render_ops, RenderOp, builder->render_ops->len - 1);
  /* If the previous op was a DRAW as well, we didn't change anything between the two calls,
   * so these are just 2 subsequent draw calls. Same VAO, same program etc.
   * And the offsets into the vao are in order as well, so make it one draw call. */
  if (last_op->op == OP_DRAW)
    {
      /* We allow ourselves a little trick here. We still have to add a CHANGE_VAO op for
       * this draw call so we can add our vertex data there, but we want it to be placed before
       * the last draw call, so we reorder those. */
      RenderOp new_draw;
      new_draw.op = OP_DRAW;
      new_draw.draw.vao_offset = last_op->draw.vao_offset;
      new_draw.draw.vao_size = last_op->draw.vao_size + GL_N_VERTICES;

      last_op->op = OP_CHANGE_VAO;
      memcpy (&last_op->vertex_data, vertex_data, sizeof(GskQuadVertex) * GL_N_VERTICES);

      /* Now add the DRAW */
      g_array_append_val (builder->render_ops, new_draw);
    }
  else
    {
      const gsize n_ops = builder->render_ops->len;
      RenderOp *op;
      gsize offset = builder->buffer_size / sizeof (GskQuadVertex);

      /* We will add two render ops here. */
      g_array_set_size (builder->render_ops, n_ops + 2);

      op = &g_array_index (builder->render_ops, RenderOp, n_ops);
      op->op = OP_CHANGE_VAO;
      memcpy (&op->vertex_data, vertex_data, sizeof(GskQuadVertex) * GL_N_VERTICES);

      op = &g_array_index (builder->render_ops, RenderOp, n_ops + 1);
      op->op = OP_DRAW;
      op->draw.vao_offset = offset;
      op->draw.vao_size = GL_N_VERTICES;
    }

  /* We added new vertex data in both cases so increase the buffer size */
  builder->buffer_size += sizeof (GskQuadVertex) * GL_N_VERTICES;
}

void
ops_offset (RenderOpBuilder *builder,
            float            x,
            float            y)
{
  builder->dx += x;
  builder->dy += y;
}

void
ops_add (RenderOpBuilder *builder,
         const RenderOp  *op)
{
  g_array_append_val (builder->render_ops, *op);
}
