/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2022 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>

#ifdef HAVE_LANGINFO_H
#include <langinfo.h>
#endif /* HAVE_LANGINFO_H */

#undef CAN_GLOB
#if defined(HAVE_GLOB)
#define CAN_GLOB
#include <glob.h>

#elif defined(__MINGW32__)
#define CAN_GLOB
#include <io.h>

#else /* glob: paradigm-specific global definitions */
#warning file globbing support not available on this platform
#endif /* glob: paradigm-specific global definitions */

#include "log.h"
#include "menu.h"
#include "strfmt.h"
#include "prefs.h"
#include "timing.h"
#include "parse.h"
#include "file.h"

typedef struct {
  char *directory;
  const char *extension;
  const char *pattern;
  char *initial;
  char *current;
  unsigned none:1;

#if defined(HAVE_GLOB)
  glob_t glob;
#elif defined(__MINGW32__)
  char **names;
  int offset;
#endif /* glob: paradigm-specific field declarations */

  char **paths;
  int count;
  unsigned char setting;
  char *pathsArea[3];
} FileData;

typedef struct {
  Menu *menu;
  unsigned opened:1;

  unsigned int total;
  unsigned int visible;
} SubmenuData;

struct MenuStruct {
  Menu *parent;

  struct {
    MenuItem *array;
    unsigned int size;
    unsigned int count;
    unsigned int index;
  } items;

  unsigned int menuNumber;
  unsigned int submenuCount;
  MenuItem *activeItem;

  char valueBuffer[0X20];
};

typedef struct {
  int (*testItem) (const MenuItem *item);
  int (*beginItem) (MenuItem *item);
  void (*endItem) (MenuItem *item, int deallocating);
  void (*activateItem) (MenuItem *item);
  const char * (*getValue) (const MenuItem *item);
  const char * (*getText) (const MenuItem *item);
  const char * (*getComment) (const MenuItem *item);
} MenuItemMethods;

struct MenuItemStruct {
  Menu *menu;
  unsigned char *setting;                 /* pointer to current value */

  const char *title;                      /* item name for presentation */
  const char *subtitle;                      /* item name for presentation */

  const MenuItemMethods *methods;
  MenuItemTester *test;                     /* returns true if item should be presented */
  MenuItemChanged *changed;

  unsigned char minimum;                  /* lowest valid value */
  unsigned char maximum;                  /* highest valid value */
  unsigned char step;                  /* present only multiples of this value */

  union {
    const char *text;
    const MenuString *strings;
    FileData *files;
    SubmenuData *submenu;

    struct {
      const char *unit;
      NumericMenuItemFormatter *formatter;
    } numeric;

    struct {
      MenuToolFunction *function;
    } tool;
  } data;
};

static inline const char *
getLocalizedText (const char *string) {
  return (string && *string)? gettext(string): "";
}

static const char *
formatValue (Menu *menu, const char *format, ...) {
  {
    va_list arguments;

    va_start(arguments, format);
    vsnprintf(menu->valueBuffer, sizeof(menu->valueBuffer), format, arguments);
    va_end(arguments);
  }

  return menu->valueBuffer;
}

Menu *
newMenu (void) {
  Menu *menu;

  if ((menu = malloc(sizeof(*menu)))) {
    memset(menu, 0, sizeof(*menu));
    menu->parent = NULL;

    menu->items.array = NULL;
    menu->items.size = 0;
    menu->items.count = 0;
    menu->items.index = 0;

    menu->menuNumber = 0;
    menu->submenuCount = 0;
    menu->activeItem = NULL;

    return menu;
  } else {
    logMallocError();
  }

  return NULL;
}

static int
beginMenuItem (MenuItem *item) {
  return !item->methods->beginItem || item->methods->beginItem(item);
}

static void
endMenuItem (MenuItem *item, int deallocating) {
  if (item->methods->endItem) item->methods->endItem(item, deallocating);
}

void
destroyMenu (Menu *menu) {
  if (menu) {
    if (menu->items.array) {
      MenuItem *item = menu->items.array;
      const MenuItem *end = item + menu->items.count;

      while (item < end) endMenuItem(item++, 1);
      free(menu->items.array);
    }

    free(menu);
  }
}

unsigned int
getMenuNumber (const Menu *menu) {
  return menu->menuNumber;
}

Menu *
getMenuParent (const Menu *menu) {
  return menu->parent;
}

unsigned int
getMenuSize (const Menu *menu) {
  return menu->items.count;
}

unsigned int
getMenuIndex (const Menu *menu) {
  return menu->items.index;
}

MenuItem *
getMenuItem (Menu *menu, unsigned int index) {
  return (index < menu->items.count)? &menu->items.array[index]: NULL;
}

static MenuItem *
getSelectedMenuItem (Menu *menu) {
  return getMenuItem(menu, menu->items.index);
}

static int
testMenuItem (const MenuItem *item, int all) {
  if (!item) return 0;
  if (all) return 1;
  if (item->methods->testItem && !item->methods->testItem(item)) return 0;
  return !item->test || item->test();
}

int
isMenuItemSettable (const MenuItem *item) {
  return !!item->setting;
}

int
isMenuItemAction (const MenuItem *item) {
  return !!item->methods->activateItem;
}

int
isMenuItemVisible (const MenuItem *item) {
  return testMenuItem(item, 0);
}

static inline int
testMenuItemActive (Menu *menu, unsigned int index) {
  return testMenuItem(getMenuItem(menu, index), 0);
}

static inline int
testMenuItemVisible (Menu *menu, unsigned int index) {
  return testMenuItem(getMenuItem(menu, index), prefs.showAllItems);
}

Menu *
getMenuItemMenu (const MenuItem *item) {
  return item->menu;
}

unsigned int
getMenuItemIndex (const MenuItem *item) {
  return item - item->menu->items.array;
}

const char *
getMenuItemTitle (const MenuItem *item) {
  return getLocalizedText(item->title);
}

const char *
getMenuItemSubtitle (const MenuItem *item) {
  return getLocalizedText(item->subtitle);
}

const char *
getMenuItemValue (const MenuItem *item) {
  return item->methods->getValue? item->methods->getValue(item): "";
}

const char *
getMenuItemText (const MenuItem *item) {
  return item->methods->getText? item->methods->getText(item): getMenuItemValue(item);
}

const char *
getMenuItemComment (const MenuItem *item) {
  return item->methods->getComment? item->methods->getComment(item): "";
}

static MenuItem *
newMenuItem (Menu *menu, unsigned char *setting, const MenuString *name) {
  if (menu->items.count == menu->items.size) {
    unsigned int newSize = menu->items.size? (menu->items.size << 1): 0X10;
    MenuItem *newArray = realloc(menu->items.array, (newSize * sizeof(*newArray)));

    if (!newArray) {
      logMallocError();
      return NULL;
    }

    menu->items.array = newArray;
    menu->items.size = newSize;
  }

  {
    MenuItem *item = getMenuItem(menu, menu->items.count++);

    item->menu = menu;
    item->setting = setting;

    if (name) {
      item->title = name->label;
      item->subtitle = name->comment;
    } else {
      item->title = NULL;
      item->subtitle = NULL;
    }

    item->methods = NULL;
    item->test = NULL;
    item->changed = NULL;

    item->minimum = 0;
    item->maximum = 0;
    item->step = 1;

    return item;
  }
}

void
setMenuItemTester (MenuItem *item, MenuItemTester *handler) {
  item->test = handler;
}

void
setMenuItemChanged (MenuItem *item, MenuItemChanged *handler) {
  item->changed = handler;
}

static const char *
getValue_text (const MenuItem *item) {
  return item->data.text;
}

static const MenuItemMethods menuItemMethods_text = {
  .getValue = getValue_text
};

MenuItem *
newTextMenuItem (Menu *menu, const MenuString *name, const char *text) {
  MenuItem *item = newMenuItem(menu, NULL, name);

  if (item) {
    item->methods = &menuItemMethods_text;
    item->data.text = text;
  }

  return item;
}

static const char *
getValue_numeric (const MenuItem *item) {
  Menu *menu = item->menu;

  item->data.numeric.formatter(
    menu, *item->setting,
    menu->valueBuffer, sizeof(menu->valueBuffer)
  );

  return menu->valueBuffer;
}

static const char *
getComment_numeric (const MenuItem *item) {
  return getLocalizedText(item->data.numeric.unit);
}

static const MenuItemMethods menuItemMethods_numeric = {
  .getValue = getValue_numeric,
  .getComment = getComment_numeric
};

static void
defaultNumericMenuItemFormatter (
  Menu *menu, unsigned char value,
  char *buffer, size_t size
) {
  snprintf(buffer, size, "%u", value);
}

MenuItem *
newNumericMenuItem (
  Menu *menu, unsigned char *setting, const MenuString *name,
  unsigned char minimum, unsigned char maximum, unsigned char step,
  const char *unit, NumericMenuItemFormatter *formatter
) {
  if (!formatter) formatter = defaultNumericMenuItemFormatter;

  MenuItem *item = newMenuItem(menu, setting, name);

  if (item) {
    item->methods = &menuItemMethods_numeric;
    item->minimum = minimum;
    item->maximum = maximum;
    item->step = step;
    item->data.numeric.unit = unit;
    item->data.numeric.formatter = formatter;
  }

  return item;
}

static void
formatTime (Menu *menu, unsigned char time, char *buffer, size_t size) {
  unsigned int milliseconds = PREFS2MSECS(time);

  unsigned int seconds = milliseconds / MSECS_PER_SEC;
  milliseconds %= MSECS_PER_SEC;

  const char *decimalPoint;
#ifdef HAVE_NL_LANGINFO
  decimalPoint = nl_langinfo(RADIXCHAR);
#else /* HAVE_NL_LANGINFO */
  decimalPoint = NULL;
#endif /* HAVE_NL_LANGINFO */
  if (!decimalPoint) decimalPoint = ".";

  size_t end;
  size_t decimalFrom;
  size_t decimalTo;

  STR_BEGIN(buffer, size);
  STR_PRINTF("%u", seconds);
  decimalFrom = STR_LENGTH;
  STR_PRINTF("%s", decimalPoint);
  decimalTo = STR_LENGTH;
  STR_PRINTF("%03u", milliseconds);
  end = STR_LENGTH;
  STR_END;

  while (buffer[--end] == '0');
  if (++end == decimalTo) end = decimalFrom;
  buffer[end] = 0;
}

MenuItem *
newTimeMenuItem (
  Menu *menu, unsigned char *setting,
  const MenuString *name
) {
  return newNumericMenuItem(menu, setting, name, 10, 250, 10, strtext("seconds"), formatTime);
}

MenuItem *
newPercentMenuItem (
  Menu *menu, unsigned char *setting,
  const MenuString *name, unsigned char step
) {
  return newNumericMenuItem(menu, setting, name, 0, 100, step, "%", NULL);
}

static const char *
getValue_strings (const MenuItem *item) {
  const MenuString *strings = item->data.strings;
  return getLocalizedText(strings[*item->setting - item->minimum].label);
}

static const char *
getComment_strings (const MenuItem *item) {
  const MenuString *strings = item->data.strings;
  return getLocalizedText(strings[*item->setting - item->minimum].comment);
}

static const MenuItemMethods menuItemMethods_strings = {
  .getValue = getValue_strings,
  .getComment = getComment_strings
};

static void
setMenuItemStrings (MenuItem *item, const MenuString *strings, unsigned char count) {
  item->methods = &menuItemMethods_strings;
  item->data.strings = strings;
  item->minimum = 0;
  item->maximum = count - 1;
  item->step = 1;
}

MenuItem *
newStringsMenuItem (
  Menu *menu, unsigned char *setting, const MenuString *name,
  const MenuString *strings, unsigned char count
) {
  MenuItem *item = newMenuItem(menu, setting, name);

  if (item) {
    setMenuItemStrings(item, strings, count);
  }

  return item;
}

MenuItem *
newBooleanMenuItem (Menu *menu, unsigned char *setting, const MenuString *name) {
  static const MenuString strings[] = {
    {.label=strtext("No")},
    {.label=strtext("Yes")}
  };

  return newEnumeratedMenuItem(menu, setting, name, strings);
}

static int
qsortCompare_fileNames (const void *element1, const void *element2) {
  const char *const *name1 = element1;
  const char *const *name2 = element2;
  return strcmp(*name1, *name2);
}

static int
beginItem_files (MenuItem *item) {
  FileData *files = item->data.files;
  int index;

  files->paths = files->pathsArea;
  files->count = ARRAY_COUNT(files->pathsArea) - 1;
  files->paths[files->count] = NULL;
  index = files->count;

#ifdef CAN_GLOB
  {
#ifdef HAVE_FCHDIR
    int originalDirectory = open(".", O_RDONLY);

    if (originalDirectory != -1)
#else /* HAVE_FCHDIR */
    char *originalDirectory = getWorkingDirectory();

    if (originalDirectory)
#endif /* HAVE_FCHDIR */
    {
      if (chdir(files->directory) != -1) {
#if defined(HAVE_GLOB)
        memset(&files->glob, 0, sizeof(files->glob));
        files->glob.gl_offs = files->count;

        if (glob(files->pattern, GLOB_DOOFFS, NULL, &files->glob) == 0) {
          files->paths = files->glob.gl_pathv;

          /* The behaviour of gl_pathc is inconsistent. Some implementations
           * include the leading NULL pointers and some don't. Let's just
           * figure it out the hard way by finding the trailing NULL.
           */
          while (files->paths[files->count]) files->count += 1;
        }
#elif defined(__MINGW32__)
        struct _finddata_t findData;
        long findHandle = _findfirst(files->pattern, &findData);
        int allocated = files->count | 0XF;

        files->offset = files->count;
        files->names = malloc(allocated * sizeof(*files->names));

        if (findHandle != -1) {
          do {
            if (files->count >= allocated) {
              allocated = allocated * 2;
              files->names = realloc(files->names, allocated * sizeof(*files->names));
            }

            files->names[files->count++] = strdup(findData.name);
          } while (_findnext(findHandle, &findData) == 0);

          _findclose(findHandle);
        }

        files->names = realloc(files->names, files->count * sizeof(*files->names));
        files->paths = files->names;
#endif /* glob: paradigm-specific field initialization */

#ifdef HAVE_FCHDIR
        if (fchdir(originalDirectory) == -1) logSystemError("fchdir");
#else /* HAVE_FCHDIR */
        if (chdir(originalDirectory) == -1) logSystemError("chdir");
#endif /* HAVE_FCHDIR */
      } else {
        logMessage(LOG_ERR, "%s: %s: %s",
                   gettext("cannot set working directory"), files->directory, strerror(errno));
      }

#ifdef HAVE_FCHDIR
      close(originalDirectory);
#else /* HAVE_FCHDIR */
      free(originalDirectory);
#endif /* HAVE_FCHDIR */
    } else {
#ifdef HAVE_FCHDIR
      logMessage(LOG_ERR, "%s: %s",
                 gettext("cannot open working directory"), strerror(errno));
#else /* HAVE_FCHDIR */
      logMessage(LOG_ERR, "%s", gettext("cannot determine working directory"));
#endif /* HAVE_FCHDIR */
    }
  }
#endif /* CAN_GLOB */

  qsort(&files->paths[index], files->count-index, sizeof(*files->paths), qsortCompare_fileNames);
  if (files->none) files->paths[--index] = "";
  files->paths[--index] = files->initial;
  files->paths += index;
  files->count -= index;
  files->setting = 0;

  for (index=1; index<files->count; index+=1) {
    if (strcmp(files->paths[index], files->initial) == 0) {
      files->paths += 1;
      files->count -= 1;
      break;
    }
  }

  for (index=0; index<files->count; index+=1) {
    if (strcmp(files->paths[index], files->current) == 0) {
      files->setting = index;
      break;
    }
  }

  item->maximum = files->count - 1;
  return 1;
}

static void
endItem_files (MenuItem *item, int deallocating) {
  FileData *files = item->data.files;

  if (files->current) free(files->current);
  files->current = deallocating? NULL: strdup(files->paths[files->setting]);

#if defined(HAVE_GLOB)
  if (files->glob.gl_pathc) {
    for (int i=0; i<files->glob.gl_offs; i+=1) files->glob.gl_pathv[i] = NULL;
    globfree(&files->glob);
    files->glob.gl_pathc = 0;
  }
#elif defined(__MINGW32__)
  if (files->names) {
    for (int i=files->offset; i<files->count; i+=1) free(files->names[i]);
    free(files->names);
    files->names = NULL;
  }
#endif /* glob: paradigm-specific memory deallocation */
}

static const char *
getValue_files (const MenuItem *item) {
  const FileData *files = item->data.files;
  const char *path;

  if (item == item->menu->activeItem) {
    path = files->paths[files->setting];
  } else {
    path = files->current;
  }

  if (!path) path = "";
  const char *name = locatePathName(path);

  if (name == path) {
    const char *extension = files->extension;

    if (hasFileExtension(name, extension)) {
      int length = strlen(path) - strlen(extension);
      Menu *menu = item->menu;

      snprintf(
        menu->valueBuffer, sizeof(menu->valueBuffer),
        "%.*s", length, name
      );

      path = menu->valueBuffer;
    }
  }

  return path;
}

static const char *
getText_files (const MenuItem *item) {
  return item->methods->getValue(item);
}

static const MenuItemMethods menuItemMethods_files = {
  .beginItem = beginItem_files,
  .endItem = endItem_files,
  .getValue = getValue_files,
  .getText = getText_files
};

MenuItem *
newFilesMenuItem (
  Menu *menu, const MenuString *name,
  const char *directory, const char *subdirectory, const char *extension,
  const char *initial, int none
) {
  FileData *files;

  if ((files = malloc(sizeof(*files)))) {
    memset(files, 0, sizeof(*files));
    files->extension = extension;
    files->none = !!none;

    char *pattern;
    {
      const char *strings[] = {"*", extension};
      pattern = joinStrings(strings, ARRAY_COUNT(strings));
    }

    if (pattern) {
      files->pattern = pattern; 

      if ((files->initial = *initial? ensureFileExtension(initial, extension): strdup(""))) {
        if ((files->current = strdup(files->initial))) {
          if (subdirectory) {
            files->directory = makePath(directory, subdirectory);
          } else if (!(files->directory = strdup(directory))) {
            logMallocError();
          }

          if (files->directory) {
            MenuItem *item = newMenuItem(menu, &files->setting, name);

            if (item) {
              item->methods = &menuItemMethods_files;
              item->data.files = files;
              return item;
            }

            free(files->directory);
          }

          free(files->current);
        } else {
          logMallocError();
        }

        free(files->initial);
      } else {
        logMallocError();
      }

      free(pattern);
    } else {
      logMallocError();
    }

    free(files);
  } else {
    logMallocError();
  }

  return NULL;
}

static void
activateItem_tool (MenuItem *item) {
  item->data.tool.function();
}

static const char *
getValue_tool (const MenuItem *item) {
  return NULL;
}

static const MenuItemMethods menuItemMethods_tool = {
  .activateItem = activateItem_tool,
  .getValue = getValue_tool
};

MenuItem *
newToolMenuItem (Menu *menu, const MenuString *name, MenuToolFunction *function) {
  MenuItem *item = newMenuItem(menu, NULL, name);

  if (item) {
    item->methods = &menuItemMethods_tool;
    item->data.tool.function = function;
  }

  return item;
}

static MenuItem *
getParentMenuItem (const MenuItem *item) {
  return getSelectedMenuItem(item->menu->parent);
}

static int
testItem_submenu (const MenuItem *item) {
  return getMenuSize(item->data.submenu->menu) > 1;
}

static int
beginItem_submenu (MenuItem *item) {
  item->data.submenu->visible = 0;

  {
    Menu *menu = item->data.submenu->menu;
    unsigned int size = getMenuSize(menu);

    for (unsigned int index=1; index<size; index+=1) {
      if (testMenuItemActive(menu, index)) {
        item->data.submenu->visible += 1;
      }
    }

    item->data.submenu->total = size - 1;
  }

  return 1;
}

static void
endItem_submenu (MenuItem *item, int deallocating) {
  if (deallocating) {
    SubmenuData *submenu = item->data.submenu;

    destroyMenu(submenu->menu);
    free(submenu);
  }
}

static void
activateItem_submenu (MenuItem *item) {
  endMenuItem(item, 0);
  item->data.submenu->opened = 1;
}

static const char *
getValue_submenu (const MenuItem *item) {
  return "--->";
}

static const char *
getComment_submenu (const MenuItem *item) {
  if (!prefs.showSubmenuSizes) return "";

  {
    const SubmenuData *submenu = item->data.submenu;

    return prefs.showAllItems?
             formatValue(item->menu, "%u", submenu->total):
             formatValue(item->menu, "%u/%u", submenu->visible, submenu->total);
  }
}

static const MenuItemMethods menuItemMethods_submenu = {
  .testItem = testItem_submenu,
  .beginItem = beginItem_submenu,
  .endItem = endItem_submenu,
  .activateItem = activateItem_submenu,
  .getValue = getValue_submenu,
  .getComment = getComment_submenu
};

static void
activateItem_close (MenuItem *item) {
  item = getParentMenuItem(item);
  item->data.submenu->opened = 0;
  beginMenuItem(item);
}

static const char *
getValue_close (const MenuItem *item) {
  return getLocalizedText(strtext("Close"));
}

static const char *
getComment_close (const MenuItem *item) {
  return getMenuItemTitle(getParentMenuItem(item));
}

static const MenuItemMethods menuItemMethods_close = {
  .activateItem = activateItem_close,
  .getValue = getValue_close,
  .getComment = getComment_close
};

Menu *
newSubmenuMenuItem (
  Menu *menu, const MenuString *name
) {
  SubmenuData *submenu;

  if ((submenu = malloc(sizeof(*submenu)))) {
    memset(submenu, 0, sizeof(*submenu));

    if ((submenu->menu = newMenu())) {
      static const MenuString closeName = {.label="<---"};
      MenuItem *close;

      if ((close = newMenuItem(submenu->menu, NULL, &closeName))) {
        MenuItem *item;

        if ((item = newMenuItem(menu, NULL, name))) {
          submenu->menu->parent = menu;
          submenu->opened = 0;
          close->methods = &menuItemMethods_close;

          item->methods = &menuItemMethods_submenu;
          item->data.submenu = submenu;

          while (1) {
            menu->submenuCount += 1;
            if (!menu->parent) break;
            menu = menu->parent;
          }

          submenu->menu->menuNumber = menu->submenuCount;
          return submenu->menu;
        }
      }

      destroyMenu(submenu->menu);
    }

    free(submenu);
  } else {
    logMallocError();
  }

  return NULL;
}

void
changeMenuItem (MenuItem *item) {
  Menu *menu = item->menu;

  menu->items.index = getMenuItemIndex(item);
}

int
changeMenuItemPrevious (Menu *menu, int wrap) {
  unsigned int index = menu->items.index;
  if (index >= menu->items.count) return 0;

  do {
    if (!menu->items.index) {
      if (!wrap) {
        menu->items.index = index;
        return 0;
      }

      menu->items.index = menu->items.count;
    }

    if (--menu->items.index == index) return 0;
  } while (!testMenuItemVisible(menu, menu->items.index));

  return 1;
}

int
changeMenuItemNext (Menu *menu, int wrap) {
  unsigned int index = menu->items.index;
  if (index >= menu->items.count) return 0;

  do {
    if (++menu->items.index == menu->items.count) {
      if (!wrap) {
        menu->items.index = index;
        return 0;
      }

      menu->items.index = 0;
    }

    if (menu->items.index == index) return 0;
  } while (!testMenuItemVisible(menu, menu->items.index));

  return 1;
}

int
changeMenuItemFirst (Menu *menu) {
  if (!menu->items.count) return 0;
  menu->items.index = 0;
  return testMenuItemVisible(menu, menu->items.index) || changeMenuItemNext(menu, 0);
}

int
changeMenuItemLast (Menu *menu) {
  if (!menu->items.count) return 0;
  menu->items.index = menu->items.count - 1;
  return testMenuItemVisible(menu, menu->items.index) || changeMenuItemPrevious(menu, 0);
}

int
changeMenuItemIndex (Menu *menu, unsigned int index) {
  if (index >= menu->items.count) return 0;
  menu->items.index = index;
  return 1;
}

static int
activateMenuItem (MenuItem *item) {
  if (!item->methods->activateItem) return 0;
  item->methods->activateItem(item);
  return 1;
}

static int
adjustMenuSetting (const MenuItem *item, int (*adjust) (const MenuItem *item, int wrap), int wrap) {
  unsigned char setting = *item->setting;
  int count = item->maximum - item->minimum + 1;

  do {
    int ok = 0;

    if (--count) {
      if (adjust(item, wrap)) {
        ok = 1;
      }
    }

    if (!ok) {
      *item->setting = setting;
      return 0;
    }
  } while ((*item->setting % item->step) || (item->changed && !item->changed(item, *item->setting)));

  return 1;
}

static int
decrementMenuSetting (const MenuItem *item, int wrap) {
  if ((*item->setting)-- <= item->minimum) {
    if (!wrap) return 0;
    *item->setting = item->maximum;
  }

  return 1;
}

int
changeMenuSettingPrevious (Menu *menu, int wrap) {
  MenuItem *item = getCurrentMenuItem(menu);

  if (activateMenuItem(item)) return 1;
  if (!item->setting) return 0;
  return adjustMenuSetting(item, decrementMenuSetting, wrap);
}

static int
incrementMenuSetting (const MenuItem *item, int wrap) {
  if ((*item->setting)++ >= item->maximum) {
    if (!wrap) return 0;
    *item->setting = item->minimum;
  }

  return 1;
}

int
changeMenuSettingNext (Menu *menu, int wrap) {
  MenuItem *item = getCurrentMenuItem(menu);

  if (activateMenuItem(item)) return 1;
  if (!item->setting) return 0;
  return adjustMenuSetting(item, incrementMenuSetting, wrap);
}

int
changeMenuSettingScaled (Menu *menu, unsigned int index, unsigned int count) {
  MenuItem *item = getCurrentMenuItem(menu);

  if (activateMenuItem(item)) return 1;

  if (item->setting) {
    unsigned char oldSetting = *item->setting;

    if (item->methods->getValue == getValue_numeric) {
      *item->setting = rescaleInteger(index, count-1, item->maximum-item->minimum) + item->minimum;
    } else {
      *item->setting = index % (item->maximum + 1);
    }

    if (!item->changed || item->changed(item, *item->setting)) return 1;
    *item->setting = oldSetting;
  }

  return 0;
}

MenuItem *
getCurrentMenuItem (Menu *menu) {
  MenuItem *newItem = getSelectedMenuItem(menu);
  MenuItem *oldItem = menu->activeItem;

  if (newItem != oldItem) {
    if (oldItem) endMenuItem(oldItem, 0);
    menu->activeItem = beginMenuItem(newItem)? newItem: NULL;
  }

  return newItem;
}

Menu *
getCurrentSubmenu (Menu *menu) {
  while (1) {
    MenuItem *item = getCurrentMenuItem(menu);

    if (item->methods != &menuItemMethods_submenu) break;
    if (!item->data.submenu->opened) break;
    menu = item->data.submenu->menu;
  }

  return menu;
}
