/* GdkMacosLayer.c
 *
 * Copyright © 2022 Red Hat, Inc.
 *
 * This library 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 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#import "GdkMacosLayer.h"
#import "GdkMacosTile.h"

@protocol CanSetContentsOpaque
- (void)setContentsOpaque:(BOOL)mask;
@end

@implementation GdkMacosLayer

#define TILE_MAX_SIZE 128
#define TILE_EDGE_MAX_SIZE 512

static CGAffineTransform flipTransform;
static gboolean hasFlipTransform;

typedef struct
{
  GdkMacosTile          *tile;
  cairo_rectangle_int_t  cr_area;
  CGRect                 area;
  guint                  opaque : 1;
} TileInfo;

typedef struct
{
  const cairo_region_t *region;
  guint n_rects;
  guint iter;
  cairo_rectangle_int_t rect;
  cairo_rectangle_int_t stash;
  guint finished : 1;
} Tiler;

static void
tiler_init (Tiler                *tiler,
            const cairo_region_t *region)
{
  memset (tiler, 0, sizeof *tiler);

  if (region == NULL)
    {
      tiler->finished = TRUE;
      return;
    }

  tiler->region = region;
  tiler->n_rects = cairo_region_num_rectangles (region);

  if (tiler->n_rects > 0)
    cairo_region_get_rectangle (region, 0, &tiler->rect);
  else
    tiler->finished = TRUE;
}

static gboolean
tiler_next (Tiler                 *tiler,
            cairo_rectangle_int_t *tile,
            int                    max_size)
{
  if (tiler->finished)
    return FALSE;

  if (tiler->rect.width == 0 || tiler->rect.height == 0)
    {
      tiler->iter++;

      if (tiler->iter >= tiler->n_rects)
        {
          tiler->finished = TRUE;
          return FALSE;
        }

      cairo_region_get_rectangle (tiler->region, tiler->iter, &tiler->rect);
    }

  /* If the next rectangle is too tall, slice the bottom off to
   * leave just the height we want into tiler->stash.
   */
  if (tiler->rect.height > max_size)
    {
      tiler->stash = tiler->rect;
      tiler->stash.y += max_size;
      tiler->stash.height -= max_size;
      tiler->rect.height = max_size;
    }

  /* Now we can take the next horizontal slice */
  tile->x = tiler->rect.x;
  tile->y = tiler->rect.y;
  tile->height = tiler->rect.height;
  tile->width = MIN (max_size, tiler->rect.width);

  tiler->rect.x += tile->width;
  tiler->rect.width -= tile->width;

  if (tiler->rect.width == 0)
    {
      tiler->rect = tiler->stash;
      tiler->stash.width = tiler->stash.height = 0;
    }

  return TRUE;
}

static inline cairo_rectangle_int_t
fromCGRect (const CGRect rect)
{
  return (cairo_rectangle_int_t) {
    rect.origin.x,
    rect.origin.y,
    rect.size.width,
    rect.size.height
  };
}

-(id)init
{
  if (!hasFlipTransform)
    {
      hasFlipTransform = TRUE;
      flipTransform = CGAffineTransformMakeScale (1, -1);
    }

  self = [super init];

  if (self == NULL)
    return NULL;

  self->_layoutInvalid = TRUE;

  [self setContentsGravity:kCAGravityCenter];
  [self setGeometryFlipped:YES];

  return self;
}

-(BOOL)isOpaque
{
  return NO;
}

-(void)_applyLayout:(GArray *)tiles
{
  CGAffineTransform transform;
  GArray *prev;
  gboolean exhausted;
  guint j = 0;

  if (self->_isFlipped)
    transform = flipTransform;
  else
    transform = CGAffineTransformIdentity;

  prev = g_steal_pointer (&self->_tiles);
  self->_tiles = tiles;
  exhausted = prev == NULL;

  /* Try to use existing CALayer to avoid creating new layers
   * as that can be rather expensive.
   */
  for (guint i = 0; i < tiles->len; i++)
    {
      TileInfo *info = &g_array_index (tiles, TileInfo, i);

      if (!exhausted)
        {
          TileInfo *other = NULL;

          for (; j < prev->len; j++)
            {
              other = &g_array_index (prev, TileInfo, j);

              if (other->opaque == info->opaque)
                {
                  j++;
                  break;
                }

              other = NULL;
            }

          if (other != NULL)
            {
              info->tile = g_steal_pointer (&other->tile);
              [info->tile setFrame:info->area];
              [info->tile setAffineTransform:transform];
              continue;
            }
        }

      info->tile = [GdkMacosTile layer];

      [info->tile setAffineTransform:transform];
      [info->tile setOpaque:info->opaque];
      [(id<CanSetContentsOpaque>)info->tile setContentsOpaque:info->opaque];
      [info->tile setFrame:info->area];

      [self addSublayer:info->tile];
    }

  /* Release all of our old layers */
  if (prev != NULL)
    {
      for (guint i = 0; i < prev->len; i++)
        {
          TileInfo *info = &g_array_index (prev, TileInfo, i);

          if (info->tile != NULL)
            [info->tile removeFromSuperlayer];
        }

      g_array_unref (prev);
    }
}

-(void)layoutSublayers
{
  Tiler tiler;
  GArray *ar;
  cairo_region_t *transparent;
  cairo_rectangle_int_t rect;
  int max_size;

  if (!self->_inSwapBuffer)
    return;

  self->_layoutInvalid = FALSE;

  ar = g_array_sized_new (FALSE, FALSE, sizeof (TileInfo), 32);

  rect = fromCGRect ([self bounds]);
  rect.x = rect.y = 0;

  /* Calculate the transparent region (edges usually) */
  transparent = cairo_region_create_rectangle (&rect);
  if (self->_opaqueRegion)
    cairo_region_subtract (transparent, self->_opaqueRegion);

  self->_opaque = cairo_region_is_empty (transparent);

  /* If we have transparent borders around the opaque region, then
   * we are okay with a bit larger tiles since they don't change
   * all that much and are generally small in width.
   */
  if (!self->_opaque &&
      self->_opaqueRegion &&
      !cairo_region_is_empty (self->_opaqueRegion))
    max_size = TILE_EDGE_MAX_SIZE;
  else
    max_size = TILE_MAX_SIZE;

  /* Track transparent children */
  tiler_init (&tiler, transparent);
  while (tiler_next (&tiler, &rect, max_size))
    {
      TileInfo *info;

      g_array_set_size (ar, ar->len+1);

      info = &g_array_index (ar, TileInfo, ar->len-1);
      info->tile = NULL;
      info->opaque = FALSE;
      info->cr_area = rect;
      info->area = CGRectMake (rect.x, rect.y, rect.width, rect.height);
    }

  /* Track opaque children */
  tiler_init (&tiler, self->_opaqueRegion);
  while (tiler_next (&tiler, &rect, TILE_MAX_SIZE))
    {
      TileInfo *info;

      g_array_set_size (ar, ar->len+1);

      info = &g_array_index (ar, TileInfo, ar->len-1);
      info->tile = NULL;
      info->opaque = TRUE;
      info->cr_area = rect;
      info->area = CGRectMake (rect.x, rect.y, rect.width, rect.height);
    }

  cairo_region_destroy (transparent);

  [self _applyLayout:g_steal_pointer (&ar)];
  [super layoutSublayers];
}

-(void)setFrame:(NSRect)frame
{
  if (frame.size.width != self.bounds.size.width ||
      frame.size.height != self.bounds.size.height)
    {
      self->_layoutInvalid = TRUE;
      [self setNeedsLayout];
    }

  [super setFrame:frame];
}

-(void)setOpaqueRegion:(const cairo_region_t *)opaqueRegion
{
  g_clear_pointer (&self->_opaqueRegion, cairo_region_destroy);
  self->_opaqueRegion = cairo_region_copy (opaqueRegion);
  self->_layoutInvalid = TRUE;

  [self setNeedsLayout];
}

-(void)swapBuffer:(GdkMacosBuffer *)buffer withDamage:(const cairo_region_t *)damage
{
  IOSurfaceRef ioSurface = _gdk_macos_buffer_get_native (buffer);
  gboolean flipped = _gdk_macos_buffer_get_flipped (buffer);
  double scale = _gdk_macos_buffer_get_device_scale (buffer);
  double width = _gdk_macos_buffer_get_width (buffer) / scale;
  double height = _gdk_macos_buffer_get_height (buffer) / scale;

  if (flipped != self->_isFlipped)
    {
      self->_isFlipped = flipped;
      self->_layoutInvalid = TRUE;
    }

  if (self->_layoutInvalid)
    {
      self->_inSwapBuffer = TRUE;
      [self layoutSublayers];
      self->_inSwapBuffer = FALSE;
    }

  if (self->_tiles == NULL)
    return;

  for (guint i = 0; i < self->_tiles->len; i++)
    {
      const TileInfo *info = &g_array_index (self->_tiles, TileInfo, i);
      cairo_region_overlap_t overlap;
      CGRect area;
      cairo_rectangle_int_t cr_area_scaled = {
        info->cr_area.x * scale,
        info->cr_area.y * scale,
        info->cr_area.width * scale,
        info->cr_area.height * scale
      };

      overlap = cairo_region_contains_rectangle (damage, &cr_area_scaled);
      if (overlap == CAIRO_REGION_OVERLAP_OUT)
        continue;

      area.origin.x = info->area.origin.x / width;
      area.size.width = info->area.size.width / width;
      area.size.height = info->area.size.height / height;

      if (flipped)
        area.origin.y = (height - info->area.origin.y - info->area.size.height) / height;
      else
        area.origin.y = info->area.origin.y / height;

      [info->tile swapBuffer:ioSurface withRect:area];
    }
}

@end
