/* menu_text.c - Basic text menu implementation.  */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2003,2004,2005,2006,2007,2008,2009  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/normal.h>
#include <grub/term.h>
#include <grub/misc.h>
#include <grub/loader.h>
#include <grub/mm.h>
#include <grub/time.h>
#include <grub/env.h>
#include <grub/menu_viewer.h>
#include <grub/i18n.h>
#include <grub/charset.h>

static grub_uint8_t grub_color_menu_normal;
static grub_uint8_t grub_color_menu_highlight;

struct menu_viewer_data
{
  int first, offset;
  int nested;
  struct grub_term_screen_geometry geo;
  enum
  {
    TIMEOUT_UNKNOWN,
    TIMEOUT_NORMAL,
    TIMEOUT_TERSE,
    TIMEOUT_TERSE_NO_MARGIN
  } timeout_msg;
  grub_menu_t menu;
  struct grub_term_output *term;
};

static inline int
grub_term_cursor_x (const struct grub_term_screen_geometry *geo)
{
  return (geo->first_entry_x + geo->entry_width);
}

grub_size_t
grub_getstringwidth (grub_uint32_t * str, const grub_uint32_t * last_position,
             struct grub_term_output *term)
{
  grub_ssize_t width = 0;

  while (str < last_position)
    {
      struct grub_unicode_glyph glyph;
      glyph.ncomb = 0;
      str += grub_unicode_aglomerate_comb (str, last_position - str, &glyph);
      width += grub_term_getcharwidth (term, &glyph);
      grub_unicode_destroy_glyph (&glyph);
    }
  return width;
}

static int
grub_print_message_indented_real (const char *msg, int margin_left,
                  int margin_right,
                  struct grub_term_output *term, int dry_run)
{
  grub_uint32_t *unicode_msg;
  grub_uint32_t *last_position;
  grub_size_t msg_len = grub_strlen (msg) + 2;
  int ret = 0;

  unicode_msg = grub_calloc (msg_len, sizeof (grub_uint32_t));

  if (!unicode_msg)
    return 0;

  msg_len = grub_utf8_to_ucs4 (unicode_msg, msg_len,
                   (grub_uint8_t *) msg, -1, 0);

  last_position = unicode_msg + msg_len;
  *last_position = 0;

  if (dry_run)
    ret = grub_ucs4_count_lines (unicode_msg, last_position, margin_left,
                 margin_right, term);
  else
    grub_print_ucs4_menu (unicode_msg, last_position, margin_left,
              margin_right, term, 0, -1, 0, 0);

  grub_free (unicode_msg);

  return ret;
}

void
grub_print_message_indented (const char *msg, int margin_left, int margin_right,
                 struct grub_term_output *term)
{
  grub_print_message_indented_real (msg, margin_left, margin_right, term, 0);
}

static void
draw_border (struct grub_term_output *term, const struct grub_term_screen_geometry *geo)
{
  int i;

  grub_term_setcolorstate (term, GRUB_TERM_COLOR_NORMAL);

  grub_term_gotoxy (term, (struct grub_term_coordinate) { geo->first_entry_x - 1,
    geo->first_entry_y - 1 });
  grub_putcode (GRUB_UNICODE_CORNER_UL, term);
  for (i = 0; i < geo->entry_width + 1; i++)
    grub_putcode (GRUB_UNICODE_HLINE, term);
  grub_putcode (GRUB_UNICODE_CORNER_UR, term);

  for (i = 0; i < geo->num_entries; i++)
    {
      grub_term_gotoxy (term, (struct grub_term_coordinate) { geo->first_entry_x - 1,
        geo->first_entry_y + i });
      grub_putcode (GRUB_UNICODE_VLINE, term);
      grub_term_gotoxy (term,
            (struct grub_term_coordinate) { geo->first_entry_x + geo->entry_width + 1,
                geo->first_entry_y + i });
      grub_putcode (GRUB_UNICODE_VLINE, term);
    }

  grub_term_gotoxy (term,
            (struct grub_term_coordinate) { geo->first_entry_x - 1,
            geo->first_entry_y - 1 + geo->num_entries + 1 });
  grub_putcode (GRUB_UNICODE_CORNER_LL, term);
  for (i = 0; i < geo->entry_width + 1; i++)
    grub_putcode (GRUB_UNICODE_HLINE, term);
  grub_putcode (GRUB_UNICODE_CORNER_LR, term);

  grub_term_setcolorstate (term, GRUB_TERM_COLOR_NORMAL);

  grub_term_gotoxy (term,
            (struct grub_term_coordinate) { geo->first_entry_x - 1,
            (geo->first_entry_y - 1 + geo->num_entries
             + GRUB_TERM_MARGIN + 1) });
}

static int
print_message (int nested, int edit, const char *msg,
               struct grub_term_output *term, int dry_run)
{
  int ret = 0;
  grub_term_setcolorstate (term, GRUB_TERM_COLOR_NORMAL);

  if (edit)
  {
    ret += grub_print_message_indented_real
      (_("Minimum Emacs-like screen editing is \
supported. TAB lists completions. Press Ctrl-x or F10 to boot, Ctrl-c or F2 for a \
command-line or ESC to discard edits and return to the GRUB menu."),
                      STANDARD_MARGIN, STANDARD_MARGIN, term, dry_run);
  }
  else if (msg && msg[0])
  {
    ret += grub_print_message_indented_real (msg,
              STANDARD_MARGIN, STANDARD_MARGIN, term, dry_run);
  }
  else
  {
    char *msg_translated;

    msg_translated = grub_xasprintf
        (_("Use the %C and %C keys to select which entry is highlighted."),
                      GRUB_UNICODE_UPARROW, GRUB_UNICODE_DOWNARROW);
    if (!msg_translated)
      return 0;
    ret += grub_print_message_indented_real (msg_translated, STANDARD_MARGIN,
                           STANDARD_MARGIN, term, dry_run);

    grub_free (msg_translated);

    if (nested)
    {
      ret += grub_print_message_indented_real
          (_("Press enter to boot the selected OS, "
             "`e' to edit the commands before booting "
             "or `c' for a command-line. ESC to return previous menu."),
            STANDARD_MARGIN, STANDARD_MARGIN, term, dry_run);
    }
    else
    {
      ret += grub_print_message_indented_real
          (_("Press enter to boot the selected OS, "
             "`e' to edit the commands before booting "
             "or `c' for a command-line."),
            STANDARD_MARGIN, STANDARD_MARGIN, term, dry_run);
    }
  }
  return ret;
}

static void
print_entry (int y, int highlight, grub_menu_entry_t entry,
         const struct menu_viewer_data *data)
{
  const char *title;
  grub_size_t title_len;
  grub_ssize_t len;
  grub_uint32_t *unicode_title;
  grub_ssize_t i;
  grub_uint8_t old_color_normal, old_color_highlight;

  title = entry ? entry->title : "";
  title_len = grub_strlen (title);
  unicode_title = grub_calloc (title_len, sizeof (*unicode_title));
  if (! unicode_title)
    /* XXX How to show this error?  */
    return;

  len = grub_utf8_to_ucs4 (unicode_title, title_len,
                           (grub_uint8_t *) title, -1, 0);
  if (len < 0)
    {
      /* It is an invalid sequence.  */
      grub_free (unicode_title);
      return;
    }

  old_color_normal = grub_term_normal_color;
  old_color_highlight = grub_term_highlight_color;
  grub_term_normal_color = grub_color_menu_normal;
  grub_term_highlight_color = grub_color_menu_highlight;
  grub_term_setcolorstate (data->term, highlight
               ? GRUB_TERM_COLOR_HIGHLIGHT
               : GRUB_TERM_COLOR_NORMAL);

  grub_term_gotoxy (data->term, (struct grub_term_coordinate) { 
      data->geo.first_entry_x, y });

  for (i = 0; i < len; i++)
    if (unicode_title[i] == '\n' || unicode_title[i] == '\b'
    || unicode_title[i] == '\r' || unicode_title[i] == '\e')
      unicode_title[i] = ' ';

  if (data->geo.num_entries > 1)
    grub_putcode (highlight ? '*' : ' ', data->term);

  grub_print_ucs4_menu (unicode_title,
            unicode_title + len,
            0,
            data->geo.right_margin,
            data->term, 0, 1,
            GRUB_UNICODE_RIGHTARROW, 0);

  grub_term_setcolorstate (data->term, GRUB_TERM_COLOR_NORMAL);
  grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) { 
              grub_term_cursor_x (&data->geo), y });

  grub_term_normal_color = old_color_normal;
  grub_term_highlight_color = old_color_highlight;

  grub_term_setcolorstate (data->term, GRUB_TERM_COLOR_NORMAL);
  grub_free (unicode_title);
}

static void
print_entries (grub_menu_t menu, const struct menu_viewer_data *data)
{
  grub_menu_entry_t e;
  int i;

  grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) { 
              data->geo.first_entry_x + data->geo.entry_width
            + data->geo.border + 1,
            data->geo.first_entry_y });

  if (data->geo.num_entries != 1)
    {
      if (data->first)
    grub_putcode (GRUB_UNICODE_UPARROW, data->term);
      else
    grub_putcode (' ', data->term);
    }
  e = grub_menu_get_entry (menu, data->first);

  for (i = 0; i < data->geo.num_entries; i++)
    {
      print_entry (data->geo.first_entry_y + i, data->offset == i,
           e, data);
      if (e)
    e = e->next;
    }

  grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) { data->geo.first_entry_x + data->geo.entry_width
            + data->geo.border + 1,
            data->geo.first_entry_y + data->geo.num_entries - 1 });
  if (data->geo.num_entries == 1)
    {
      if (data->first && e)
    grub_putcode (GRUB_UNICODE_UPDOWNARROW, data->term);
      else if (data->first)
    grub_putcode (GRUB_UNICODE_UPARROW, data->term);
      else if (e)
    grub_putcode (GRUB_UNICODE_DOWNARROW, data->term);
      else
    grub_putcode (' ', data->term);
    }
  else
    {
      if (e)
    grub_putcode (GRUB_UNICODE_DOWNARROW, data->term);
      else
    grub_putcode (' ', data->term);
    }

  grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) { grub_term_cursor_x (&data->geo),
            data->geo.first_entry_y + data->offset });
}

/* Initialize the screen.  If NESTED is non-zero, assume that this menu
   is run from another menu or a command-line. If EDIT is non-zero, show
   a message for the menu entry editor.  */
void
grub_menu_init_page (int nested, int edit,
             struct grub_term_screen_geometry *geo,
             struct grub_term_output *term, const char *msg)
{
  grub_uint8_t old_color_normal, old_color_highlight;
  int msg_num_lines;
  int bottom_message = 1;
  int empty_lines = 1;
  int version_msg = 1;

  geo->border = 1;
  geo->first_entry_x = 1 /* margin */ + 1 /* border */;
  geo->entry_width = grub_term_width (term) - 5;

  geo->first_entry_y = 2 /* two empty lines*/
    + 1 /* GNU GRUB version text  */ + 1 /* top border */;

  geo->timeout_lines = 2;

  /* 3 lines for timeout message and bottom margin.  2 lines for the border.  */
  geo->num_entries = grub_term_height (term) - geo->first_entry_y
    - 1 /* bottom border */
    - 1 /* empty line before info message*/
    - geo->timeout_lines /* timeout */
    - 1 /* empty final line  */;
  msg_num_lines = print_message (nested, edit, NULL, term, 1);
  if (geo->num_entries - msg_num_lines < 3
      || geo->entry_width < 10)
    {
      geo->num_entries += 4;
      geo->first_entry_y -= 2;
      empty_lines = 0;
      geo->first_entry_x -= 1;
      geo->entry_width += 1;
    }
  if (geo->num_entries - msg_num_lines < 3
      || geo->entry_width < 10)
    {
      geo->num_entries += 2;
      geo->first_entry_y -= 1;
      geo->first_entry_x -= 1;
      geo->entry_width += 2;
      geo->border = 0;
    }

  if (geo->entry_width <= 0)
    geo->entry_width = 1;

  if (geo->num_entries - msg_num_lines < 3
      && geo->timeout_lines == 2)
    {
      geo->timeout_lines = 1;
      geo->num_entries++;
    }

  if (geo->num_entries - msg_num_lines < 3)
    {
      geo->num_entries += 1;
      geo->first_entry_y -= 1;
      version_msg = 0;
    }

  if (geo->num_entries - msg_num_lines >= 2)
    geo->num_entries -= msg_num_lines;
  else
    bottom_message = 0;

  /* By default, use the same colors for the menu.  */
  old_color_normal = grub_term_normal_color;
  old_color_highlight = grub_term_highlight_color;
  grub_color_menu_normal = grub_term_normal_color;
  grub_color_menu_highlight = grub_term_highlight_color;

  /* Then give user a chance to replace them.  */
  grub_parse_color_name_pair (&grub_color_menu_normal,
                  grub_env_get ("menu_color_normal"));
  grub_parse_color_name_pair (&grub_color_menu_highlight,
                  grub_env_get ("menu_color_highlight"));

  if (version_msg)
    grub_normal_init_page (term, empty_lines);
  else
    grub_term_cls (term);

  grub_term_normal_color = grub_color_menu_normal;
  grub_term_highlight_color = grub_color_menu_highlight;
  /* disable draw_border */
  const char *env_draw = NULL;
  env_draw = grub_env_get ("grub_draw_border");
  if (env_draw && env_draw[0] == '1')
  {
    if (geo->border)
      draw_border (term, geo);
  }
  grub_term_normal_color = old_color_normal;
  grub_term_highlight_color = old_color_highlight;
  geo->timeout_y = geo->first_entry_y + geo->num_entries
    + geo->border + empty_lines;
  geo->help_y = 0;
  if (bottom_message)
    {
      geo->help_y = geo->timeout_y;
      grub_term_gotoxy (term,
            (struct grub_term_coordinate) { GRUB_TERM_MARGIN,
                geo->timeout_y });

      if (msg && msg[0])
        print_message (nested, edit, msg, term, 0);
      else
        print_message (nested, edit, NULL, term, 0);
      geo->timeout_y += msg_num_lines;
    }
  geo->right_margin = grub_term_width (term)
    - geo->first_entry_x
    - geo->entry_width - 1;
}

static void
menu_text_print_timeout (int timeout, void *dataptr)
{
  struct menu_viewer_data *data = dataptr;
  char *msg_translated = 0;

  grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) { 0, data->geo.timeout_y });

  if (data->timeout_msg == TIMEOUT_TERSE
      || data->timeout_msg == TIMEOUT_TERSE_NO_MARGIN)
    msg_translated = grub_xasprintf (_("%ds"), timeout);
  else
    msg_translated = grub_xasprintf (_("The highlighted entry will be executed automatically in %ds."), timeout);
  if (!msg_translated)
    {
      grub_print_error ();
      grub_errno = GRUB_ERR_NONE;
      return;
    }

  if (data->timeout_msg == TIMEOUT_UNKNOWN)
    {
      data->timeout_msg = grub_print_message_indented_real (msg_translated,
                                3, 1, data->term, 1)
    <= data->geo.timeout_lines ? TIMEOUT_NORMAL : TIMEOUT_TERSE;
      if (data->timeout_msg == TIMEOUT_TERSE)
    {
      grub_free (msg_translated);
      msg_translated = grub_xasprintf (_("%ds"), timeout);
      if (grub_term_width (data->term) < 10)
        data->timeout_msg = TIMEOUT_TERSE_NO_MARGIN;
    }
    }

  grub_print_message_indented (msg_translated,
                   data->timeout_msg == TIMEOUT_TERSE_NO_MARGIN ? 0 : 3,
                   data->timeout_msg == TIMEOUT_TERSE_NO_MARGIN ? 0 : 1,
                   data->term);
  grub_free (msg_translated);

  grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) { 
              grub_term_cursor_x (&data->geo),
            data->geo.first_entry_y + data->offset });
  grub_term_refresh (data->term);
}

static void
menu_text_print_help (void *dataptr, const char *text)
{
  struct menu_viewer_data *data = dataptr;
  int nested = data->nested;
  struct grub_term_output *term = data->term;

  if (!data->geo.help_y)
    return;
  grub_term_gotoxy (term, (struct grub_term_coordinate)
                    { GRUB_TERM_MARGIN, data->geo.help_y });

  grub_term_setcolorstate (term, GRUB_TERM_COLOR_NORMAL);

  if (text && text[0])
  {
    int num;
    num = grub_print_message_indented_real (text,
                    STANDARD_MARGIN, STANDARD_MARGIN, term, 1);
    num = data->geo.timeout_y - data->geo.help_y - num;
    grub_print_message_indented (text, STANDARD_MARGIN, STANDARD_MARGIN, term);
    for (; num > 0; num--)
      grub_print_message_indented (" ", STANDARD_MARGIN, STANDARD_MARGIN, term);
  }
  else
  {
    char *msg = NULL;
    msg = grub_xasprintf (_("Use the %C and %C keys to select which "
                            "entry is highlighted."),
                            GRUB_UNICODE_UPARROW, GRUB_UNICODE_DOWNARROW);
    if (!msg)
      return;
    grub_print_message_indented (msg, STANDARD_MARGIN, STANDARD_MARGIN, term);
    grub_free (msg);
    if (nested)
    {
      grub_print_message_indented (_("Press enter to boot the selected OS, "
            "`e' to edit the commands before booting "
            "or `c' for a command-line. ESC to return previous menu."),
            STANDARD_MARGIN, STANDARD_MARGIN, term);
    }
    else
    {
      grub_print_message_indented (_("Press enter to boot the selected OS, "
            "`e' to edit the commands before booting "
            "or `c' for a command-line."),
            STANDARD_MARGIN, STANDARD_MARGIN, term);
    }
  }
  grub_term_gotoxy (data->term,
          (struct grub_term_coordinate) { grub_term_cursor_x (&data->geo),
          data->geo.first_entry_y + data->offset });
}

static void
menu_text_set_chosen_entry (int entry, void *dataptr)
{
  struct menu_viewer_data *data = dataptr;
  int oldoffset = data->offset;
  int complete_redraw = 0;
  grub_menu_entry_t e;

  data->offset = entry - data->first;
  if (data->offset > data->geo.num_entries - 1)
  {
    data->first = entry - (data->geo.num_entries - 1);
    data->offset = data->geo.num_entries - 1;
    complete_redraw = 1;
  }
  if (data->offset < 0)
  {
    data->offset = 0;
    data->first = entry;
    complete_redraw = 1;
  }
  if (complete_redraw)
    print_entries (data->menu, data);
  else
  {
    e = grub_menu_get_entry (data->menu, data->first + oldoffset);
    print_entry (data->geo.first_entry_y + oldoffset, 0, e, data);
    e = grub_menu_get_entry (data->menu, data->first + data->offset);
    print_entry (data->geo.first_entry_y + data->offset, 1, e, data);
    menu_text_print_help (data, e ? e->help_message : NULL);
  }
  grub_term_refresh (data->term);
}

static void
menu_text_fini (void *dataptr)
{
  struct menu_viewer_data *data = dataptr;

  grub_term_setcursor (data->term, 1);
  grub_term_cls (data->term);
  grub_free (data);
}

static void
menu_set_animation_state (int need_refresh __attribute__ ((unused)),
                          void *data __attribute__ ((unused)))
{
}

static void
menu_update_screen (void *data __attribute__ ((unused)))
{
}

static void
menu_text_clear_timeout (void *dataptr)
{
  struct menu_viewer_data *data = dataptr;
  int i;

  for (i = 0; i < data->geo.timeout_lines;i++)
    {
      grub_term_gotoxy (data->term, (struct grub_term_coordinate) {
      0, data->geo.timeout_y + i });
      grub_print_spaces (data->term, grub_term_width (data->term) - 1);
    }
  if (data->geo.num_entries <= 5 && !data->geo.border)
    {
      grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) { 
              data->geo.first_entry_x + data->geo.entry_width
                + data->geo.border + 1,
                data->geo.first_entry_y + data->geo.num_entries - 1
                });
      grub_putcode (' ', data->term);

      data->geo.timeout_lines = 0;
      data->geo.num_entries++;
      print_entries (data->menu, data);
    }
  grub_term_gotoxy (data->term,
            (struct grub_term_coordinate) {
              grub_term_cursor_x (&data->geo),
            data->geo.first_entry_y + data->offset });
  grub_term_refresh (data->term);
}

grub_err_t
grub_menu_try_text (struct grub_term_output *term, 
            int entry, grub_menu_t menu, int nested)
{
  struct menu_viewer_data *data;
  struct grub_menu_viewer *instance;
  grub_menu_entry_t e;

  instance = grub_zalloc (sizeof (*instance));
  if (!instance)
    return grub_errno;

  data = grub_zalloc (sizeof (*data));
  if (!data)
  {
    grub_free (instance);
    return grub_errno;
  }

  data->term = term;
  instance->data = data;
  instance->set_chosen_entry = menu_text_set_chosen_entry;
  instance->print_timeout = menu_text_print_timeout;
  instance->clear_timeout = menu_text_clear_timeout;
  instance->set_animation_state = menu_set_animation_state;
  instance->update_screen = menu_update_screen;
  instance->fini = menu_text_fini;

  data->menu = menu;

  data->offset = entry;
  data->first = 0;
  data->nested = nested;

  grub_term_setcursor (data->term, 0);
  e = grub_menu_get_entry (data->menu, entry);
  grub_menu_init_page (nested, 0, &data->geo, data->term,
                       e ? e->help_message : NULL);

  if (data->offset > data->geo.num_entries - 1)
  {
    data->first = data->offset - (data->geo.num_entries - 1);
    data->offset = data->geo.num_entries - 1;
  }

  print_entries (menu, data);
  grub_term_refresh (data->term);
  grub_menu_register_viewer (instance);

  return GRUB_ERR_NONE;
}
