/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

/*
 * Copyright (C) 2001 Havoc Pennington
 *
 * 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 of the
 * License, 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "ui/theme-private.h"

#include <gtk/gtk.h>
#include <math.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

#include "core/util-private.h"
#include "meta/prefs.h"
#include "ui/frames.h"

#define DEBUG_FILL_STRUCT(s) memset ((s), 0xef, sizeof (*(s)))

static void scale_border (GtkBorder *border, double factor);

static MetaFrameLayout *
meta_frame_layout_new  (void)
{
  MetaFrameLayout *layout;

  layout = g_new0 (MetaFrameLayout, 1);

  /* Spacing as hardcoded in GTK+:
   * https://git.gnome.org/browse/gtk+/tree/gtk/gtkheaderbar.c?h=gtk-3-14#n53
   */
  layout->titlebar_spacing = 6;
  layout->has_title = TRUE;
  layout->title_scale = PANGO_SCALE_MEDIUM;
  layout->icon_size = META_MINI_ICON_WIDTH;

  return layout;
}

static void
meta_frame_layout_free (MetaFrameLayout *layout)
{
  g_return_if_fail (layout != NULL);

  DEBUG_FILL_STRUCT (layout);
  g_free (layout);
}

static void
meta_frame_layout_get_borders (const MetaFrameLayout *layout,
                               int                    text_height,
                               MetaFrameFlags         flags,
                               MetaFrameType          type,
                               MetaFrameBorders      *borders)
{
  int buttons_height, content_height, draggable_borders;
  int scale = meta_theme_get_window_scaling_factor ();

  meta_frame_borders_clear (borders);

  /* For a full-screen window, we don't have any borders, visible or not. */
  if (flags & META_FRAME_FULLSCREEN)
    return;

  g_return_if_fail (layout != NULL);

  if (!layout->has_title)
    text_height = 0;
  else
    text_height = layout->title_margin.top + text_height + layout->title_margin.bottom;

  buttons_height = MAX ((int)layout->icon_size, layout->button_min_size.height) +
    layout->button_margin.top + layout->button_border.top +
    layout->button_margin.bottom + layout->button_border.bottom;
  content_height = MAX (buttons_height, text_height);
  content_height = MAX (content_height, layout->titlebar_min_size.height) +
                   layout->titlebar_border.top + layout->titlebar_border.bottom;

  borders->visible.top    = layout->frame_border.top + content_height;
  borders->visible.left   = layout->frame_border.left;
  borders->visible.right  = layout->frame_border.right;
  borders->visible.bottom = layout->frame_border.bottom;

  borders->invisible = layout->invisible_border;

  draggable_borders = meta_prefs_get_draggable_border_width ();

  if (flags & META_FRAME_ALLOWS_HORIZONTAL_RESIZE)
    {
      borders->invisible.left   = MAX (borders->invisible.left,
                                       draggable_borders - borders->visible.left);
      borders->invisible.right  = MAX (borders->invisible.right,
                                       draggable_borders - borders->visible.right);
    }

  if (flags & META_FRAME_ALLOWS_VERTICAL_RESIZE)
    {
      borders->invisible.bottom = MAX (borders->invisible.bottom,
                                       draggable_borders - borders->visible.bottom);

      /* borders.visible.top is the height of the *title bar*. We can't do the same
       * algorithm here, titlebars are expectedly much bigger. Just subtract a couple
       * pixels to get a proper feel. */
      if (type != META_FRAME_TYPE_ATTACHED)
        borders->invisible.top    = MAX (borders->invisible.top, draggable_borders - 2);
    }

  borders->total.left   = borders->invisible.left   + borders->visible.left;
  borders->total.right  = borders->invisible.right  + borders->visible.right;
  borders->total.bottom = borders->invisible.bottom + borders->visible.bottom;
  borders->total.top    = borders->invisible.top    + borders->visible.top;

  /* Scale geometry for HiDPI, see comment in meta_frame_layout_draw_with_style() */
  scale_border (&borders->visible, scale);
  scale_border (&borders->invisible, scale);
  scale_border (&borders->total, scale);
}

int
meta_theme_get_window_scaling_factor (void)
{
  GdkScreen *screen;
  GValue value = G_VALUE_INIT;

  g_value_init (&value, G_TYPE_INT);

  screen = gdk_screen_get_default ();
  if (gdk_screen_get_setting (screen, "gdk-window-scaling-factor", &value))
    return g_value_get_int (&value);
  else
    return 1;
}

void
meta_frame_layout_apply_scale (const MetaFrameLayout *layout,
                               PangoFontDescription  *font_desc)
{
  int size = pango_font_description_get_size (font_desc);
  double scale = layout->title_scale / meta_theme_get_window_scaling_factor ();
  pango_font_description_set_size (font_desc, MAX (size * scale, 1));
}

static MetaButtonSpace*
rect_for_function (MetaFrameGeometry *fgeom,
                   MetaFrameFlags     flags,
                   MetaButtonFunction function,
                   MetaTheme         *theme)
{
  switch (function)
    {
    case META_BUTTON_FUNCTION_MENU:
      if (flags & META_FRAME_ALLOWS_MENU)
        return &fgeom->menu_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_APPMENU:
      if (flags & META_FRAME_ALLOWS_APPMENU)
        return &fgeom->appmenu_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_MINIMIZE:
      if (flags & META_FRAME_ALLOWS_MINIMIZE)
        return &fgeom->min_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_MAXIMIZE:
      if (flags & META_FRAME_ALLOWS_MAXIMIZE)
        return &fgeom->max_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_CLOSE:
      if (flags & META_FRAME_ALLOWS_DELETE)
        return &fgeom->close_rect;
      else
        return NULL;

    case META_BUTTON_FUNCTION_LAST:
      return NULL;
    }

  return NULL;
}

static gboolean
strip_button (MetaButtonSpace *func_rects[MAX_BUTTONS_PER_CORNER],
              int             *n_rects,
              MetaButtonSpace *to_strip)
{
  int i;

  i = 0;
  while (i < *n_rects)
    {
      if (func_rects[i] == to_strip)
        {
          *n_rects -= 1;

          /* shift the other rects back in the array */
          while (i < *n_rects)
            {
              func_rects[i] = func_rects[i+1];

              ++i;
            }

          func_rects[i] = NULL;

          return TRUE;
        }

      ++i;
    }

  return FALSE; /* did not strip anything */
}

static void
get_padding_and_border (GtkStyleContext *style,
                        GtkBorder       *border)
{
  GtkBorder tmp;
  GtkStateFlags state = gtk_style_context_get_state (style);

  gtk_style_context_get_border (style, state, border);
  gtk_style_context_get_padding (style, state, &tmp);

  border->left += tmp.left;
  border->top += tmp.top;
  border->right += tmp.right;
  border->bottom += tmp.bottom;
}

static void
get_min_size (GtkStyleContext *style,
              GtkRequisition  *requisition)
{
  gtk_style_context_get (style, gtk_style_context_get_state (style),
                         "min-width", &requisition->width,
                         "min-height", &requisition->height,
                         NULL);
}

static void
scale_border (GtkBorder *border,
              double     factor)
{
  border->left *= factor;
  border->right *= factor;
  border->top *= factor;
  border->bottom *= factor;
}

static void
meta_frame_layout_sync_with_style (MetaFrameLayout *layout,
                                   MetaStyleInfo   *style_info,
                                   MetaFrameFlags   flags)
{
  GtkStyleContext *style;
  GtkBorder border;
  GtkRequisition requisition;
  GdkRectangle clip_rect;
  int border_radius, max_radius;

  meta_style_info_set_flags (style_info, flags);

  style = style_info->styles[META_STYLE_ELEMENT_FRAME];
  get_padding_and_border (style, &layout->frame_border);
  scale_border (&layout->frame_border, layout->title_scale);

  gtk_render_background_get_clip (style, 0, 0, 0, 0, &clip_rect);
  layout->invisible_border.left = -clip_rect.x;
  layout->invisible_border.right = clip_rect.width + clip_rect.x;
  layout->invisible_border.top = -clip_rect.y;
  layout->invisible_border.bottom = clip_rect.height + clip_rect.y;

  if (layout->hide_buttons)
    layout->icon_size = 0;

  if (!layout->has_title && layout->hide_buttons)
    return; /* border-only - be done */

  style = style_info->styles[META_STYLE_ELEMENT_TITLEBAR];
  gtk_style_context_get (style, gtk_style_context_get_state (style),
                         "border-radius", &border_radius,
                         NULL);
  /* GTK+ currently does not allow us to look up radii of individual
   * corners; however we don't clip the client area, so with the
   * current trend of using small/no visible frame borders, most
   * themes should work fine with this.
   */
  layout->top_left_corner_rounded_radius = border_radius;
  layout->top_right_corner_rounded_radius = border_radius;
  max_radius = MIN (layout->frame_border.bottom, layout->frame_border.left);
  layout->bottom_left_corner_rounded_radius = MAX (border_radius, max_radius);
  max_radius = MIN (layout->frame_border.bottom, layout->frame_border.right);
  layout->bottom_right_corner_rounded_radius = MAX (border_radius, max_radius);

  get_min_size (style, &layout->titlebar_min_size);
  get_padding_and_border (style, &layout->titlebar_border);
  scale_border (&layout->titlebar_border, layout->title_scale);

  style = style_info->styles[META_STYLE_ELEMENT_TITLE];
  gtk_style_context_get_margin (style, gtk_style_context_get_state (style),
                                &layout->title_margin);
  scale_border (&layout->title_margin, layout->title_scale);

  style = style_info->styles[META_STYLE_ELEMENT_BUTTON];
  get_min_size (style, &layout->button_min_size);
  get_padding_and_border (style, &layout->button_border);
  scale_border (&layout->button_border, layout->title_scale);
  gtk_style_context_get_margin (style, gtk_style_context_get_state (style),
                                &layout->button_margin);
  scale_border (&layout->button_margin, layout->title_scale);

  style = style_info->styles[META_STYLE_ELEMENT_IMAGE];
  get_min_size (style, &requisition);
  get_padding_and_border (style, &border);
  scale_border (&border, layout->title_scale);

  layout->button_border.left += border.left;
  layout->button_border.right += border.right;
  layout->button_border.top += border.top;
  layout->button_border.bottom += border.bottom;

  gtk_style_context_get_margin (style, gtk_style_context_get_state (style),
                                &border);
  layout->button_border.left += border.left;
  layout->button_border.right += border.right;
  layout->button_border.top += border.top;
  layout->button_border.bottom += border.bottom;

  layout->button_min_size.width = MAX(layout->button_min_size.width,
                                      requisition.width);
  layout->button_min_size.height = MAX(layout->button_min_size.height,
                                       requisition.height);
}

static void
meta_frame_layout_calc_geometry (MetaFrameLayout        *layout,
                                 MetaStyleInfo          *style_info,
                                 int                     text_height,
                                 MetaFrameFlags          flags,
                                 int                     client_width,
                                 int                     client_height,
                                 const MetaButtonLayout *button_layout,
                                 MetaFrameType           type,
                                 MetaFrameGeometry      *fgeom,
                                 MetaTheme              *theme)
{
  int i, n_left, n_right, n_left_spacers, n_right_spacers;
  int x;
  int button_y;
  int title_right_edge;
  int width, height;
  int content_width, content_height;
  int button_width, button_height;
  int min_size_for_rounding;
  int scale = meta_theme_get_window_scaling_factor ();

  /* the left/right rects in order; the max # of rects
   * is the number of button functions
   */
  MetaButtonSpace *left_func_rects[MAX_BUTTONS_PER_CORNER];
  MetaButtonSpace *right_func_rects[MAX_BUTTONS_PER_CORNER];
  gboolean left_buttons_has_spacer[MAX_BUTTONS_PER_CORNER];
  gboolean right_buttons_has_spacer[MAX_BUTTONS_PER_CORNER];

  MetaFrameBorders borders;

  meta_frame_layout_sync_with_style (layout, style_info, flags);

  meta_frame_layout_get_borders (layout, text_height,
                                 flags, type,
                                 &borders);

  fgeom->borders = borders;

  /* Scale geometry for HiDPI, see comment in meta_frame_layout_draw_with_style() */
  fgeom->content_border = layout->frame_border;
  fgeom->content_border.left   += layout->titlebar_border.left * scale;
  fgeom->content_border.right  += layout->titlebar_border.right * scale;
  fgeom->content_border.top    += layout->titlebar_border.top * scale;
  fgeom->content_border.bottom += layout->titlebar_border.bottom * scale;

  width = client_width + borders.total.left + borders.total.right;

  height = borders.total.top + borders.total.bottom;
  if (!(flags & META_FRAME_SHADED))
    height += client_height;

  fgeom->width = width;
  fgeom->height = height;

  content_width = width -
                  (fgeom->content_border.left + borders.invisible.left) -
                  (fgeom->content_border.right + borders.invisible.right);
  content_height = borders.visible.top - fgeom->content_border.top - fgeom->content_border.bottom;

  button_width = MAX ((int)layout->icon_size, layout->button_min_size.width) +
                 layout->button_border.left + layout->button_border.right;
  button_height = MAX ((int)layout->icon_size, layout->button_min_size.height) +
                  layout->button_border.top + layout->button_border.bottom;
  button_width *= scale;
  button_height *= scale;

  /* FIXME all this code sort of pretends that duplicate buttons
   * with the same function are allowed, but that breaks the
   * code in frames.c, so isn't really allowed right now.
   * Would need left_close_rect, right_close_rect, etc.
   */

  /* Init all button rects to 0, lame hack */
  memset (ADDRESS_OF_BUTTON_RECTS (fgeom), '\0',
          LENGTH_OF_BUTTON_RECTS);

  n_left = 0;
  n_right = 0;
  n_left_spacers = 0;
  n_right_spacers = 0;

  if (!layout->hide_buttons)
    {
      /* Try to fill in rects */
      for (i = 0; i < MAX_BUTTONS_PER_CORNER && button_layout->left_buttons[i] != META_BUTTON_FUNCTION_LAST; i++)
        {
          left_func_rects[n_left] = rect_for_function (fgeom, flags,
                                                       button_layout->left_buttons[i],
                                                       theme);
          if (left_func_rects[n_left] != NULL)
            {
              left_buttons_has_spacer[n_left] = button_layout->left_buttons_has_spacer[i];
              if (button_layout->left_buttons_has_spacer[i])
                ++n_left_spacers;

              ++n_left;
            }
        }

      for (i = 0; i < MAX_BUTTONS_PER_CORNER && button_layout->right_buttons[i] != META_BUTTON_FUNCTION_LAST; i++)
        {
          right_func_rects[n_right] = rect_for_function (fgeom, flags,
                                                         button_layout->right_buttons[i],
                                                         theme);
          if (right_func_rects[n_right] != NULL)
            {
              right_buttons_has_spacer[n_right] = button_layout->right_buttons_has_spacer[i];
              if (button_layout->right_buttons_has_spacer[i])
                ++n_right_spacers;

              ++n_right;
            }
        }
    }

  /* Be sure buttons fit */
  while (n_left > 0 || n_right > 0)
    {
      int space_used_by_buttons;

      space_used_by_buttons = 0;

      space_used_by_buttons += layout->button_margin.left * scale * n_left;
      space_used_by_buttons += button_width * n_left;
      space_used_by_buttons += layout->button_margin.right * scale * n_left;
      space_used_by_buttons += (button_width * 0.75) * n_left_spacers;
      space_used_by_buttons += layout->titlebar_spacing * scale * MAX (n_left - 1, 0);

      space_used_by_buttons += layout->button_margin.left * scale * n_right;
      space_used_by_buttons += button_width * n_right;
      space_used_by_buttons += layout->button_margin.right * scale * n_right;
      space_used_by_buttons += (button_width * 0.75) * n_right_spacers;
      space_used_by_buttons += layout->titlebar_spacing * scale * MAX (n_right - 1, 0);

      if (space_used_by_buttons <= content_width)
        break; /* Everything fits, bail out */

      /* First try to remove separators */
      if (n_left_spacers > 0)
        {
          left_buttons_has_spacer[--n_left_spacers] = FALSE;
          continue;
        }
      else if (n_right_spacers > 0)
        {
          right_buttons_has_spacer[--n_right_spacers] = FALSE;
          continue;
        }

      /* Otherwise we need to shave out a button. Shave
       * min, max, close, then menu (menu is most useful);
       * prefer the default button locations.
       */
      if (strip_button (left_func_rects, &n_left, &fgeom->min_rect))
        continue;
      else if (strip_button (right_func_rects, &n_right, &fgeom->min_rect))
        continue;
      else if (strip_button (left_func_rects, &n_left, &fgeom->max_rect))
        continue;
      else if (strip_button (right_func_rects, &n_right, &fgeom->max_rect))
        continue;
      else if (strip_button (left_func_rects, &n_left, &fgeom->close_rect))
        continue;
      else if (strip_button (right_func_rects, &n_right, &fgeom->close_rect))
        continue;
      else if (strip_button (right_func_rects, &n_right, &fgeom->menu_rect))
        continue;
      else if (strip_button (left_func_rects, &n_left, &fgeom->menu_rect))
        continue;
      else if (strip_button (right_func_rects, &n_right, &fgeom->appmenu_rect))
        continue;
      else if (strip_button (left_func_rects, &n_left, &fgeom->appmenu_rect))
        continue;
      else
        {
          meta_bug ("Could not find a button to strip. n_left = %d n_right = %d\n",
                    n_left, n_right);
        }
    }

  /* Save the button layout */
  fgeom->button_layout = *button_layout;
  fgeom->n_left_buttons = n_left;
  fgeom->n_right_buttons = n_right;

  /* center buttons vertically */
  button_y = fgeom->content_border.top + borders.invisible.top +
             (content_height - button_height) / 2;

  /* right edge of farthest-right button */
  x = width - fgeom->content_border.right - borders.invisible.right;

  i = n_right - 1;
  while (i >= 0)
    {
      MetaButtonSpace *rect;

      if (x < 0) /* if we go negative, leave the buttons we don't get to as 0-width */
        break;

      x -= layout->button_margin.right * scale;

      rect = right_func_rects[i];
      rect->visible.x = x - button_width;
      if (right_buttons_has_spacer[i])
        rect->visible.x -= (button_width * 0.75);

      rect->visible.y = button_y;
      rect->visible.width = button_width;
      rect->visible.height = button_height;

      if (flags & META_FRAME_MAXIMIZED ||
          flags & META_FRAME_TILED_LEFT ||
          flags & META_FRAME_TILED_RIGHT)
        {
          rect->clickable.x = rect->visible.x;
          rect->clickable.y = 0;
          rect->clickable.width = rect->visible.width;
          rect->clickable.height = button_height + button_y;

          if (i == n_right - 1)
            rect->clickable.width += fgeom->content_border.right;

        }
      else
        g_memmove (&(rect->clickable), &(rect->visible), sizeof(rect->clickable));

      x = rect->visible.x - layout->button_margin.left * scale;

      if (i > 0)
        x -= layout->titlebar_spacing * scale;

      --i;
    }

  /* save right edge of titlebar for later use */
  title_right_edge = x;

  /* Now x changes to be position from the left and we go through
   * the left-side buttons
   */
  x = fgeom->content_border.left + borders.invisible.left;
  for (i = 0; i < n_left; i++)
    {
      MetaButtonSpace *rect;

      x += layout->button_margin.left * scale;

      rect = left_func_rects[i];

      rect->visible.x = x;
      rect->visible.y = button_y;
      rect->visible.width = button_width;
      rect->visible.height = button_height;

      if (flags & META_FRAME_MAXIMIZED)
        {
          if (i==0)
            {
              rect->clickable.x = 0;
              rect->clickable.width = button_width + x;
            }
          else
            {
              rect->clickable.x = rect->visible.x;
              rect->clickable.width = button_width;
            }

          rect->clickable.y = 0;
          rect->clickable.height = button_height + button_y;
        }
      else
        g_memmove (&(rect->clickable), &(rect->visible), sizeof(rect->clickable));

      x = rect->visible.x + rect->visible.width + layout->button_margin.right * scale;
      if (i < n_left - 1)
        x += layout->titlebar_spacing * scale;
      if (left_buttons_has_spacer[i])
        x += (button_width * 0.75);
    }

  /* Center vertically in the available content area */
  fgeom->title_rect.x = x;
  fgeom->title_rect.y = fgeom->content_border.top + borders.invisible.top +
                        (content_height - text_height) / 2;
  fgeom->title_rect.width = title_right_edge - fgeom->title_rect.x;
  fgeom->title_rect.height = text_height;

  /* Nuke title if it won't fit */
  if (fgeom->title_rect.width < 0 ||
      fgeom->title_rect.height < 0)
    {
      fgeom->title_rect.width = 0;
      fgeom->title_rect.height = 0;
    }

  if (flags & META_FRAME_SHADED)
    min_size_for_rounding = 0;
  else
    min_size_for_rounding = 5 * scale;

  fgeom->top_left_corner_rounded_radius = 0;
  fgeom->top_right_corner_rounded_radius = 0;
  fgeom->bottom_left_corner_rounded_radius = 0;
  fgeom->bottom_right_corner_rounded_radius = 0;

  if (borders.visible.top + borders.visible.left >= min_size_for_rounding)
    fgeom->top_left_corner_rounded_radius = layout->top_left_corner_rounded_radius * scale;
  if (borders.visible.top + borders.visible.right >= min_size_for_rounding)
    fgeom->top_right_corner_rounded_radius = layout->top_right_corner_rounded_radius * scale;

  if (borders.visible.bottom + borders.visible.left >= min_size_for_rounding)
    fgeom->bottom_left_corner_rounded_radius = layout->bottom_left_corner_rounded_radius * scale;
  if (borders.visible.bottom + borders.visible.right >= min_size_for_rounding)
    fgeom->bottom_right_corner_rounded_radius = layout->bottom_right_corner_rounded_radius * scale;
}

static void
get_button_rect (MetaButtonType           type,
                 const MetaFrameGeometry *fgeom,
                 GdkRectangle            *rect)
{
  switch (type)
    {
    case META_BUTTON_TYPE_CLOSE:
      *rect = fgeom->close_rect.visible;
      break;

    case META_BUTTON_TYPE_MAXIMIZE:
      *rect = fgeom->max_rect.visible;
      break;

    case META_BUTTON_TYPE_MINIMIZE:
      *rect = fgeom->min_rect.visible;
      break;

    case META_BUTTON_TYPE_MENU:
      *rect = fgeom->menu_rect.visible;
      break;

    case META_BUTTON_TYPE_APPMENU:
      *rect = fgeom->appmenu_rect.visible;
      break;

    default:
    case META_BUTTON_TYPE_LAST:
      g_assert_not_reached ();
      break;
    }
}

static const char *
get_class_from_button_type (MetaButtonType type)
{
  switch (type)
    {
    case META_BUTTON_TYPE_CLOSE:
      return "close";
    case META_BUTTON_TYPE_MAXIMIZE:
      return "maximize";
    case META_BUTTON_TYPE_MINIMIZE:
      return "minimize";
    case META_BUTTON_TYPE_APPMENU:
      return "appmenu";
    default:
      return NULL;
    }
}

static void
meta_frame_layout_draw_with_style (MetaFrameLayout         *layout,
                                   MetaStyleInfo           *style_info,
                                   cairo_t                 *cr,
                                   const MetaFrameGeometry *fgeom,
                                   PangoLayout             *title_layout,
                                   MetaFrameFlags           flags,
                                   MetaButtonState          button_states[META_BUTTON_TYPE_LAST],
                                   cairo_surface_t         *mini_icon)
{
  GtkStyleContext *style;
  GtkStateFlags state;
  MetaButtonType button_type;
  GdkRectangle visible_rect;
  GdkRectangle titlebar_rect;
  GdkRectangle button_rect;
  const MetaFrameBorders *borders;
  cairo_surface_t *frame_surface;
  double xscale, yscale;
  int scale;

  /* We opt out of GTK+/Clutter's HiDPI handling, so we have to do the scaling
   * ourselves; the nitty-gritty is a bit confusing, so here is an overview:
   *  - the values in MetaFrameLayout are always as they appear in the theme,
   *    i.e. unscaled
   *  - calculated values (borders, MetaFrameGeometry) include the scale - as
   *    the geometry is comprised of scaled decorations and the client size
   *    which we must not scale, we don't have another option
   *  - for drawing, we scale the canvas to have GTK+ render elements (borders,
   *    radii, ...) at the correct scale - as a result, we have to "unscale"
   *    the geometry again to not apply the scaling twice
   *  - As per commit e36b629c GTK expects the device scale to be set and match
   *    the final scaling or the surface caching won't take this in account
   *    breaking -gtk-scaled items.
   */
  scale = meta_theme_get_window_scaling_factor ();
  frame_surface = cairo_get_target (cr);
  cairo_surface_get_device_scale (frame_surface, &xscale, &yscale);
  cairo_surface_set_device_scale (frame_surface, scale, scale);

  borders = &fgeom->borders;

  visible_rect.x = borders->invisible.left / scale;
  visible_rect.y = borders->invisible.top / scale;
  visible_rect.width = (fgeom->width - borders->invisible.left - borders->invisible.right) / scale;
  visible_rect.height = (fgeom->height - borders->invisible.top - borders->invisible.bottom) / scale;

  meta_style_info_set_flags (style_info, flags);

  style = style_info->styles[META_STYLE_ELEMENT_FRAME];
  gtk_render_background (style, cr,
                         visible_rect.x, visible_rect.y,
                         visible_rect.width, visible_rect.height);
  gtk_render_frame (style, cr,
                    visible_rect.x, visible_rect.y,
                    visible_rect.width, visible_rect.height);

  titlebar_rect.x = visible_rect.x;
  titlebar_rect.y = visible_rect.y;
  titlebar_rect.width = visible_rect.width;
  titlebar_rect.height = borders->visible.top / scale;

  style = style_info->styles[META_STYLE_ELEMENT_TITLEBAR];
  gtk_render_background (style, cr,
                         titlebar_rect.x, titlebar_rect.y,
                         titlebar_rect.width, titlebar_rect.height);
  gtk_render_frame (style, cr,
                    titlebar_rect.x, titlebar_rect.y,
                    titlebar_rect.width, titlebar_rect.height);

  if (layout->has_title && title_layout)
    {
      PangoRectangle logical;
      int text_width, x, y;

      pango_layout_set_width (title_layout, -1);
      pango_layout_get_pixel_extents (title_layout, NULL, &logical);

      text_width = MIN(fgeom->title_rect.width / scale, logical.width);

      if (text_width < logical.width)
        pango_layout_set_width (title_layout, PANGO_SCALE * text_width);

      /* Center within the frame if possible */
      x = titlebar_rect.x + (titlebar_rect.width - text_width) / 2;
      y = titlebar_rect.y + (titlebar_rect.height - logical.height) / 2;

      if (x < fgeom->title_rect.x / scale)
        x = fgeom->title_rect.x / scale;
      else if (x + text_width > (fgeom->title_rect.x + fgeom->title_rect.width) / scale)
        x = (fgeom->title_rect.x + fgeom->title_rect.width) / scale - text_width;

      style = style_info->styles[META_STYLE_ELEMENT_TITLE];
      gtk_render_layout (style, cr, x, y, title_layout);
    }

  style = style_info->styles[META_STYLE_ELEMENT_BUTTON];
  state = gtk_style_context_get_state (style);
  for (button_type = META_BUTTON_TYPE_CLOSE; button_type < META_BUTTON_TYPE_LAST; button_type++)
    {
      const char *button_class = get_class_from_button_type (button_type);
      if (button_class)
        gtk_style_context_add_class (style, button_class);

      get_button_rect (button_type, fgeom, &button_rect);

      button_rect.x /= scale;
      button_rect.y /= scale;
      button_rect.width /= scale;
      button_rect.height /= scale;

      if (button_states[button_type] == META_BUTTON_STATE_PRELIGHT)
        gtk_style_context_set_state (style, state | GTK_STATE_PRELIGHT);
      else if (button_states[button_type] == META_BUTTON_STATE_PRESSED)
        gtk_style_context_set_state (style, state | GTK_STATE_ACTIVE);
      else
        gtk_style_context_set_state (style, state);

      cairo_save (cr);

      if (button_rect.width > 0 && button_rect.height > 0)
        {
          cairo_surface_t *surface = NULL;
          const char *icon_name = NULL;

          gtk_render_background (style, cr,
                                 button_rect.x, button_rect.y,
                                 button_rect.width, button_rect.height);
          gtk_render_frame (style, cr,
                            button_rect.x, button_rect.y,
                            button_rect.width, button_rect.height);

          switch (button_type)
            {
            case META_BUTTON_TYPE_CLOSE:
               icon_name = "window-close-symbolic";
               break;
            case META_BUTTON_TYPE_MAXIMIZE:
               if (flags & META_FRAME_MAXIMIZED)
                 icon_name = "window-restore-symbolic";
               else
                 icon_name = "window-maximize-symbolic";
               break;
            case META_BUTTON_TYPE_MINIMIZE:
               icon_name = "window-minimize-symbolic";
               break;
            case META_BUTTON_TYPE_MENU:
               icon_name = "open-menu-symbolic";
               break;
            case META_BUTTON_TYPE_APPMENU:
               surface = cairo_surface_reference (mini_icon);
               break;
            default:
               icon_name = NULL;
               break;
            }

          if (icon_name)
            {
              GtkIconTheme *theme = gtk_icon_theme_get_default ();
              GtkIconInfo *info;
              GdkPixbuf *pixbuf;

              info = gtk_icon_theme_lookup_icon_for_scale (theme, icon_name,
                                                           layout->icon_size, scale, 0);
              pixbuf = gtk_icon_info_load_symbolic_for_context (info, style, NULL, NULL);
              surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL);
            }

          if (surface)
            {
              float width, height;
              int x, y;

              width = cairo_image_surface_get_width (surface) / scale;
              height = cairo_image_surface_get_height (surface) / scale;
              x = button_rect.x + (button_rect.width - layout->icon_size) / 2;
              y = button_rect.y + (button_rect.height - layout->icon_size) / 2;

              cairo_translate (cr, x, y);
              cairo_scale (cr,
                           layout->icon_size / width,
                           layout->icon_size / height);
              cairo_set_source_surface (cr, surface, 0, 0);
              cairo_paint (cr);

              cairo_surface_destroy (surface);
            }
        }
      cairo_restore (cr);
      if (button_class)
        gtk_style_context_remove_class (style, button_class);
      gtk_style_context_set_state (style, state);
    }

  cairo_surface_set_device_scale (frame_surface, xscale, yscale);
}

/**
 * meta_theme_get_default: (skip)
 *
 */
MetaTheme*
meta_theme_get_default (void)
{
  static MetaTheme *theme = NULL;
  int frame_type;

  if (theme)
    return theme;

  theme = meta_theme_new ();

  for (frame_type = 0; frame_type < META_FRAME_TYPE_LAST; frame_type++)
    {
      MetaFrameLayout *layout = meta_frame_layout_new ();

      switch (frame_type)
        {
        case META_FRAME_TYPE_NORMAL:
        case META_FRAME_TYPE_DIALOG:
        case META_FRAME_TYPE_MODAL_DIALOG:
        case META_FRAME_TYPE_ATTACHED:
          break;
        case META_FRAME_TYPE_MENU:
        case META_FRAME_TYPE_UTILITY:
          layout->title_scale = PANGO_SCALE_SMALL;
          break;
        case META_FRAME_TYPE_BORDER:
          layout->has_title = FALSE;
          layout->hide_buttons = TRUE;
          break;
        default:
          g_assert_not_reached ();
        }

      theme->layouts[frame_type] = layout;
    }
  return theme;
}

/**
 * meta_theme_new: (skip)
 *
 */
MetaTheme*
meta_theme_new (void)
{
  return g_new0 (MetaTheme, 1);
}


void
meta_theme_free (MetaTheme *theme)
{
  int i;

  g_return_if_fail (theme != NULL);

  for (i = 0; i < META_FRAME_TYPE_LAST; i++)
    if (theme->layouts[i])
      meta_frame_layout_free (theme->layouts[i]);

  DEBUG_FILL_STRUCT (theme);
  g_free (theme);
}

MetaFrameLayout*
meta_theme_get_frame_layout (MetaTheme     *theme,
                             MetaFrameType  type)
{
  g_return_val_if_fail (type < META_FRAME_TYPE_LAST, NULL);

  return theme->layouts[type];
}

static GtkStyleContext *
create_style_context (GType            widget_type,
                      GtkStyleContext *parent_style,
                      GtkCssProvider  *provider,
                      const char      *object_name,
                      const char      *first_class,
                      ...)
{
  GtkStyleContext *style;
  GtkStateFlags state;
  GtkWidgetPath *path;
  const char *name;
  va_list ap;

  style = gtk_style_context_new ();
  gtk_style_context_set_scale (style, meta_theme_get_window_scaling_factor ());
  gtk_style_context_set_parent (style, parent_style);

  if (parent_style)
    path = gtk_widget_path_copy (gtk_style_context_get_path (parent_style));
  else
    path = gtk_widget_path_new ();

  gtk_widget_path_append_type (path, widget_type);

  if (object_name)
    gtk_widget_path_iter_set_object_name (path, -1, object_name);

  state = gtk_style_context_get_state (style);
  if (meta_get_locale_direction() == META_LOCALE_DIRECTION_RTL)
    {
      state |= GTK_STATE_FLAG_DIR_RTL;
      state &= ~GTK_STATE_FLAG_DIR_LTR;
    }
  else
    {
      state |= GTK_STATE_FLAG_DIR_LTR;
      state &= ~GTK_STATE_FLAG_DIR_RTL;
    }
  gtk_style_context_set_state (style, state);

  va_start (ap, first_class);
  for (name = first_class; name; name = va_arg (ap, const char *))
    gtk_widget_path_iter_add_class (path, -1, name);
  va_end (ap);

  gtk_style_context_set_path (style, path);
  gtk_widget_path_unref (path);

  gtk_style_context_add_provider (style, GTK_STYLE_PROVIDER (provider),
                                  GTK_STYLE_PROVIDER_PRIORITY_SETTINGS);

  return style;
}

static inline GtkCssProvider *
get_css_provider_for_theme_name (const gchar *theme_name,
                                 const gchar *variant)
{
  static GtkCssProvider *default_provider = NULL;

  if (!theme_name || *theme_name == '\0')
    {
      if (G_UNLIKELY (default_provider == NULL))
        default_provider = gtk_css_provider_new ();

      return default_provider;
    }

  return gtk_css_provider_get_named (theme_name, variant);
}

MetaStyleInfo *
meta_theme_create_style_info (GdkScreen   *screen,
                              const gchar *variant)
{
  MetaStyleInfo *style_info;
  GtkCssProvider *provider;
  char *theme_name;

  g_object_get (gtk_settings_get_for_screen (screen),
                "gtk-theme-name", &theme_name,
                NULL);

  provider = get_css_provider_for_theme_name (theme_name, variant);
  g_free (theme_name);

  style_info = g_new0 (MetaStyleInfo, 1);
  style_info->refcount = 1;

  style_info->styles[META_STYLE_ELEMENT_WINDOW] =
    create_style_context (META_TYPE_FRAMES,
                          NULL,
                          provider,
                          "window",
                          GTK_STYLE_CLASS_BACKGROUND,
                          "ssd",
                          NULL);
  style_info->styles[META_STYLE_ELEMENT_FRAME] =
    create_style_context (META_TYPE_FRAMES,
                          style_info->styles[META_STYLE_ELEMENT_WINDOW],
                          provider,
                          "decoration",
                          NULL);
  style_info->styles[META_STYLE_ELEMENT_TITLEBAR] =
    create_style_context (GTK_TYPE_HEADER_BAR,
                          style_info->styles[META_STYLE_ELEMENT_FRAME],
                          provider,
                          "headerbar",
                          GTK_STYLE_CLASS_TITLEBAR,
                          GTK_STYLE_CLASS_HORIZONTAL,
                          "default-decoration",
                          NULL);
  style_info->styles[META_STYLE_ELEMENT_TITLE] =
    create_style_context (GTK_TYPE_LABEL,
                          style_info->styles[META_STYLE_ELEMENT_TITLEBAR],
                          provider,
                          "label",
                          GTK_STYLE_CLASS_TITLE,
                          NULL);
  style_info->styles[META_STYLE_ELEMENT_BUTTON] =
    create_style_context (GTK_TYPE_BUTTON,
                          style_info->styles[META_STYLE_ELEMENT_TITLEBAR],
                          provider,
                          "button",
                          "titlebutton",
                          NULL);
  style_info->styles[META_STYLE_ELEMENT_IMAGE] =
    create_style_context (GTK_TYPE_IMAGE,
                          style_info->styles[META_STYLE_ELEMENT_BUTTON],
                          provider,
                          "image",
                          NULL);
  return style_info;
}

MetaStyleInfo *
meta_style_info_ref (MetaStyleInfo *style_info)
{
  g_return_val_if_fail (style_info != NULL, NULL);
  g_return_val_if_fail (style_info->refcount > 0, NULL);

  g_atomic_int_inc ((volatile int *)&style_info->refcount);
  return style_info;
}

void
meta_style_info_unref (MetaStyleInfo *style_info)
{
  g_return_if_fail (style_info != NULL);
  g_return_if_fail (style_info->refcount > 0);

  if (g_atomic_int_dec_and_test ((volatile int *)&style_info->refcount))
    {
      int i;
      for (i = 0; i < META_STYLE_ELEMENT_LAST; i++)
        g_object_unref (style_info->styles[i]);
      g_free (style_info);
    }
}

static void
add_toplevel_class (GtkStyleContext *style,
                    const char      *class_name)
{
  if (gtk_style_context_get_parent (style))
    {
      GtkWidgetPath *path;

      path = gtk_widget_path_copy (gtk_style_context_get_path (style));
      gtk_widget_path_iter_add_class (path, 0, class_name);
      gtk_style_context_set_path (style, path);
      gtk_widget_path_unref (path);
    }
  else
    gtk_style_context_add_class (style, class_name);
}

static void
remove_toplevel_class (GtkStyleContext *style,
                       const char      *class_name)
{
  if (gtk_style_context_get_parent (style))
    {
      GtkWidgetPath *path;

      path = gtk_widget_path_copy (gtk_style_context_get_path (style));
      gtk_widget_path_iter_remove_class (path, 0, class_name);
      gtk_style_context_set_path (style, path);
      gtk_widget_path_unref (path);
    }
  else
    gtk_style_context_remove_class (style, class_name);
}

void
meta_style_info_set_flags (MetaStyleInfo  *style_info,
                           MetaFrameFlags  flags)
{
  GtkStyleContext *style;
  const char *class_name = NULL;
  gboolean backdrop;
  GtkStateFlags state;
  int i;

  backdrop = !(flags & META_FRAME_HAS_FOCUS);
  if (flags & META_FRAME_IS_FLASHING)
    backdrop = !backdrop;

  if (flags & META_FRAME_MAXIMIZED)
    class_name = "maximized";
  else if (flags & META_FRAME_TILED_LEFT ||
           flags & META_FRAME_TILED_RIGHT)
    class_name = "tiled";

  for (i = 0; i < META_STYLE_ELEMENT_LAST; i++)
    {
      style = style_info->styles[i];

      state = gtk_style_context_get_state (style);
      if (backdrop)
        gtk_style_context_set_state (style, state | GTK_STATE_FLAG_BACKDROP);
      else
        gtk_style_context_set_state (style, state & ~GTK_STATE_FLAG_BACKDROP);

      remove_toplevel_class (style, "maximized");
      remove_toplevel_class (style, "tiled");

      if (class_name)
        add_toplevel_class (style, class_name);
    }
}

PangoFontDescription*
meta_style_info_create_font_desc (MetaStyleInfo *style_info)
{
  PangoFontDescription *font_desc;
  const PangoFontDescription *override = meta_prefs_get_titlebar_font ();
  GtkStyleContext *context = style_info->styles[META_STYLE_ELEMENT_TITLE];

  gtk_style_context_get (context,
                         gtk_style_context_get_state (context),
                         "font", &font_desc, NULL);

  if (override)
    pango_font_description_merge (font_desc, override, TRUE);

  return font_desc;
}

void
meta_theme_draw_frame (MetaTheme              *theme,
                       MetaStyleInfo          *style_info,
                       cairo_t                *cr,
                       MetaFrameType           type,
                       MetaFrameFlags          flags,
                       int                     client_width,
                       int                     client_height,
                       PangoLayout            *title_layout,
                       int                     text_height,
                       const MetaButtonLayout *button_layout,
                       MetaButtonState         button_states[META_BUTTON_TYPE_LAST],
                       cairo_surface_t        *mini_icon)
{
  MetaFrameGeometry fgeom;
  MetaFrameLayout *layout;

  g_return_if_fail (type < META_FRAME_TYPE_LAST);

  layout = theme->layouts[type];

  /* Parser is not supposed to allow this currently */
  if (layout == NULL)
    return;

  meta_frame_layout_calc_geometry (layout,
                                   style_info,
                                   text_height,
                                   flags,
                                   client_width, client_height,
                                   button_layout,
                                   type,
                                   &fgeom,
                                   theme);

  meta_frame_layout_draw_with_style (layout,
                                     style_info,
                                     cr,
                                     &fgeom,
                                     title_layout,
                                     flags,
                                     button_states,
                                     mini_icon);
}

void
meta_theme_get_frame_borders (MetaTheme        *theme,
                              MetaStyleInfo    *style_info,
                              MetaFrameType     type,
                              int               text_height,
                              MetaFrameFlags    flags,
                              MetaFrameBorders *borders)
{
  MetaFrameLayout *layout;

  g_return_if_fail (type < META_FRAME_TYPE_LAST);

  layout = theme->layouts[type];

  meta_frame_borders_clear (borders);

  /* Parser is not supposed to allow this currently */
  if (layout == NULL)
    return;

  meta_frame_layout_sync_with_style (layout, style_info, flags);

  meta_frame_layout_get_borders (layout,
                                 text_height,
                                 flags, type,
                                 borders);
}

void
meta_theme_calc_geometry (MetaTheme              *theme,
                          MetaStyleInfo          *style_info,
                          MetaFrameType           type,
                          int                     text_height,
                          MetaFrameFlags          flags,
                          int                     client_width,
                          int                     client_height,
                          const MetaButtonLayout *button_layout,
                          MetaFrameGeometry      *fgeom)
{
  MetaFrameLayout *layout;

  g_return_if_fail (type < META_FRAME_TYPE_LAST);

  layout = theme->layouts[type];

  /* Parser is not supposed to allow this currently */
  if (layout == NULL)
    return;

  meta_frame_layout_calc_geometry (layout,
                                   style_info,
                                   text_height,
                                   flags,
                                   client_width, client_height,
                                   button_layout,
                                   type,
                                   fgeom,
                                   theme);
}

/**
 * meta_pango_font_desc_get_text_height:
 * @font_desc: the font
 * @context: the context of the font
 *
 * Returns the height of the letters in a particular font.
 *
 * Returns: the height of the letters
 */
int
meta_pango_font_desc_get_text_height (const PangoFontDescription *font_desc,
                                      PangoContext         *context)
{
  PangoFontMetrics *metrics;
  PangoLanguage *lang;
  int retval;

  lang = pango_context_get_language (context);
  metrics = pango_context_get_metrics (context, font_desc, lang);

  retval = PANGO_PIXELS (pango_font_metrics_get_ascent (metrics) +
                         pango_font_metrics_get_descent (metrics));

  pango_font_metrics_unref (metrics);

  return retval;
}

/**
 * meta_frame_type_to_string:
 * @type: a #MetaFrameType
 *
 * Converts a frame type enum value to the name string that would
 * appear in the theme definition file.
 *
 * Return value: the string value
 */
const char*
meta_frame_type_to_string (MetaFrameType type)
{
  switch (type)
    {
    case META_FRAME_TYPE_NORMAL:
      return "normal";
    case META_FRAME_TYPE_DIALOG:
      return "dialog";
    case META_FRAME_TYPE_MODAL_DIALOG:
      return "modal_dialog";
    case META_FRAME_TYPE_UTILITY:
      return "utility";
    case META_FRAME_TYPE_MENU:
      return "menu";
    case META_FRAME_TYPE_BORDER:
      return "border";
    case META_FRAME_TYPE_ATTACHED:
      return "attached";
#if 0
    case META_FRAME_TYPE_TOOLBAR:
      return "toolbar";
#endif
    case  META_FRAME_TYPE_LAST:
      break;
    }

  return "<unknown>";
}
