/* theme_loader.c - Theme file loader for gfxmenu.  */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2008  Free Software Foundation, Inc.
 *
 *  GRUB 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB 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 GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/types.h>
#include <grub/file.h>
#include <grub/misc.h>
#include <grub/mm.h>
#include <grub/err.h>
#include <grub/dl.h>
#include <grub/video.h>
#include <grub/gui_string_util.h>
#include <grub/bitmap.h>
#include <grub/bitmap_scale.h>
#include <grub/gfxwidgets.h>
#include <grub/gfxmenu_view.h>
#include <grub/gui.h>

/* Construct a new box widget using ABSPATTERN to find the pixmap files for
   it, storing the new box instance at *BOXPTR.
   PATTERN should be of the form: "(hd0,0)/somewhere/style*.png".
   The '*' then gets substituted with the various pixmap names that the
   box uses.

   Returns zero on failure, nonzero on success.
 */
static int
recreate_box_absolute (grub_gfxmenu_box_t *boxptr, const char *abspattern)
{
  char *prefix;
  char *suffix;
  char *star;
  grub_gfxmenu_box_t box;

  star = grub_strchr (abspattern, '*');
  if (! star)
    return 0;

  /* Prefix:  Get the part before the '*'.  */
  prefix = grub_malloc (star - abspattern + 1);
  if (! prefix)
    return 0;

  grub_memcpy (prefix, abspattern, star - abspattern);
  prefix[star - abspattern] = '\0';

  /* Suffix:  Everything after the '*' is the suffix.  */
  suffix = star + 1;

  box = grub_gfxmenu_create_box (prefix, suffix);
  grub_free (prefix);
  if (! box)
    return 0;

  if (*boxptr)
    (*boxptr)->destroy (*boxptr);
  *boxptr = box;
  return 1;
}


/* Construct a new box widget using PATTERN to find the pixmap files for it,
   storing the new widget at *BOXPTR.  PATTERN should be of the form:
   "somewhere/style*.png".  The '*' then gets substituted with the various
   pixmap names that the widget uses.

   Important!  The value of *BOXPTR must be initialized!  It must either
   (1) Be 0 (a NULL pointer), or
   (2) Be a pointer to a valid 'grub_gfxmenu_box_t' instance.
   In this case, the previous instance is destroyed.

   Returns zero on failure, nonzero on success.
 */
int
grub_gui_recreate_box (grub_gfxmenu_box_t *boxptr,
                       const char *pattern, const char *theme_dir)
{
  char *abspattern;
  int success;

  /* Check arguments.  */
  if (! pattern || ! theme_dir)
    return 0;

  /* Resolve to an absolute path.  */
  abspattern = grub_resolve_relative_path (theme_dir, pattern);
  if (! abspattern)
    return 0;

  /* Create the box.  */
  success = recreate_box_absolute (boxptr, abspattern);
  grub_free (abspattern);
  return success;
}

/* Set the specified property NAME on the view to the given string VALUE.
   This function takes ownership of both NAME and VALUE, so the caller
   should pass pointers to new heap-allocated strings.  */
static void
theme_set_string (grub_gfxmenu_view_t view,
                  const char *name,
                  const char *value,
                  const char *theme_dir)
{
  if (! grub_strcmp ("title-font", name))
    view->title_font = grub_font_get (value);
  else if (! grub_strcmp ("message-font", name))
    view->message_font = grub_font_get (value);
  else if (! grub_strcmp ("terminal-font", name))
    {
      grub_free (view->terminal_font_name);
      view->terminal_font_name = grub_strdup (value);
    }
  else if (! grub_strcmp ("title-color", name))
    view->title_color = grub_gui_parse_color (value);
  else if (! grub_strcmp ("message-color", name))
    view->message_color = grub_gui_parse_color (value);
  else if (! grub_strcmp ("message-bg-color", name))
    view->message_bg_color = grub_gui_parse_color (value);
  else if (! grub_strcmp ("desktop-image", name))
    {
      struct grub_video_bitmap *raw_bitmap;
      struct grub_video_bitmap *scaled_bitmap;
      char *path;
      path = grub_resolve_relative_path (theme_dir, value);
      if (! path)
        return;
      grub_errno = GRUB_ERR_NONE;
      if (grub_video_bitmap_load (&raw_bitmap, path) != GRUB_ERR_NONE)
        {
          grub_errno = GRUB_ERR_NONE;
          grub_free (path);
          return;
        }
      grub_free(path);
      grub_video_bitmap_create_scaled (&scaled_bitmap,
                                       view->screen.width,
                                       view->screen.height,
                                       raw_bitmap,
                                       GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST);
      grub_video_bitmap_destroy (raw_bitmap);
      if (!scaled_bitmap)
        return;

      grub_video_bitmap_destroy (view->desktop_image);
      view->desktop_image = scaled_bitmap;
    }
  else if (! grub_strcmp ("desktop-color", name))
    view->desktop_color = grub_gui_parse_color (value);
  else if (! grub_strcmp ("terminal-box", name))
    grub_gui_recreate_box (&view->terminal_box, value, theme_dir);
  else if (! grub_strcmp ("title-text", name))
    {
      grub_free (view->title_text);
      view->title_text = grub_strdup (value);
    }
}

struct parsebuf
{
  char *buf;
  int pos;
  int len;
  char *theme_dir;
  grub_gfxmenu_view_t view;
};

static void
advance_to_next_line (struct parsebuf *p)
{
  /* Eat characters up to the newline.  */
  while (p->pos < p->len && p->buf[p->pos] != '\n')
    p->pos++;
  p->pos++;   /* Eat the newline.  */
}

static void
skip_whitespace (struct parsebuf *p)
{
  while (p->pos < p->len
         && (p->buf[p->pos] == ' '
             || p->buf[p->pos] == '\t'
             || p->buf[p->pos] == '\r'
             || p->buf[p->pos] == '\n'
             || p->buf[p->pos] == '\f'))
    p->pos++;
}

static int
read_char (struct parsebuf *p)
{
  if (p->pos < p->len)
    return p->buf[p->pos++];
  else
    return -1;
}

static char *
read_identifier (struct parsebuf *p)
{
  int start;
  int end;

  skip_whitespace (p);
  start = p->pos;
  while (p->pos < p->len
         && (grub_isalpha(p->buf[p->pos])
             || grub_isdigit(p->buf[p->pos])
             || p->buf[p->pos] == '_'
             || p->buf[p->pos] == '-'))
    p->pos++;
  end = p->pos;
  if (end - start < 1)
    return 0;

  return grub_new_substring (p->buf, start, end);
}

static char *
read_expression (struct parsebuf *p)
{
  int start;
  int end;

  skip_whitespace (p);
  if (p->pos < p->len && p->buf[p->pos] == '"')
    {
      /* Read as a quoted string.  */
      /* The quotation marks are not included in the expression value.  */
      p->pos++;
      start = p->pos;
      while (p->pos < p->len
             && p->buf[p->pos] != '"')
        p->pos++;
      end = p->pos;
      if (p->pos < p->len)
        p->pos++;   /* Skip the terminating quotation mark.  */
    }
  else if (p->pos < p->len && p->buf[p->pos] == '(')
    {
      /* Read as a parenthesized string -- for tuples/coordinates.  */
      /* The parentheses are included in the expression value.  */
      start = p->pos;
      while (p->pos < p->len)
        {
          char c = p->buf[p->pos];
          p->pos++;
          if (c == ')')
            break;
        }
      end = p->pos;
    }
  else if (p->pos < p->len)
    {
      /* Read as a single word -- for numeric values.  */
      start = p->pos;
      while (p->pos < p->len
             && ! grub_isspace (p->buf[p->pos]))
        p->pos++;
      end = p->pos;
    }
  else
    return 0;

  return grub_new_substring (p->buf, start, end);
}

/* Read a GUI object specification from the theme file.
   Any components created will be added to the GUI container PARENT.
   Returns 0 on success, nonzero on failure.  */
static int
read_object (struct parsebuf *p, grub_gui_container_t parent)
{
  char *name;
  name = read_identifier (p);
  if (! name)
    goto fail;

  grub_gui_component_t component = 0;
  if (grub_strcmp (name, "label") == 0)
    {
      component = grub_gui_label_new ();
    }
  else if (grub_strcmp (name, "image") == 0)
    {
      component = grub_gui_image_new ();
    }
  else if (grub_strcmp (name, "vbox") == 0)
    {
      component = (grub_gui_component_t) grub_gui_vbox_new ();
    }
  else if (grub_strcmp (name, "hbox") == 0)
    {
      component = (grub_gui_component_t) grub_gui_hbox_new ();
    }
  else if (grub_strcmp (name, "canvas") == 0)
    {
      component = (grub_gui_component_t) grub_gui_canvas_new ();
    }
  else if (grub_strcmp (name, "progress_bar") == 0)
    {
      component = grub_gui_progress_bar_new ();
    }
  else if (grub_strcmp (name, "circular_progress") == 0)
    {
      component = grub_gui_circular_progress_new ();
    }
  else if (grub_strcmp (name, "boot_menu") == 0)
    {
      component = grub_gui_list_new ();
    }
  else
    {
      /* Unknown type; ignore.  */
      goto fail;   /* We should actually parse the structure.  */
    }

  if (component)
    {
      grub_video_rect_t r = { .x=0, .y=0, .width=-1, .height=-1 };
      component->ops->set_bounds (component, &r);
      parent->ops->add (parent, component);
    }

  skip_whitespace (p);
  if (read_char (p) != '{')
    goto fail;

  while (p->pos < p->len)
    {
      skip_whitespace (p);

      /* Check whether the end has been encountered.  */
      if (p->pos < p->len
          && p->buf[p->pos] == '}')
        {
          p->pos++;   /* Skip the closing brace.  */
          break;
        }

      if (p->pos < p->len
          && p->buf[p->pos] == '#')
        {
          /* Skip comments.  */
          advance_to_next_line (p);
          continue;
        }

      if (p->pos < p->len
          && p->buf[p->pos] == '+')
        {
          p->pos++;  /* Skip the '+'.  */
          /* Check whether this component is a container.  */
          if (component->ops->is_instance (component, "container"))
            {
              /* Read the sub-object recursively and add it as a child.  */
              if (read_object (p, (grub_gui_container_t) component) != 0)
                goto fail;
              /* After reading the sub-object, resume parsing, expecting
                 another property assignment or sub-object definition.  */
              continue;
            }
          else
            {
              /* error ("Attempted to add object to non-container.") */
              goto fail;
            }
        }

      char *property;
      property = read_identifier (p);
      if (! property)
        goto fail;

      skip_whitespace (p);
      if (read_char (p) != '=')
        {
          grub_free (property);
          goto fail;
        }
      skip_whitespace (p);

      char *value;
      value = read_expression (p);
      if (! value)
        {
          grub_free (property);
          goto fail;
        }

      /* Handle the property value.  */
      if (grub_strcmp (property, "position") == 0)
        {
          /* Special case for position value.  */
          int x;
          int y;

          if (grub_gui_parse_2_tuple (value, &x, &y))
            {
              grub_video_rect_t r;
              component->ops->get_bounds (component, &r);
              r.x = x;
              r.y = y;
              component->ops->set_bounds (component, &r);
            }
        }
      else if (grub_strcmp (property, "size") == 0)
        {
          /* Special case for size value.  */
          int w;
          int h;

          if (grub_gui_parse_2_tuple (value, &w, &h))
            {
              grub_video_rect_t r;
              component->ops->get_bounds (component, &r);
              r.width = w;
              r.height = h;
              component->ops->set_bounds (component, &r);
            }
        }
      else
        {
          /* General property handling.  */
          component->ops->set_property (component, property, value);
        }

      grub_free (value);
      grub_free (property);
    }

  /* Set the object's size to its preferred size unless the user has
     explicitly specified the size.  */
  grub_video_rect_t r;
  component->ops->get_bounds (component, &r);
  if (r.width == -1 || r.height == -1)
    {
      component->ops->get_preferred_size (component, &r.width, &r.height);
      component->ops->set_bounds (component, &r);
    }

  return 0;

fail:
  grub_free (name);

  /* Once the opening brace has been found, we always jump past the closing
     brace if an error is encountered.  */
  while (p->pos < p->len
         && p->buf[p->pos] != '}')
    p->pos++;
  return 1;
}

static void
read_property (struct parsebuf *p)
{
  char *name;

  name = read_identifier (p);  /* Read the property name.  */
  if (! name)
    goto next_line;

  skip_whitespace (p);         /* Skip whitespace before separator.  */

  if (read_char (p) != ':')    /* Read separator.  */
    goto next_line;

  skip_whitespace (p);         /* Skip whitespace after separator.  */

  /* Get the value based on its type.  */
  if (p->pos < p->len && p->buf[p->pos] == '"')
    {
      /* String value.  (e.g., '"My string"')  */
      char *value = read_expression (p);
      if (! value)
        goto next_line;
      theme_set_string (p->view, name, value, p->theme_dir);
      grub_free (value);
    }

next_line:
  grub_free (name);
  advance_to_next_line (p);
}

struct theme_info
{
  const char *theme_dir;
  const char *theme_path;
};

static void
update_theme_info_visit (grub_gui_component_t component, void *userdata)
{
  struct theme_info *info;
  info = userdata;
  component->ops->set_property (component, "theme_dir", info->theme_dir);
  component->ops->set_property (component, "theme_path", info->theme_path);
}

/* Update any boot menu components with the current menu model and
   theme path.  */
static void
update_component_tree_theme_info (grub_gui_component_t root,
                                  const char *theme_dir,
                                  const char *theme_path)
{
  struct theme_info info;
  info.theme_dir = theme_dir;
  info.theme_path = theme_path;
  grub_gui_iterate_recursively (root, update_theme_info_visit, &info);
}

/* Set properties on the view based on settings from the specified
   theme file.  Returns nonzero on success, zero on failure.  */
int
grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, const char *theme_path)
{
  grub_file_t file;
  struct parsebuf p;

  p.view = view;
  p.theme_dir = grub_get_dirname (theme_path);

  file = grub_file_open (theme_path);
  if (!file)
    {
      grub_free (p.theme_dir);
      return 0;
    }

  p.len = grub_file_size (file);
  p.buf = grub_malloc (p.len);
  if (! p.buf)
    {
      grub_file_close (file);
      grub_free (p.theme_dir);
      return 0;
    }
  if (grub_file_read (file, p.buf, p.len) != p.len)
    {
      grub_free (p.buf);
      grub_file_close (file);
      grub_free (p.theme_dir);
      return 0;
    }

  if (view->canvas)
    view->canvas->ops->component.destroy (view->canvas);

  view->canvas = grub_gui_canvas_new ();
  ((grub_gui_component_t) view->canvas)
    ->ops->set_bounds ((grub_gui_component_t) view->canvas,
                       &view->screen);

  p.pos = 0;
  while (p.pos < p.len)
    {
      /* Skip comments (lines beginning with #).  */
      if (p.pos < p.len && p.buf[p.pos] == '#')
        {
          advance_to_next_line (&p);
          continue;
        }

      /* Get name.  */
      /* Find the first non-whitespace character.  */
      skip_whitespace (&p);

      /* Handle the content.  */
      if (p.buf[p.pos] == '+')
        {
          p.pos++;   /* Skip the '+'.  */
          read_object (&p, view->canvas);
        }
      else
        {
          read_property (&p);
        }
    }

  /* Set the new theme path.  */
  grub_free (view->theme_path);
  view->theme_path = grub_strdup (theme_path);
  update_component_tree_theme_info ((grub_gui_component_t) view->canvas,
                                    p.theme_dir,
                                    theme_path);

  grub_free (p.buf);
  grub_file_close (file);
  grub_free (p.theme_dir);
  return 1;
}
