/* video/setmode.c - Smart video mode selection based on preferences. */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2006,2007,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/video.h>
#include <grub/types.h>
#include <grub/mm.h>
#include <grub/misc.h>

/* Set the video mode based on the preferred modes specified in MODE_LIST in
   the form: <width>x<height>[x<depth>][;...]

   For example:  640x480;800x600x8;400x300x32

   If MODE_LIST is null, or no modes in it are usable, then DEFAULT_WIDTH and
   DEFAULT_HEIGHT are used to set the mode.  The MODE_FLAGS argument determines
   the video mode flags such as double buffering that are used.  */

grub_err_t
grub_video_setup_preferred_mode (const char *mode_list, int mode_flags,
                                 int default_width, int default_height)
{
  int mode_found = 0;

  if (mode_list != NULL)
    {
      /* Take copy of mode_list as we don't want tat.  */
      char *const modes_copy = grub_strdup (mode_list);
      if (modes_copy == NULL)
        return grub_errno;

      /* Initialize next mode.  */
      char *next_mode = modes_copy;

      /* Loop until all modes has been tested out.  */
      while ((next_mode != NULL) && !mode_found)
        {
          /* Use last next_mode as current mode.  */
          char *tmp = next_mode;

          int width = -1;
          int height = -1;
          int depth = -1;

          /* Save position of next mode and separate modes.  */
          next_mode = grub_strchr(next_mode, ';');
          if (next_mode)
            {
              *next_mode = 0;
              next_mode++;
            }

          /* Skip whitespace.  */
          while (grub_isspace (*tmp))
            tmp++;

          /* Initialize token holders.  */
          char *current_mode = tmp;
          char *param = tmp;
          char *value = NULL;

          /* Parse <width>x<height>[x<depth>]*/

          /* Find width value.  */
          value = param;
          param = grub_strchr(param, 'x');
          if (param == NULL)
            {
              grub_err_t rc;

              /* First setup error message.  */
              rc = grub_error (GRUB_ERR_BAD_ARGUMENT, 
                               "Invalid mode: %s\n",
                               current_mode);

              /* Free memory before returning.  */
              grub_free (modes_copy);

              return rc;
            }

          *param = 0;
          param++;

          width = grub_strtoul (value, 0, 0);
          if (grub_errno != GRUB_ERR_NONE)
            {
              grub_err_t rc;

              /* First setup error message.  */
              rc = grub_error (GRUB_ERR_BAD_ARGUMENT, 
                               "Invalid mode: %s\n",
                               current_mode);

              /* Free memory before returning.  */
              grub_free (modes_copy);

              return rc;
            }

          /* Find height value.  */
          value = param;
          param = grub_strchr(param, 'x');
          if (param == NULL)
            {
              height = grub_strtoul (value, 0, 0);
              if (grub_errno != GRUB_ERR_NONE)
                {
                  grub_err_t rc;

                  /* First setup error message.  */
                  rc = grub_error (GRUB_ERR_BAD_ARGUMENT, 
                                   "Invalid mode: %s\n",
                                   current_mode);

                  /* Free memory before returning.  */
                  grub_free (modes_copy);

                  return rc;
                }
            }
          else
            {
              /* We have optional color depth value.  */
              *param = 0;
              param++;

              height = grub_strtoul (value, 0, 0);
              if (grub_errno != GRUB_ERR_NONE)
                {
                  grub_err_t rc;

                  /* First setup error message.  */
                  rc = grub_error (GRUB_ERR_BAD_ARGUMENT, 
                                   "Invalid mode: %s\n",
                                   current_mode);

                  /* Free memory before returning.  */
                  grub_free (modes_copy);

                  return rc;
                }

              /* Convert color depth value.  */
              value = param;
              depth = grub_strtoul (value, 0, 0);
              if (grub_errno != GRUB_ERR_NONE)
                {
                  grub_err_t rc;

                  /* First setup error message.  */
                  rc = grub_error (GRUB_ERR_BAD_ARGUMENT, 
                                   "Invalid mode: %s\n",
                                   current_mode);

                  /* Free memory before returning.  */
                  grub_free (modes_copy);

                  return rc;
                }
            }

          /* Try out video mode.  */

          int flags = mode_flags;
          /* If we have <= 8 bits, assume it is an indexed color mode.  */
          if ((depth <= 8) && (depth != -1))
            flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR;

          /* We have > 8 bits; assume that it is RGB color mode.  */
          if (depth > 8)
            flags |= GRUB_VIDEO_MODE_TYPE_RGB;

          /* If user requested specific depth, pass the request to driver.  */
          if (depth != -1)
            flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS)
              & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK;

          /* Try to initialize requested mode.  Ignore any errors.  */
          grub_error_push ();
          if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE)
            {
              grub_error_pop ();
              continue;
            }

          /* Figure out what mode we ended up.  */
          struct grub_video_mode_info mode_info;
          if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE)
            {
              /* Couldn't get video mode info, restore old mode
                 and continue to next one.  */
              grub_error_pop ();

              grub_video_restore ();
              continue;
            }

          /* Restore state of error stack.  */
          grub_error_pop ();

          /* Mode found!  Exit loop.  */
          mode_found = 1;
        }

      /* Free memory.  */
      grub_free (modes_copy);
    }

  if (!mode_found)
    {
      /* No gfxmode variable set, or no listed mode was supported.
         Use the caller-specified defaults.  */
      int flags = mode_flags | GRUB_VIDEO_MODE_TYPE_RGB;

      /* Initialize user requested mode.  */
      if (grub_video_setup (default_width, default_height, flags) 
          != GRUB_ERR_NONE)
        return grub_errno;

      /* Figure out what mode we ended up.  */
      struct grub_video_mode_info mode_info;
      if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE)
        grub_video_restore ();
      else
        mode_found = 1;
    }

  if (!mode_found)
    return grub_error (GRUB_ERR_BAD_ARGUMENT,
                       "No suitable mode found.");

  return (grub_errno = GRUB_ERR_NONE);
}
