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

/* This file started as a cut-and-paste of cr-sel-eng.c from libcroco.
 *
 * In moving it to hippo-canvas:
 * - Reformatted and otherwise edited to match our coding style
 * - Switched from handling xmlNode to handling HippoStyle
 * - Simplified by removing things that we don't need or that don't
 *   make sense in our context.
 * - The code to get a list of matching properties works quite differently;
 *   we order things in priority order, but we don't actually try to
 *   coalesce properties with the same name.
 *
 * In moving it to GNOME Shell:
 *  - Renamed again to StTheme
 *  - Reformatted to match the gnome-shell coding style
 *  - Removed notion of "theme engine" from hippo-canvas
 *  - pseudo-class matching changed from link enum to strings
 *  - Some code simplification
 *
 * In moving it to libeek:
 *  - Renamed again to EekTheme
 */

/*
 * This file is part of The Croco Library
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2.1 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * 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 Lesser
 * 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
 *
 * Copyright (C) 2003-2004 Dodji Seketeli.  All Rights Reserved.
 */


#include <stdlib.h>
#include <string.h>

#include <libcroco/libcroco.h>
#include <gio/gio.h>

#include "eek-theme.h"
#include "eek-theme-node.h"

static GObject *eek_theme_constructor (GType                  type,
                                      guint                  n_construct_properties,
                                      GObjectConstructParam *construct_properties);

static void eek_theme_finalize     (GObject      *object);
static void eek_theme_set_property (GObject      *object,
                                    guint         prop_id,
                                    const GValue *value,
                                    GParamSpec   *pspec);
static void eek_theme_get_property (GObject      *object,
                                    guint         prop_id,
                                    GValue       *value,
                                    GParamSpec   *pspec);
static char *eek_theme_resolve_url (EekTheme     *theme,
                                    CRStyleSheet *base_stylesheet,
                                    const char   *url);

struct _EekTheme
{
  GObject parent;

  char *application_stylesheet;
  char *default_stylesheet;
  char *theme_stylesheet;
  GSList *custom_stylesheets;

  GHashTable *stylesheets_by_filename;
  GHashTable *filenames_by_stylesheet;

  CRCascade *cascade;
};

struct _EekThemeClass
{
  GObjectClass parent_class;
};

enum
{
  PROP_0,
  PROP_APPLICATION_STYLESHEET,
  PROP_THEME_STYLESHEET,
  PROP_DEFAULT_STYLESHEET
};

G_DEFINE_TYPE (EekTheme, eek_theme, G_TYPE_OBJECT);

/* Quick strcmp.  Test only for == 0 or != 0, not < 0 or > 0.  */
#define strqcmp(str,lit,lit_len) \
  (strlen (str) != (lit_len) || memcmp (str, lit, lit_len))

static void
eek_theme_init (EekTheme *theme)
{
  theme->stylesheets_by_filename = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                          (GDestroyNotify)g_free, (GDestroyNotify)cr_stylesheet_unref);
  theme->filenames_by_stylesheet = g_hash_table_new (g_direct_hash, g_direct_equal);
}

static void
eek_theme_class_init (EekThemeClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructor = eek_theme_constructor;
  object_class->finalize = eek_theme_finalize;
  object_class->set_property = eek_theme_set_property;
  object_class->get_property = eek_theme_get_property;

  /**
   * EekTheme:application-stylesheet:
   *
   * The highest priority stylesheet, representing application-specific
   * styling; this is associated with the CSS "author" stylesheet.
   */
  g_object_class_install_property (object_class,
                                   PROP_APPLICATION_STYLESHEET,
                                   g_param_spec_string ("application-stylesheet",
                                                        "Application Stylesheet",
                                                        "Stylesheet with application-specific styling",
                                                        NULL,
                                                        G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));

  /**
   * EekTheme:theme-stylesheet:
   *
   * The second priority stylesheet, representing theme-specific styling;
   * this is associated with the CSS "user" stylesheet.
   */
  g_object_class_install_property (object_class,
                                   PROP_THEME_STYLESHEET,
                                   g_param_spec_string ("theme-stylesheet",
                                                        "Theme Stylesheet",
                                                        "Stylesheet with theme-specific styling",
                                                        NULL,
                                                        G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));

  /**
   * EekTheme:default-stylesheet:
   *
   * The lowest priority stylesheet, representing global default
   * styling; this is associated with the CSS "user agent" stylesheet.
   */
  g_object_class_install_property (object_class,
                                   PROP_DEFAULT_STYLESHEET,
                                   g_param_spec_string ("default-stylesheet",
                                                        "Default Stylesheet",
                                                        "Stylesheet with global default styling",
                                                        NULL,
                                                        G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));

}

/* This is a workaround for a bug in libcroco < 0.6.2 where
 * function starting with 'r' (and 'u') are misparsed. We work
 * around this by exploiting the fact that libcroco is incomformant
 * with the CSS-spec and case sensitive and pre-convert all
 * occurrences of rgba to RGBA. Then we make our own parsing
 * code check for RGBA as well.
 */
#if LIBCROCO_VERSION_NUMBER < 602
static gboolean
is_identifier_character (char c)
{
  /* Actual CSS rules allow for unicode > 0x00a1 and escaped
   * characters, but we'll assume we won't do that in our stylesheets
   * or at least not next to the string 'rgba'.
   */
  return g_ascii_isalnum(c) || c == '-' || c == '_';
}

static void
convert_rgba_RGBA (char *buf)
{
  char *p;

  p = strstr (buf, "rgba");
  while (p)
    {
      /* Check if this looks like a complete token; this is to
       * avoiding mangling, say, a selector '.rgba-entry' */
      if (!((p > buf && is_identifier_character (*(p - 1))) ||
            (is_identifier_character (*(p + 4)))))
        memcpy(p, "RGBA", 4);
      p += 4;
      p = strstr (p, "rgba");
    }
}

static CRStyleSheet *
parse_stylesheet (const char  *filename,
                  GError     **error)
{
  enum CRStatus status;
  char *contents;
  gsize length;
  CRStyleSheet *stylesheet = NULL;

  if (filename == NULL)
    return NULL;

  if (!g_file_get_contents (filename, &contents, &length, error))
    return NULL;

  convert_rgba_RGBA (contents);

  status = cr_om_parser_simply_parse_buf ((const guchar *) contents,
                                          length,
                                          CR_UTF_8,
                                          &stylesheet);
  g_free (contents);

  if (status != CR_OK)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Error parsing stylesheet '%s'; errcode:%d", filename, status);
      return NULL;
    }

  return stylesheet;
}

CRDeclaration *
_eek_theme_parse_declaration_list (const char *str)
{
  char *copy = g_strdup (str);
  CRDeclaration *result;

  convert_rgba_RGBA (copy);

  result = cr_declaration_parse_list_from_buf ((const guchar *)copy,
                                               CR_UTF_8);
  g_free (copy);

  return result;
}
#else /* LIBCROCO_VERSION_NUMBER >= 602 */
static CRStyleSheet *
parse_stylesheet (const char  *filename,
                  GError     **error)
{
  enum CRStatus status;
  CRStyleSheet *stylesheet;

  if (filename == NULL)
    return NULL;

  status = cr_om_parser_simply_parse_file ((const guchar *) filename,
                                           CR_UTF_8,
                                           &stylesheet);

  if (status != CR_OK)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Error parsing stylesheet '%s'; errcode:%d", filename, status);
      return NULL;
    }

  return stylesheet;
}

CRDeclaration *
_eek_theme_parse_declaration_list (const char *str)
{
  return cr_declaration_parse_list_from_buf ((const guchar *)str,
                                             CR_UTF_8);
}
#endif /* LIBCROCO_VERSION_NUMBER < 602 */

/* Just g_warning for now until we have something nicer to do */
static CRStyleSheet *
parse_stylesheet_nofail (const char *filename)
{
  GError *error = NULL;
  CRStyleSheet *result;

  result = parse_stylesheet (filename, &error);
  if (error)
    {
      g_warning ("%s", error->message);
      g_clear_error (&error);
    }
  return result;
}

static void
insert_stylesheet (EekTheme      *theme,
                   const char   *filename,
                   CRStyleSheet *stylesheet)
{
  char *filename_copy;

  if (stylesheet == NULL)
    return;

  filename_copy = g_strdup(filename);
  cr_stylesheet_ref (stylesheet);

  g_hash_table_insert (theme->stylesheets_by_filename, filename_copy, stylesheet);
  g_hash_table_insert (theme->filenames_by_stylesheet, stylesheet, filename_copy);
}

gboolean
eek_theme_load_stylesheet (EekTheme    *theme,
                          const char *path,
                          GError    **error)
{
  CRStyleSheet *stylesheet;

  stylesheet = parse_stylesheet (path, error);
  if (!stylesheet)
    return FALSE;

  insert_stylesheet (theme, path, stylesheet);
  theme->custom_stylesheets = g_slist_prepend (theme->custom_stylesheets, stylesheet);

  return TRUE;
}

void
eek_theme_unload_stylesheet (EekTheme    *theme,
                            const char *path)
{
  CRStyleSheet *stylesheet;

  stylesheet = g_hash_table_lookup (theme->stylesheets_by_filename, path);
  if (!stylesheet)
    return;

  if (!g_slist_find (theme->custom_stylesheets, stylesheet))
    return;

  theme->custom_stylesheets = g_slist_remove (theme->custom_stylesheets, stylesheet);
  g_hash_table_remove (theme->stylesheets_by_filename, path);
  g_hash_table_remove (theme->filenames_by_stylesheet, stylesheet);
  cr_stylesheet_unref (stylesheet);
}

static GObject *
eek_theme_constructor (GType                  type,
                      guint                  n_construct_properties,
                      GObjectConstructParam *construct_properties)
{
  GObject *object;
  EekTheme *theme;
  CRStyleSheet *application_stylesheet;
  CRStyleSheet *theme_stylesheet;
  CRStyleSheet *default_stylesheet;

  object = (*G_OBJECT_CLASS (eek_theme_parent_class)->constructor) (type,
                                                                      n_construct_properties,
                                                                      construct_properties);
  theme = EEK_THEME (object);

  application_stylesheet = parse_stylesheet_nofail (theme->application_stylesheet);
  theme_stylesheet = parse_stylesheet_nofail (theme->theme_stylesheet);
  default_stylesheet = parse_stylesheet_nofail (theme->default_stylesheet);

  theme->cascade = cr_cascade_new (application_stylesheet,
                                   theme_stylesheet,
                                   default_stylesheet);

  if (theme->cascade == NULL)
    g_error ("Out of memory when creating cascade object");

  insert_stylesheet (theme, theme->application_stylesheet, application_stylesheet);
  insert_stylesheet (theme, theme->theme_stylesheet, theme_stylesheet);
  insert_stylesheet (theme, theme->default_stylesheet, default_stylesheet);

  return object;
}

static void
eek_theme_finalize (GObject * object)
{
  EekTheme *theme = EEK_THEME (object);

  g_slist_foreach (theme->custom_stylesheets, (GFunc) cr_stylesheet_unref, NULL);
  g_slist_free (theme->custom_stylesheets);
  theme->custom_stylesheets = NULL;

  g_hash_table_destroy (theme->stylesheets_by_filename);
  g_hash_table_destroy (theme->filenames_by_stylesheet);

  g_free (theme->application_stylesheet);
  g_free (theme->theme_stylesheet);
  g_free (theme->default_stylesheet);

  if (theme->cascade)
    {
      cr_cascade_unref (theme->cascade);
      theme->cascade = NULL;
    }

  G_OBJECT_CLASS (eek_theme_parent_class)->finalize (object);
}

static void
eek_theme_set_property (GObject      *object,
                       guint         prop_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
  EekTheme *theme = EEK_THEME (object);

  switch (prop_id)
    {
    case PROP_APPLICATION_STYLESHEET:
      {
        const char *path = g_value_get_string (value);

        if (path != theme->application_stylesheet)
          {
            g_free (theme->application_stylesheet);
            theme->application_stylesheet = g_strdup (path);
          }

        break;
      }
    case PROP_THEME_STYLESHEET:
      {
        const char *path = g_value_get_string (value);

        if (path != theme->theme_stylesheet)
          {
            g_free (theme->theme_stylesheet);
            theme->theme_stylesheet = g_strdup (path);
          }

        break;
      }
    case PROP_DEFAULT_STYLESHEET:
      {
        const char *path = g_value_get_string (value);

        if (path != theme->default_stylesheet)
          {
            g_free (theme->default_stylesheet);
            theme->default_stylesheet = g_strdup (path);
          }

        break;
      }
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
eek_theme_get_property (GObject    *object,
                       guint       prop_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  EekTheme *theme = EEK_THEME (object);

  switch (prop_id)
    {
    case PROP_APPLICATION_STYLESHEET:
      g_value_set_string (value, theme->application_stylesheet);
      break;
    case PROP_THEME_STYLESHEET:
      g_value_set_string (value, theme->theme_stylesheet);
      break;
    case PROP_DEFAULT_STYLESHEET:
      g_value_set_string (value, theme->default_stylesheet);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

/**
 * eek_theme_new:
 * @application_stylesheet: (allow-none): The highest priority stylesheet, representing application-specific
 *   styling; this is associated with the CSS "author" stylesheet, may be %NULL
 * @theme_stylesheet: (allow-none): The second priority stylesheet, representing theme-specific styling ;
 *   this is associated with the CSS "user" stylesheet, may be %NULL
 * @default_stylesheet: (allow-none): The lowest priority stylesheet, representing global default styling;
 *   this is associated with the CSS "user agent" stylesheet, may be %NULL
 *
 * Return value: the newly created theme object
 **/
EekTheme *
eek_theme_new (const char       *application_stylesheet,
              const char       *theme_stylesheet,
              const char       *default_stylesheet)
{
    EekTheme *theme = g_object_new (EEK_TYPE_THEME,
                                    "application-stylesheet", application_stylesheet,
                                    "theme-stylesheet", theme_stylesheet,
                                    "default-stylesheet", default_stylesheet,
                                    NULL);

  return theme;
}

static gboolean
string_in_list (GString    *stryng,
                const char *list)
{
  const char *cur;

  for (cur = list; *cur;)
    {
      while (*cur && cr_utils_is_white_space (*cur))
        cur++;

      if (strncmp (cur, stryng->str, stryng->len) == 0)
        {
          cur +=  stryng->len;
          if ((!*cur) || cr_utils_is_white_space (*cur))
            return TRUE;
        }

      /*  skip to next whitespace character  */
      while (*cur && !cr_utils_is_white_space (*cur))
        cur++;
    }

  return FALSE;
}

static gboolean
pseudo_class_add_sel_matches_style (EekTheme         *a_this,
                                    CRAdditionalSel *a_add_sel,
                                    EekThemeNode     *a_node)
{
  const char *node_pseudo_class;

  g_return_val_if_fail (a_this
                        && a_add_sel
                        && a_add_sel->content.pseudo
                        && a_add_sel->content.pseudo->name
                        && a_add_sel->content.pseudo->name->stryng
                        && a_add_sel->content.pseudo->name->stryng->str
                        && a_node, FALSE);

  node_pseudo_class = eek_theme_node_get_pseudo_class (a_node);

  if (node_pseudo_class == NULL)
    return FALSE;

  return string_in_list (a_add_sel->content.pseudo->name->stryng, node_pseudo_class);
}

/**
 *@param a_add_sel the class additional selector to consider.
 *@param a_node the style node to consider.
 *@return TRUE if the class additional selector matches
 *the style node given in argument, FALSE otherwise.
 */
static gboolean
class_add_sel_matches_style (CRAdditionalSel *a_add_sel,
                             EekThemeNode     *a_node)
{
  const char *element_class;

  g_return_val_if_fail (a_add_sel
                        && a_add_sel->type == CLASS_ADD_SELECTOR
                        && a_add_sel->content.class_name
                        && a_add_sel->content.class_name->stryng
                        && a_add_sel->content.class_name->stryng->str
                        && a_node, FALSE);

  element_class = eek_theme_node_get_element_class (a_node);
  if (element_class == NULL)
    return FALSE;

  return string_in_list (a_add_sel->content.class_name->stryng, element_class);
}

/**
 *@return TRUE if the additional attribute selector matches
 *the current style node given in argument, FALSE otherwise.
 *@param a_add_sel the additional attribute selector to consider.
 *@param a_node the style node to consider.
 */
static gboolean
id_add_sel_matches_style (CRAdditionalSel *a_add_sel,
                          EekThemeNode     *a_node)
{
  gboolean result = FALSE;
  const char *id;

  g_return_val_if_fail (a_add_sel
                        && a_add_sel->type == ID_ADD_SELECTOR
                        && a_add_sel->content.id_name
                        && a_add_sel->content.id_name->stryng
                        && a_add_sel->content.id_name->stryng->str
                        && a_node, FALSE);
  g_return_val_if_fail (a_add_sel
                        && a_add_sel->type == ID_ADD_SELECTOR
                        && a_node, FALSE);

  id = eek_theme_node_get_element_id (a_node);

  if (id != NULL)
    {
      if (!strqcmp (id, a_add_sel->content.id_name->stryng->str,
                    a_add_sel->content.id_name->stryng->len))
        {
          result = TRUE;
        }
    }

  return result;
}

/**
 *additional_selector_matches_style:
 *Evaluates if a given additional selector matches an style node.
 *@param a_add_sel the additional selector to consider.
 *@param a_node the style node to consider.
 *@return TRUE is a_add_sel matches a_node, FALSE otherwise.
 */
static gboolean
additional_selector_matches_style (EekTheme         *a_this,
                                   CRAdditionalSel *a_add_sel,
                                   EekThemeNode     *a_node)
{
  CRAdditionalSel *cur_add_sel = NULL;

  g_return_val_if_fail (a_add_sel, FALSE);

  for (cur_add_sel = a_add_sel; cur_add_sel; cur_add_sel = cur_add_sel->next)
    {
      switch (cur_add_sel->type)
        {
        case NO_ADD_SELECTOR:
          return FALSE;
        case CLASS_ADD_SELECTOR:
          if (!class_add_sel_matches_style (cur_add_sel, a_node))
            return FALSE;
          break;
        case ID_ADD_SELECTOR:
          if (!id_add_sel_matches_style (cur_add_sel, a_node))
            return FALSE;
          break;
        case ATTRIBUTE_ADD_SELECTOR:
          g_warning ("Attribute selectors not supported");
          return FALSE;
        case  PSEUDO_CLASS_ADD_SELECTOR:
          if (!pseudo_class_add_sel_matches_style (a_this, cur_add_sel, a_node))
            return FALSE;
          break;
        }
    }

  return TRUE;
}

static gboolean
element_name_matches_type (const char *element_name,
                           GType       element_type)
{
  if (element_type == G_TYPE_NONE)
    {
      return strcmp (element_name, "stage") == 0;
    }
  else
    {
      GType match_type = g_type_from_name (element_name);
      if (match_type == G_TYPE_INVALID)
        return FALSE;

      return g_type_is_a (element_type, match_type);
    }
}

/**
 *Evaluate a selector (a simple selectors list) and says
 *if it matches the style node given in parameter.
 *The algorithm used here is the following:
 *Walk the combinator separated list of simple selectors backward, starting
 *from the end of the list. For each simple selector, looks if
 *if matches the current style.
 *
 *@param a_this the selection engine.
 *@param a_sel the simple selection list.
 *@param a_node the style node.
 *@param a_result out parameter. Set to true if the
 *selector matches the style node, FALSE otherwise.
 *@param a_recurse if set to TRUE, the function will walk to
 *the next simple selector (after the evaluation of the current one)
 *and recursively evaluate it. Must be usually set to TRUE unless you
 *know what you are doing.
 */
static enum CRStatus
sel_matches_style_real (EekTheme     *a_this,
                        CRSimpleSel *a_sel,
                        EekThemeNode *a_node,
                        gboolean    *a_result,
                        gboolean     a_eval_sel_list_from_end,
                        gboolean     a_recurse)
{
  CRSimpleSel *cur_sel = NULL;
  EekThemeNode *cur_node = NULL;
  GType cur_type;

  *a_result = FALSE;

  if (a_eval_sel_list_from_end)
    {
      /*go and get the last simple selector of the list */
      for (cur_sel = a_sel; cur_sel && cur_sel->next; cur_sel = cur_sel->next)
        ;
    }
  else
    {
      cur_sel = a_sel;
    }

  cur_node = a_node;
  cur_type = eek_theme_node_get_element_type (cur_node);

  while (cur_sel)
    {
      if (((cur_sel->type_mask & TYPE_SELECTOR)
           && (cur_sel->name
               && cur_sel->name->stryng
               && cur_sel->name->stryng->str)
           &&
           (element_name_matches_type (cur_sel->name->stryng->str, cur_type)))
          || (cur_sel->type_mask & UNIVERSAL_SELECTOR))
        {
          /*
           *this simple selector
           *matches the current style node
           *Let's see if the preceding
           *simple selectors also match
           *their style node counterpart.
           */
          if (cur_sel->add_sel)
            {
              if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node))
                goto walk_a_step_in_expr;
              else
                goto done;
            }
          else
            goto walk_a_step_in_expr;
        }
      if (!(cur_sel->type_mask & TYPE_SELECTOR)
          && !(cur_sel->type_mask & UNIVERSAL_SELECTOR))
        {
          if (!cur_sel->add_sel)
            goto done;
          if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node))
            goto walk_a_step_in_expr;
          else
            goto done;
        }
      else
        {
          goto done;
        }

    walk_a_step_in_expr:
      if (a_recurse == FALSE)
        {
          *a_result = TRUE;
          goto done;
        }

      /*
       *here, depending on the combinator of cur_sel
       *choose the axis of the element tree traversal
       *and walk one step in the element tree.
       */
      if (!cur_sel->prev)
        break;

      switch (cur_sel->combinator)
        {
        case NO_COMBINATOR:
          break;

        case COMB_WS:           /*descendant selector */
          {
            EekThemeNode *n = NULL;

            /*
             *walk the element tree upward looking for a parent
             *style that matches the preceding selector.
             */
            for (n = eek_theme_node_get_parent (a_node); n; n = eek_theme_node_get_parent (n))
              {
                enum CRStatus status;
                gboolean matches = FALSE;

                status = sel_matches_style_real (a_this, cur_sel->prev, n, &matches, FALSE, TRUE);

                if (status != CR_OK)
                  goto done;

                if (matches)
                  {
                    cur_node = n;
                    cur_type = eek_theme_node_get_element_type (cur_node);
                    break;
                  }
              }

            if (!n)
              {
                /*
                 *didn't find any ancestor that matches
                 *the previous simple selector.
                 */
                goto done;
              }
            /*
             *in this case, the preceding simple sel
             *will have been interpreted twice, which
             *is a cpu and mem waste ... I need to find
             *another way to do this. Anyway, this is
             *my first attempt to write this function and
             *I am a bit clueless.
             */
            break;
          }

        case COMB_PLUS:
          g_warning ("+ combinators are not supported");
          goto done;

        case COMB_GT:
          cur_node = eek_theme_node_get_parent (cur_node);
          if (!cur_node)
            goto done;
          cur_type = eek_theme_node_get_element_type (cur_node);
          break;

        default:
          goto done;
        }

      cur_sel = cur_sel->prev;
    }

  /*
   *if we reached this point, it means the selector matches
   *the style node.
   */
  *a_result = TRUE;

done:
  return CR_OK;
}

static void
add_matched_properties (EekTheme      *a_this,
                        CRStyleSheet *a_nodesheet,
                        EekThemeNode  *a_node,
                        GPtrArray    *props)
{
  CRStatement *cur_stmt = NULL;
  CRSelector *sel_list = NULL;
  CRSelector *cur_sel = NULL;
  gboolean matches = FALSE;
  enum CRStatus status = CR_OK;

  /*
   *walk through the list of statements and,
   *get the selectors list inside the statements that
   *contain some, and try to match our style node in these
   *selectors lists.
   */
  for (cur_stmt = a_nodesheet->statements; cur_stmt; cur_stmt = cur_stmt->next)
    {
      /*
       *initialyze the selector list in which we will
       *really perform the search.
       */
      sel_list = NULL;

      /*
       *get the the damn selector list in
       *which we have to look
       */
      switch (cur_stmt->type)
        {
        case RULESET_STMT:
          if (cur_stmt->kind.ruleset && cur_stmt->kind.ruleset->sel_list)
            {
              sel_list = cur_stmt->kind.ruleset->sel_list;
            }
          break;

        case AT_MEDIA_RULE_STMT:
          if (cur_stmt->kind.media_rule
              && cur_stmt->kind.media_rule->rulesets
              && cur_stmt->kind.media_rule->rulesets->kind.ruleset
              && cur_stmt->kind.media_rule->rulesets->kind.ruleset->sel_list)
            {
              sel_list = cur_stmt->kind.media_rule->rulesets->kind.ruleset->sel_list;
            }
          break;

        case AT_IMPORT_RULE_STMT:
          {
            CRAtImportRule *import_rule = cur_stmt->kind.import_rule;

            if (import_rule->sheet == NULL)
              {
                char *filename = NULL;

                if (import_rule->url->stryng && import_rule->url->stryng->str)
                  filename = eek_theme_resolve_url (a_this,
                                                    a_nodesheet,
                                                    import_rule->url->stryng->str);

                if (filename)
                  import_rule->sheet = parse_stylesheet (filename, NULL);

                if (import_rule->sheet)
                  {
                    insert_stylesheet (a_this, filename, import_rule->sheet);
                    /* refcount of stylesheets starts off at zero, so we don't need to unref! */
                  }
                else
                  {
                    /* Set a marker to avoid repeatedly trying to parse a non-existent or
                     * broken stylesheet
                     */
                    import_rule->sheet = (CRStyleSheet *) - 1;
                  }

                if (filename)
                  g_free (filename);
              }

            if (import_rule->sheet != (CRStyleSheet *) - 1)
              {
                add_matched_properties (a_this, import_rule->sheet,
                                        a_node, props);
              }
          }
          break;
        default:
          break;
        }

      if (!sel_list)
        continue;

      /*
       *now, we have a comma separated selector list to look in.
       *let's walk it and try to match the style node
       *on each item of the list.
       */
      for (cur_sel = sel_list; cur_sel; cur_sel = cur_sel->next)
        {
          if (!cur_sel->simple_sel)
            continue;

          status = sel_matches_style_real (a_this, cur_sel->simple_sel, a_node, &matches, TRUE, TRUE);

          if (status == CR_OK && matches)
            {
              CRDeclaration *cur_decl = NULL;

              /* In order to sort the matching properties, we need to compute the
               * specificity of the selector that actually matched this
               * element. In a non-thread-safe fashion, we store it in the
               * ruleset. (Fixing this would mean cut-and-pasting
               * cr_simple_sel_compute_specificity(), and have no need for
               * thread-safety anyways.)
               *
               * Once we've sorted the properties, the specificity no longer
               * matters and it can be safely overriden.
               */
              cr_simple_sel_compute_specificity (cur_sel->simple_sel);

              cur_stmt->specificity = cur_sel->simple_sel->specificity;

              for (cur_decl = cur_stmt->kind.ruleset->decl_list; cur_decl; cur_decl = cur_decl->next)
                g_ptr_array_add (props, cur_decl);
            }
        }
    }
}

#define ORIGIN_AUTHOR_IMPORTANT (ORIGIN_AUTHOR + 1)
#define ORIGIN_USER_IMPORTANT   (ORIGIN_AUTHOR + 2)

static inline int
get_origin (const CRDeclaration * decl)
{
  enum CRStyleOrigin origin = decl->parent_statement->parent_sheet->origin;

  if (decl->important)
    {
      if (origin == ORIGIN_AUTHOR)
        return ORIGIN_AUTHOR_IMPORTANT;
      else if (origin == ORIGIN_USER)
        return ORIGIN_USER_IMPORTANT;
    }

  return origin;
}

/* Order of comparison is so that higher priority statements compare after
 * lower priority statements */
static int
compare_declarations (gconstpointer a,
                      gconstpointer b)
{
  /* g_ptr_array_sort() is broooken */
  CRDeclaration *decl_a = *(CRDeclaration **) a;
  CRDeclaration *decl_b = *(CRDeclaration **) b;

  int origin_a = get_origin (decl_a);
  int origin_b = get_origin (decl_b);

  if (origin_a != origin_b)
    return origin_a - origin_b;

  if (decl_a->parent_statement->specificity != decl_b->parent_statement->specificity)
    return decl_a->parent_statement->specificity - decl_b->parent_statement->specificity;

  return 0;
}

GPtrArray *
_eek_theme_get_matched_properties (EekTheme        *theme,
                                  EekThemeNode    *node)
{
  enum CRStyleOrigin origin = 0;
  CRStyleSheet *sheet = NULL;
  GPtrArray *props = g_ptr_array_new ();
  GSList *iter;

  g_return_val_if_fail (EEK_IS_THEME (theme), NULL);
  g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL);

  for (origin = ORIGIN_UA; origin < NB_ORIGINS; origin++)
    {
      sheet = cr_cascade_get_sheet (theme->cascade, origin);
      if (!sheet)
        continue;

      add_matched_properties (theme, sheet, node, props);
    }

  for (iter = theme->custom_stylesheets; iter; iter = iter->next)
    add_matched_properties (theme, iter->data, node, props);

  /* We count on a stable sort here so that later declarations come
   * after earlier declarations */
  g_ptr_array_sort (props, compare_declarations);

  return props;
}

/* Resolve an url from an url() reference in a stylesheet into an absolute
 * local filename, if possible. The resolution here is distinctly lame and
 * will fail on many examples.
 */
static char *
eek_theme_resolve_url (EekTheme     *theme,
                       CRStyleSheet *base_stylesheet,
                       const char   *url)
{
  const char *base_filename = NULL;
  char *dirname;
  char *filename;

  /* Handle absolute file:/ URLs */
  if (g_str_has_prefix (url, "file:") ||
      g_str_has_prefix (url, "File:") ||
      g_str_has_prefix (url, "FILE:"))
    {
      GError *error = NULL;
      char *filename;

      filename = g_filename_from_uri (url, NULL, &error);
      if (filename == NULL)
        {
          g_warning ("%s", error->message);
          g_error_free (error);
        }

      return NULL;
    }

  /* Guard against http:/ URLs */

  if (g_str_has_prefix (url, "http:") ||
      g_str_has_prefix (url, "Http:") ||
      g_str_has_prefix (url, "HTTP:"))
    {
      g_warning ("Http URL '%s' in theme stylesheet is not supported", url);
      return NULL;
    }

  /* Assume anything else is a relative URL, and "resolve" it
   */
  if (url[0] == '/')
    return g_strdup (url);

  base_filename = g_hash_table_lookup (theme->filenames_by_stylesheet, base_stylesheet);

  if (base_filename == NULL)
    {
      g_warning ("Can't get base to resolve url '%s'", url);
      return NULL;
    }

  dirname = g_path_get_dirname (base_filename);
  filename = g_build_filename (dirname, url, NULL);
  g_free (dirname);

  return filename;
}
