/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "menu_layer.h"
#include "menu_layer_private.h"

#include "applib/applib_malloc.auto.h"
#include "applib/preferred_content_size.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/text.h"
#include "util/trig.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/click.h"
#include "applib/ui/window.h"
#include "applib/pbl_std/pbl_std.h"
#include "applib/legacy2/ui/menu_layer_legacy2.h"
#include "kernel/pbl_malloc.h"
#include "process_management/process_manager.h"
#include "shell/system_theme.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"

#include <string.h>

//! @return True if there was an animation to cancel, false otherwise
static bool prv_cancel_selection_animation(MenuLayer *menu_layer);

//////////////////////
// Menu Layer
//
// NOTES: The MenuLayer is built on top of ScrollLayer. It uses ScrollLayer's scrolling and clipping features.
// Since it easily becomes to costly in terms of RAM to hold a layer for each row in the menu in memory,
// the MenuLayer does not use layers for its rows and headers. When a row is about to be displayed,
// it will call out to the client using a callback to get that row drawn.
// Inside the MenuLayer's update_proc (Layer drawing callback), it will call out to its client for each row
// that needs to be drawn, until all visible rows have been drawn.

static void prv_menu_scroll_offset_changed_handler(ScrollLayer *scroll_layer,
                                                   MenuLayer *menu_layer) {
  // TODO: we might need to propagate this event down to MenuLayerCallbacks
}

static void prv_menu_select_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
  // If the selection animation is running, complete it. Note that 2.x apps don't have a selection
  // animation.
  if (menu_layer->animation.animation) {
    animation_set_elapsed(menu_layer->animation.animation,
                          animation_get_duration(menu_layer->animation.animation, true, true));
  }

  // If we're in the middle of scrolling, finish scrolling immediately before handling the select
  // click. We do this to make a transition animation have a consistent position to animate from.
  // Note that animation_set_elapsed isn't supported on 2.x animations. Just skip this step, as
  // no 2.x transitions interact directly with menu layer state.
  if (!process_manager_compiled_with_legacy2_sdk() && menu_layer->scroll_layer.animation) {
    Animation *scroll_layer_animation =
        property_animation_get_animation(menu_layer->scroll_layer.animation);
    animation_set_elapsed(scroll_layer_animation,
                          animation_get_duration(scroll_layer_animation, true, true));
  }

  // Actually handle the click
  if (menu_layer->callbacks.select_click) {
    menu_layer->callbacks.select_click(menu_layer, &menu_layer->selection.index,
                                       menu_layer->callback_context);
  }
}

static void prv_menu_select_long_click_handler(ClickRecognizerRef recognizer,
    MenuLayer *menu_layer) {
  if (menu_layer->callbacks.select_long_click) {
    menu_layer->callbacks.select_long_click(menu_layer, &menu_layer->selection.index,
        menu_layer->callback_context);
  }
}

void menu_up_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
  const bool up = true;
  const bool animated = true;
  menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
}

void menu_down_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
  const bool up = false;
  const bool animated = true;
  menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
}

static void prv_menu_click_config_provider(MenuLayer *menu_layer) {
  // The config that gets passed in, has already the UP and DOWN buttons configured
  // we're overriding the default behavior here:
  window_single_repeating_click_subscribe(BUTTON_ID_UP, 100 /*ms*/,
      (ClickHandler)menu_up_click_handler);
  if (menu_layer->callbacks.select_click) {
    window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler)prv_menu_select_click_handler);
  }
  if (menu_layer->callbacks.select_long_click) {
    window_long_click_subscribe(BUTTON_ID_SELECT, 0,
        (ClickHandler)prv_menu_select_long_click_handler, NULL);
  }
  window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100 /*ms*/,
      (ClickHandler)menu_down_click_handler);
}

static inline uint16_t prv_menu_layer_get_num_sections(MenuLayer *menu_layer) {
  if (menu_layer->callbacks.get_num_sections) {
    return menu_layer->callbacks.get_num_sections(menu_layer, menu_layer->callback_context);
  } else {
    return 1; // default
  }
}

static inline uint16_t prv_menu_layer_get_num_rows(MenuLayer *menu_layer, uint16_t section_index) {
  if (section_index == MENU_INDEX_NOT_FOUND) {
    return 0;
  }

  if (menu_layer->callbacks.get_num_rows) {
    return menu_layer->callbacks.get_num_rows(menu_layer, section_index,
        menu_layer->callback_context);
  } else {
    return 1;  // default
  }
}

static inline int16_t prv_menu_layer_get_separator_height(MenuLayer *menu_layer,
    MenuIndex *cell_index) {
  if (menu_layer->callbacks.get_separator_height) {
    return menu_layer->callbacks.get_separator_height(menu_layer, cell_index, menu_layer->callback_context);
  } else if (process_manager_compiled_with_legacy2_sdk()) {
    return MENU_CELL_LEGACY2_BASIC_SEPARATOR_HEIGHT;
  } else {
    return MENU_CELL_BASIC_SEPARATOR_HEIGHT;
  }
}

static inline int16_t prv_menu_layer_get_header_height(MenuLayer *menu_layer,
    uint16_t section_index) {
  if (menu_layer->callbacks.get_header_height) {
    return menu_layer->callbacks.get_header_height(menu_layer, section_index, menu_layer->callback_context);
  } else {
    return 0; // default
  }
}

static inline int16_t prv_menu_layer_get_cell_height(MenuLayer *menu_layer, MenuIndex
    *cell_index, bool provide_correct_selection_index) {
  if (menu_layer->callbacks.get_cell_height) {
    const MenuIndex prev_selection_index = menu_layer->selection.index;
    if (!provide_correct_selection_index) {
      menu_layer->selection.index.section = MENU_INDEX_NOT_FOUND;
    }
    const int16_t result = menu_layer->callbacks.get_cell_height(menu_layer, cell_index,
                                                                 menu_layer->callback_context);

    menu_layer->selection.index = prev_selection_index;
    return result;
  } else {
    return menu_cell_basic_cell_height();  // default
  }
}

static inline void prv_menu_layer_draw_separator(MenuLayer *menu_layer, Layer *cell_layer,
    MenuCellSpan *cursor, GContext* ctx) {
  const int16_t y = cursor->y - cursor->sep;
  if (menu_layer->callbacks.draw_separator) {
    // Save current drawing state:
    GDrawState prev_state = graphics_context_get_drawing_state(ctx);
    GRect prev_bounds = cell_layer->bounds;
    GRect new_bounds = prev_bounds;

    // Translate the drawing_box to the bounds of the layer:
    ctx->draw_state.drawing_box.origin.y += y;
    ctx->draw_state.drawing_box.size.h = cursor->h;

    // Set the height appropriately on the cell layer
    new_bounds.size.h = cursor->sep;
    layer_set_bounds(cell_layer, &new_bounds);

    // Call the client, to ask to draw the separator:
    menu_layer->callbacks.draw_separator(ctx, cell_layer, &cursor->index, menu_layer->callback_context);

    // Restore current drawing state:
    graphics_context_set_drawing_state(ctx, prev_state);

    // Restore the layer bounds:
    layer_set_bounds(cell_layer, &prev_bounds);
  } else {
    graphics_fill_rect(
        ctx, &GRect(0, y, menu_layer->scroll_layer.layer.bounds.size.w, cursor->sep));
  }
}

static void prv_prepare_row(GContext *ctx, MenuLayer *menu_layer,
                            Layer *cell_layer, bool highlight) {
  if (!process_manager_compiled_with_legacy2_sdk()) {
    GColor *colors = (highlight) ? menu_layer->highlight_colors : menu_layer->normal_colors;
    ctx->draw_state.fill_color = colors[MenuLayerColorBackground];
    ctx->draw_state.text_color = colors[MenuLayerColorForeground];
    ctx->draw_state.tint_color = colors[MenuLayerColorForeground];
    if (!gcolor_is_transparent(ctx->draw_state.fill_color)) {
      graphics_fill_rect(ctx, &cell_layer->bounds);
    }
  }
  cell_layer->is_highlighted = highlight;
}

static void prv_prepare_and_draw_row(GContext *ctx, MenuLayer *menu_layer,
                                     Layer *cell_layer, MenuCellSpan *cursor, bool highlight) {
  prv_prepare_row(ctx, menu_layer, cell_layer, highlight);
  const GRect prev_bounds = cell_layer->bounds;

  // in theory, we could decrement the origin by cell_content_origin_offset_y after the call
  // in practice once shouldn't trust the draw_row implementation
  const int16_t draw_box_origin_y = ctx->draw_state.drawing_box.origin.y;
  ctx->draw_state.drawing_box.origin.y += menu_layer->animation.cell_content_origin_offset_y;

  // Call the client, to ask to draw the row:
  menu_layer->callbacks.draw_row(ctx, cell_layer, &cursor->index, menu_layer->callback_context);

  ctx->draw_state.drawing_box.origin.y = draw_box_origin_y;
  cell_layer->bounds = prev_bounds;
}

static inline void prv_menu_layer_draw_row(MenuLayer *menu_layer, Layer *cell_layer,
    MenuCellSpan *cursor, GContext* ctx) {
  if (cursor->h == 0) {
    // cell has height 0, no need to draw anything.
    return;
  }

  cell_layer->bounds.size.h = cursor->h;
  cell_layer->frame.size.h = cursor->h;
  cell_layer->frame.origin.y = cursor->y;

  // Save current drawing state:
  GDrawState prev_state = graphics_context_get_drawing_state(ctx);

  // Translate the drawing_box to the bounds of the layer:
  ctx->draw_state.drawing_box.origin.y += cursor->y;
  ctx->draw_state.drawing_box.size.h = cursor->h;

  // Use the drawing_box as a clipper to force the content to only use
  // the space available to it and remove overflow
  const GRect *const rect_clipper = (const GRect *const)&ctx->draw_state.drawing_box;
  grect_clip((GRect *const)&ctx->draw_state.clip_box, rect_clipper);

  const bool fully_covered = grect_equal(&cell_layer->frame, &menu_layer->inverter.layer.frame);
  const bool partial = grect_overlaps_grect(&cell_layer->frame, &menu_layer->inverter.layer.frame);

  if (fully_covered || !partial) {
    prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, fully_covered);
  } else {
    // Render the full cell without highlight
    prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, false);

    // Set clipper to the inverter layer in clipping box coordinates
    GRect selection_clipper;
    layer_get_global_frame(&menu_layer->inverter.layer, &selection_clipper);
    grect_clip((GRect *const)&ctx->draw_state.clip_box, &selection_clipper);

    // Render with highlight
    prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, true);
  }

  // Restore current drawing state:
  graphics_context_set_drawing_state(ctx, prev_state);
}

static inline void prv_menu_layer_draw_section_header(MenuLayer *menu_layer, Layer *cell_layer,
    MenuCellSpan *cursor, GContext* ctx) {
  cell_layer->bounds.size.h = cursor->h;
  cell_layer->frame.size.h = cursor->h;
  cell_layer->frame.origin.y = cursor->y;

  // Callback to get the shared cell instance filled with data:

  // Save current drawing state:
  GDrawState prev_state = graphics_context_get_drawing_state(ctx);

  // Translate the drawing_box to the bounds of the layer:
  ctx->draw_state.drawing_box.origin.y += cursor->y;
  ctx->draw_state.drawing_box.size.h = cursor->h;

  const GRect *const rect_clipper = (const GRect *const)&ctx->draw_state.drawing_box;
  grect_clip((GRect *const)&ctx->draw_state.clip_box, rect_clipper);

  prv_prepare_row(ctx, menu_layer, cell_layer, false);

  // Call the client, to ask to draw the section:
  menu_layer->callbacks.draw_header(ctx, cell_layer, cursor->index.section, menu_layer->callback_context);

  // Restore current drawing state:
  graphics_context_set_drawing_state(ctx, prev_state);
}

static void prv_menu_layer_render_section_from_iterator(MenuIterator *iterator) {
  MenuRenderIterator *it = (MenuRenderIterator*)iterator;
  const int16_t top_diff = it->it.cursor.y - it->content_top_y;
  const bool is_header_in_frame = (top_diff >= 0 && it->it.cursor.y <= it->content_bottom_y) ||
      (it->it.cell_bottom_y >= it->content_top_y && it->it.cell_bottom_y <= it->content_bottom_y);
  if (is_header_in_frame) {
    // Draw section header:
    prv_menu_layer_draw_section_header(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
    // Draw the separator on top of the cell:
    if (top_diff >= it->it.cursor.sep) {
      prv_menu_layer_draw_separator(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
    }
  }
}

static void prv_menu_layer_render_row_from_iterator(MenuIterator *iterator) {
  MenuRenderIterator *it = (MenuRenderIterator*)iterator;
  const int16_t iter_y = it->it.cursor.y;

  const int16_t top_diff = it->it.cursor.y - it->content_top_y;
  const bool is_row_in_frame = (top_diff >= 0 && it->it.cursor.y <= it->content_bottom_y) ||
      (it->it.cell_bottom_y >= it->content_top_y && it->it.cell_bottom_y <= it->content_bottom_y);
  if (is_row_in_frame) {
    it->cursor_in_frame = true;
    // Draw the cell
    prv_menu_layer_draw_row(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
    // Draw the separator on top of the cell
    if (top_diff >= it->it.cursor.sep) {
      prv_menu_layer_draw_separator(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
    }
    // Update the cache with the center-most row
    it->it.cursor.y = iter_y;
    if (false == it->cache_set) {
      it->new_cache = it->it.cursor;
      it->cache_set = true;
    }
  } else {
    if (it->cursor_in_frame) {
      it->it.should_continue = false;
    }
  }
  it->it.cursor.y = iter_y;
}

// NOTE: The following two iteration functions are asymmetrical!
// In other words, even one is going downward and the other upward, there are some subtle
// differences. Most importantly: the downward function calls the row_callback_after_geometry for
// the row the iterator's cursor is currently set to, while the upward function skips over the
// current row.
// Secondly, section_callback is only called when a sections is encountered while walking.
// For example, if the current index is (section: 0, row: 0), the section_callback for section 0
// will only be called when walking upward.

static void prv_menu_layer_walk_downward_from_iterator(MenuIterator *it) {
  const uint16_t num_sections = prv_menu_layer_get_num_sections(it->menu_layer);
  it->should_continue = true;
  for (;;) { // sections
    const uint16_t num_rows_in_section = prv_menu_layer_get_num_rows(it->menu_layer,
        it->cursor.index.section);
    for (;;) { // rows
      if (it->cursor.index.row >= num_rows_in_section) {
        // Reached last row
        break;
      }

      if (it->row_callback_before_geometry) {
        it->row_callback_before_geometry(it);
      }

      it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, true);
      it->cell_bottom_y = it->cursor.y + it->cursor.h;

      // ROW
      if (it->row_callback_after_geometry) {
        it->row_callback_after_geometry(it);
      }
      if (it->should_continue == false) {
        return;
      }

      // Next row:
      it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
      it->cursor.y = it->cell_bottom_y; // Bottom of previous cell is y of the next cell

      // Don't leave space for the seperator for the (non-existent) row after the last row.
      // This doesn't impact cell drawing in this loop (this condition will only trip on the last run).
      // But, other parts of the system rely on the cursor being set properly at the end of this iteration.
      if (it->cursor.index.row < num_rows_in_section - 1 || it->cursor.index.section < num_sections - 1) {
        it->cursor.y += it->cursor.sep;
      }
      ++(it->cursor.index.row);
    } // for() rows

    // Next section:
    ++(it->cursor.index.section);
    if (it->cursor.index.section >= num_sections) {
      break;
      // Reached last section
    }
    it->cursor.index.row = 0;
    it->cursor.h = prv_menu_layer_get_header_height(it->menu_layer, it->cursor.index.section);
    it->cell_bottom_y = it->cursor.y + it->cursor.h;

    // SECTION
    if (it->cursor.h > 0) {
      it->section_callback(it);
      it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
      it->cursor.y = it->cell_bottom_y + it->cursor.sep;
    }

    if (it->should_continue == false) {
      return;
    }

  } // for() sections
}

static void prv_menu_layer_walk_upward_from_iterator(MenuIterator *it) {
  it->should_continue = true;
  for (;;) { // sections
    for (;;) { // rows
      // Previous row
      if (it->cursor.index.row == 0) {
        // Reached top-most row in current section
        break;
      }
      --(it->cursor.index.row);

      if (it->row_callback_before_geometry) {
        it->row_callback_before_geometry(it);
      }

      // when walking upwards, selected_index isn't set yet here
      // hence, the heights are the sizes as they were before the selection changed
      it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, false);
      it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
      it->cursor.y -= it->cursor.h + it->cursor.sep;
      it->cell_bottom_y = it->cursor.y + it->cursor.h;

      // ask for height again, this time with correct selection status
      it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, true);

      // ROW
      if (it->row_callback_after_geometry) {
        it->row_callback_after_geometry(it);
      }

      if (it->should_continue == false) {
        break;
      }
    } // for() rows

    if (it->cursor.index.row == 0) {
      // If top-most row, layout the section header
      it->cursor.h = prv_menu_layer_get_header_height(it->menu_layer, it->cursor.index.section);
      it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);

      if (it->cursor.h > 0) {
        // Bottom of previous cell is y of the next cell
        const int16_t total_height = it->cursor.h + it->cursor.sep;
        if (total_height > it->cursor.y) {
          // If the total height is greater than the cursor y, don't
          // add in space to accodomate the separator as the downwards callback
          // will add it for us.
          it->cursor.y -= it->cursor.h;
        } else {
          it->cursor.y -= total_height;
        }
        it->cell_bottom_y = it->cursor.y + it->cursor.h;

        // SECTION
        it->section_callback(it);
      }
    }

    if (it->should_continue == false) {
      return;
    }

    // Previous section:
    if (it->cursor.index.section == 0) {
      // Reached top
      break;
    }
    --(it->cursor.index.section);
    // -1 will happen when entering for() rows
    it->cursor.index.row = it->menu_layer->callbacks.get_num_rows(it->menu_layer,
        it->cursor.index.section, it->menu_layer->callback_context);

  } // for() sections
}

static void NOINLINE prv_draw_background(MenuLayer *menu_layer, GContext *ctx,
                                Layer *bg_layer, bool highlight) {
  GDrawState prev_state = graphics_context_get_drawing_state(ctx);

  const GRect *bounds = &bg_layer->bounds;
  ctx->draw_state.drawing_box.origin.y = bounds->origin.y;
  ctx->draw_state.drawing_box.size.h = bounds->size.h;

  MenuLayerDrawBackgroundCallback draw_background_cb = menu_layer->callbacks.draw_background;
  if (draw_background_cb) {
    draw_background_cb(ctx, bg_layer, false, menu_layer->callback_context);
  } else if (highlight) {
    ctx->draw_state.fill_color = menu_layer->highlight_colors[MenuLayerColorBackground];
    graphics_fill_rect(ctx, bounds);
  } else {
    ctx->draw_state.fill_color = menu_layer->normal_colors[MenuLayerColorBackground];
    graphics_fill_rect(ctx, bounds);
  }

  graphics_context_set_drawing_state(ctx, prev_state);
}

void menu_layer_update_proc(Layer *scroll_content_layer, GContext* ctx) {
  MenuLayer *menu_layer = (MenuLayer*)(((uint8_t*)scroll_content_layer) -
      offsetof(MenuLayer, scroll_layer.content_sublayer));
  const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
  const int16_t content_top_y = -scroll_layer_get_content_offset(&menu_layer->scroll_layer).y;
  const int16_t content_bottom_y = content_top_y + frame_size.h;

  if (!process_manager_compiled_with_legacy2_sdk()) {
    prv_draw_background(menu_layer, ctx, &menu_layer->scroll_layer.layer, false);
  }

  MenuRenderIterator *render_iter = applib_type_malloc(MenuRenderIterator);
  PBL_ASSERTN(render_iter);

  if (menu_layer->center_focused) {
    // in this mode, the selected row is always the best candidate for the cache
    menu_layer->cache.cursor = menu_layer->selection;
  }

  *render_iter = (MenuRenderIterator) {
    .it = {
      .menu_layer = menu_layer,
      .cursor = menu_layer->cache.cursor,
      .row_callback_after_geometry = prv_menu_layer_render_row_from_iterator,
      .section_callback = prv_menu_layer_render_section_from_iterator,
    },
    .ctx = ctx,
    .content_top_y = content_top_y,
    .content_bottom_y = content_bottom_y,
    .cache_set = false,
    .cursor_in_frame = false,
    .cell_layer = {
      .bounds = {
        .size = {
          .w = frame_size.w,
        },
      },
      .frame = {
        .size = {
          .w = frame_size.w,
        },
      },
    },
  };
  layer_add_child(&menu_layer->scroll_layer.content_sublayer, &render_iter->cell_layer);

  // Set separator color
  graphics_context_set_fill_color(ctx, GColorBlack);

  // We're caching the y-coord and index of the one row, as our "anchor" point in the menu.
  // We'll be walking downward and upward from that index until the rows fall off the screen.
  const int16_t content_center_y = (content_top_y + content_bottom_y) / 2;
  if (content_center_y >= menu_layer->cache.cursor.y) {
    // Walk downward from cache.cursor, then upward
    prv_menu_layer_walk_downward_from_iterator(&render_iter->it);
    render_iter->it.cursor = menu_layer->cache.cursor;
    prv_menu_layer_walk_upward_from_iterator(&render_iter->it);
  } else {
    // Walk upward from cache.cursor, then downward
    prv_menu_layer_walk_upward_from_iterator(&render_iter->it);
    render_iter->it.cursor = menu_layer->cache.cursor;
    prv_menu_layer_walk_downward_from_iterator(&render_iter->it);
  }
  layer_remove_from_parent(&render_iter->cell_layer);

  // Assign the new cache:
  menu_layer->cache.cursor = render_iter->new_cache;

  task_free(render_iter);
}

void menu_layer_init_scroll_layer_callbacks(MenuLayer *menu_layer) {
  ScrollLayer *scroll_layer = &menu_layer->scroll_layer;
  scroll_layer_set_callbacks(scroll_layer, (ScrollLayerCallbacks) {
    .click_config_provider = (ClickConfigProvider)prv_menu_click_config_provider,
    .content_offset_changed_handler = (ScrollLayerCallback)prv_menu_scroll_offset_changed_handler,
  });
  scroll_layer->content_sublayer.update_proc = (LayerUpdateProc)menu_layer_update_proc;
}

static void prv_set_center_focused(MenuLayer *menu_layer, bool center_focused) {
  menu_layer->center_focused = center_focused;
  scroll_layer_set_clips_content_offset(&menu_layer->scroll_layer, !center_focused);
}

void menu_layer_init(MenuLayer *menu_layer, const GRect *frame) {
  *menu_layer = (MenuLayer) {
    .pad_bottom = true,
  };

  ScrollLayer *scroll_layer = &menu_layer->scroll_layer;
  scroll_layer_init(scroll_layer, frame);
  menu_layer_init_scroll_layer_callbacks(menu_layer);
  scroll_layer_set_shadow_hidden(scroll_layer, true);
  scroll_layer_set_context(scroll_layer, menu_layer);

  menu_layer_set_normal_colors(menu_layer, GColorWhite, GColorBlack);
  menu_layer_set_highlight_colors(menu_layer, GColorBlack, GColorWhite);

  InverterLayer *inverter = &menu_layer->inverter;
  inverter_layer_init(inverter, &GRectZero);
  scroll_layer_add_child(scroll_layer, &inverter->layer);

  // Hide inverter layer by default for 3.0 apps
  layer_set_hidden(inverter_layer_get_layer(&menu_layer->inverter), true);

#if PBL_ROUND
  prv_set_center_focused(menu_layer, true);
#endif
}

MenuLayer* menu_layer_create(GRect frame) {
  MenuLayer *layer = applib_type_malloc(MenuLayer);
  if (layer) {
    menu_layer_init(layer, &frame);
  }
  return layer;
}

void menu_layer_pad_bottom_enable(MenuLayer *menu_layer, bool enable) {
  menu_layer->pad_bottom = enable;
}

void menu_layer_deinit(MenuLayer *menu_layer) {
  prv_cancel_selection_animation(menu_layer);
  layer_deinit(&menu_layer->inverter.layer);
  scroll_layer_deinit(&menu_layer->scroll_layer);
}

void menu_layer_destroy(MenuLayer* menu_layer) {
  if (menu_layer == NULL) {
    return;
  }
  menu_layer_deinit(menu_layer);
  applib_free(menu_layer);
}

Layer* menu_layer_get_layer(const MenuLayer *menu_layer) {
  return &((MenuLayer *)menu_layer)->scroll_layer.layer;
}

ScrollLayer* menu_layer_get_scroll_layer(const MenuLayer *menu_layer) {
  return &((MenuLayer *)menu_layer)->scroll_layer;
}

typedef struct MenuPrimeCacheIterator {
  MenuIterator it;
  bool cache_set;
} MenuPrimeCacheIterator;

static void prv_menu_layer_iterator_noop_callback(MenuIterator *it) {
  (void)it;
}

static void prv_menu_layer_iterator_prime_cache_callback(MenuIterator *iterator) {
  MenuPrimeCacheIterator *it = (MenuPrimeCacheIterator*)iterator;
  if (false == it->cache_set) {
    // Prime the cursor cache:
    it->it.menu_layer->cache.cursor = it->it.cursor;
    // Set initial selection too:
    it->it.menu_layer->selection = it->it.cursor;
    it->cache_set = true;
  }
}

//! Calculate the total height of all row cells and section headers,
//! and assign the appropriate content size to the scroll_layer.
//! Also prime the offset cache on the fly.
void menu_layer_update_caches(MenuLayer *menu_layer) {
  // Save the currently selected cell index.
  MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
  MenuPrimeCacheIterator it = {
    .it = {
      .menu_layer = menu_layer,
      .row_callback_after_geometry = prv_menu_layer_iterator_prime_cache_callback,
      .section_callback = prv_menu_layer_iterator_noop_callback,
      .should_continue = true,
      .cursor = {
        // Section header of current section (0) is not part of the walk down, set it "manually"
        .y = prv_menu_layer_get_header_height(menu_layer, 0),
        .sep = prv_menu_layer_get_separator_height(menu_layer, 0)
      },
    },
    .cache_set = false,
  };

  if (prv_menu_layer_get_header_height(menu_layer, 0) != 0) {
    // We have to add the separator height, as when drawing down -> up, we render the separator
    // for the row above before proceeding down. We only render this separator at the top if we
    // have headers on the first section.
    it.it.cursor.y += it.it.cursor.sep;
  }

  // handle special case of just one row so that calls for menu_layer_get_selected_index()
  // will already answer correctly
  if (prv_menu_layer_get_num_sections(menu_layer) == 1 &&
      prv_menu_layer_get_num_rows(menu_layer, 0) == 1) {
    menu_layer->selection.index = MenuIndex(0, 0);
  }

  prv_menu_layer_walk_downward_from_iterator(&it.it);
  int16_t total_height = it.it.cursor.y;
  if (menu_layer->pad_bottom) {
    total_height += MENU_LAYER_BOTTOM_PADDING;
  }

  // Set the content size on the scroll layer, so all the rows will fit onto the content layer:
  const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
  scroll_layer_set_content_size(&menu_layer->scroll_layer, GSize(frame_size.w, total_height));

  // Set the selected cell again:
  const bool animated = false;
  menu_layer_set_selected_index(menu_layer, selected_index, MenuRowAlignNone, animated);
}

void menu_layer_set_callbacks(MenuLayer *menu_layer, void *callback_context,
                            const MenuLayerCallbacks *callbacks) {
  if (callbacks) {
    menu_layer->callbacks = *callbacks;
    PBL_ASSERTN(menu_layer->callbacks.draw_row);
    PBL_ASSERTN(menu_layer->callbacks.get_num_rows);
  }

  menu_layer->callback_context = callback_context;

  menu_layer_reload_data(menu_layer);
}

void menu_layer_set_callbacks_by_value(MenuLayer *menu_layer, void *callback_context,
                                       MenuLayerCallbacks callbacks) {
  menu_layer_set_callbacks(menu_layer, callback_context, &callbacks);
}

void menu_layer_set_click_config_onto_window(MenuLayer *menu_layer, struct Window *window) {
  // Delegate this directly to the scroll layer:
  scroll_layer_set_click_config_onto_window(&menu_layer->scroll_layer, window);
}

//! @returns 0 if A and B are equal, 1 if A has a higher section & row combination than B or else -1
int16_t menu_index_compare(const MenuIndex *a, const MenuIndex *b) {
  const int16_t max_rows = MAX(a->row, b->row) + 1;
  const int32_t a_abs = ((a->section * max_rows) + a->row);
  const int32_t b_abs = ((b->section * max_rows) + b->row);
  if (a_abs > b_abs) {
    return 1;
  } else if (a_abs < b_abs) {
    return -1;
  } else {
    return 0;
  }
}

static void prv_selection_complete(Animation *animation, bool finished, void *context) {
  MenuLayer *menu_layer = (MenuLayer *) context;
  menu_layer->animation.animation = NULL;
}

static bool prv_cancel_selection_animation(MenuLayer *menu_layer) {
  const bool result = animation_is_scheduled(menu_layer->animation.animation);
  if (result) {
    animation_unschedule(menu_layer->animation.animation);
  }
  menu_layer->animation.animation = NULL;
  return result;
}

#define TOP_DOWN_PX  7
#define BOTTOM_DOWN_PX 10
static void prv_setup_selection_animation(MenuLayer *menu_layer, bool up) {
  // Move selection inverter layer:
  const int16_t w = menu_layer->scroll_layer.layer.frame.size.w;
  const GSize size = GSize(w, menu_layer->selection.h);

  // Step 1. Bring down TOP of cell by TOP_DOWN_PX.
  GRect from;
  if (menu_layer->animation.animation) {
    from = menu_layer->animation.target;
    prv_cancel_selection_animation(menu_layer);
  } else {
    from = menu_layer->inverter.layer.frame;
  }
  GRect target = (GRect) {
    .origin = {
      .x = 0,
      .y = from.origin.y + ((up) ? 0 : TOP_DOWN_PX),
    },
    .size = {
      .w = size.w,
      .h = size.h - TOP_DOWN_PX,
    }
  };

  Animation *a1 = (Animation *) property_animation_create_layer_frame(&menu_layer->inverter.layer,
                                                                      &from, &target);
  animation_set_duration(a1, 100);
  animation_set_curve(a1, AnimationCurveEaseOut);
  animation_set_auto_destroy(a1, true);

  // Step 2. Skip the top of the highlight down to the top of the newly selected cell,
  // and have the selection BOTTOM_DOWN_PX below the selected cell.
  from.origin.y = menu_layer->selection.y - ((up) ? BOTTOM_DOWN_PX : 0);
  from.size.h = size.h + BOTTOM_DOWN_PX;

  // Step 3. Bring up the bottom of the highlight to only cover the selected cell.
  target.origin.y = menu_layer->selection.y;
  target.size = size;

  Animation *a2 = (Animation *) property_animation_create_layer_frame(&menu_layer->inverter.layer,
                                                                      &from, &target);
  animation_set_duration(a2, 250);
  animation_set_curve(a2, AnimationCurveEaseOut);
  animation_set_auto_destroy(a2, true);

  Animation *a = animation_sequence_create(a1, a2, NULL);

  animation_set_auto_destroy(a, true); // [MJ] false?
  animation_set_handlers(a, (AnimationHandlers) { .stopped = prv_selection_complete }, menu_layer);

  menu_layer->animation.animation = a;
  menu_layer->animation.target = target;
  animation_schedule(a);
}


static void prv_menu_layer_update_selection_highlight(MenuLayer *menu_layer, bool up,
                                                      bool animated,
                                                      bool change_ongoing_animation) {
  if (menu_layer->center_focused || menu_layer->selection_animation_disabled) {
    // animation on center_focused will not happen by moving the selection
    // see prv_schedule_center_focus_animation()
    animated = false;
  }

  Animation *scroll_animation = (Animation *) menu_layer->scroll_layer.animation;
  if (change_ongoing_animation && animation_is_scheduled(scroll_animation)) {
    animation_unschedule(scroll_animation);
  }
  if (change_ongoing_animation && animated && !process_manager_compiled_with_legacy2_sdk()) {
    prv_setup_selection_animation(menu_layer, up);
  } else {
    if (change_ongoing_animation) {
      prv_cancel_selection_animation(menu_layer);
    }
    // Move selection inverter layer:
    const int16_t w = menu_layer->scroll_layer.layer.frame.size.w;
    const GSize size = GSize(w, menu_layer->selection.h);
    menu_layer->inverter.layer.bounds = (GRect) {
      .origin = { 0, 0 },
      .size = size,
    };
    menu_layer->inverter.layer.frame = (GRect) {
      .origin = {
        .x = 0,
        .y = menu_layer->selection.y,
      },
      .size = size,
    };
    layer_mark_dirty(&menu_layer->inverter.layer);
  }
}

static MenuRowAlign prv_corrected_scroll_align(MenuLayer *menu_layer, MenuRowAlign align) {
  if (menu_layer->center_focused) {
    return MenuRowAlignCenter;
  }
  return align;
}

static void prv_menu_layer_update_selection_scroll_position(MenuLayer *menu_layer,
                                                            MenuRowAlign scroll_align,
                                                            bool animated) {
  scroll_align = prv_corrected_scroll_align(menu_layer, scroll_align);

  if (scroll_align != MenuRowAlignNone) {
    int16_t y;
    const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
    // Scroll to the right position:
    switch (scroll_align) {
      case MenuRowAlignTop:
        y = - menu_layer->selection.y;
        break;

      case MenuRowAlignBottom:
        y = frame_size.h - menu_layer->selection.y - menu_layer->selection.h;
        break;

      default:
      case MenuRowAlignCenter:
        y = (frame_size.h / 2) - menu_layer->selection.y - (menu_layer->selection.h / 2);
        break;
    }

    if (menu_layer->center_focused) {
      // animation on center_focus will not happen via scrolling
      // see prv_schedule_center_focus_animation()
      animated = false;
    }
    // scroll layer will take care of clipping if necessary
    scroll_layer_set_content_offset(&menu_layer->scroll_layer, GPoint(0, y), animated);
  }
}

typedef struct MenuSelectIndexIterator {
  MenuIterator it;
  MenuCellSpan selection;
  bool did_change_selection:1;
} MenuSelectIndexIterator;

static void prv_menu_layer_iterator_selection_index_callback(MenuIterator *iterator) {
  MenuSelectIndexIterator *it = (MenuSelectIndexIterator*)iterator;
  if (!menu_index_compare(&it->it.cursor.index, &it->selection.index)) {
    it->it.menu_layer->selection = it->it.cursor;
    it->it.should_continue = false;
    it->did_change_selection = true;
  }
}

static void prv_menu_layer_iterator_update_selection(MenuIterator *iterator) {
  MenuLayer *menu_layer = iterator->menu_layer;
  if (menu_index_compare(&iterator->cursor.index, &menu_layer->selection.index) == 0) {
    menu_layer->selection = iterator->cursor;
  }
}

static void prv_walk_with_iterator(const int8_t direction, MenuIterator *it) {
  MenuLayer *menu_layer = it->menu_layer;
  const int16_t prev_selection_height = menu_layer->selection.h;
  const MenuIndex prev_selection_index = menu_layer->selection.index;

  if (menu_layer->center_focused) {
    it->row_callback_before_geometry = it->row_callback_after_geometry;
    it->row_callback_after_geometry = prv_menu_layer_iterator_update_selection;

    // invalidate current selection while iterating
    menu_layer->selection.index.section = MENU_INDEX_NOT_FOUND;
  }

  if (direction < 0) {
    // new index comes before current selection
    prv_menu_layer_walk_upward_from_iterator(it);
  } else if (direction > 0) {
    // new index comes after current selection
    prv_menu_layer_walk_downward_from_iterator(it);
  }

  // potentially restore previous state of selection
  if (menu_layer->selection.index.section == MENU_INDEX_NOT_FOUND) {
    menu_layer->selection.index = prev_selection_index;
    menu_layer->selection.h = prev_selection_height;
  }
}

typedef struct {
  MenuLayer *menu_layer;
  bool up;
} CenterFocusSelectionAnimationState;

static CenterFocusSelectionAnimationState prv_center_focus_animation_state(Animation *animation) {
  PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
  CenterFocusSelectionAnimationState result = {};
  property_animation_get_subject(prop_anim, (void **)&result.menu_layer);
  property_animation_to(prop_anim, &result.up, sizeof(result.up), false);
  return result;
}

static void prv_center_focus_animation_setup(Animation *animation) {
  CenterFocusSelectionAnimationState state = prv_center_focus_animation_state(animation);
  state.menu_layer->animation.cell_content_origin_offset_y = 0;
  state.menu_layer->animation.selection_extend_top = 0;
  state.menu_layer->animation.selection_extend_bottom = 0;
}

static void prv_announce_selection_changed(MenuLayer *menu_layer, MenuIndex prev_index) {
  if (!menu_layer->callbacks.selection_changed) {
    return;
  }

  menu_layer->callbacks.selection_changed(menu_layer, menu_layer->selection.index,
                                          prev_index, menu_layer->callback_context);
}

void prv_center_focus_animation_update_impl(Animation *animation,
                                            bool second_half,
                                            AnimationProgress adjusted_progress) {
  CenterFocusSelectionAnimationState state = prv_center_focus_animation_state(animation);

  // values as seen in the design videos
  const int16_t move_in_dist = 16;
  const int16_t move_out_dist = 4;
  const int16_t abs_content_offset = second_half ?
                                     interpolate_int16(adjusted_progress, move_out_dist, 0) :
                                     interpolate_int16(adjusted_progress, 0, move_in_dist);
  const int16_t content_offset = (state.up ? abs_content_offset : -abs_content_offset) / 2;
  state.menu_layer->animation.cell_content_origin_offset_y = content_offset;

  const bool reached_second_half_before = menu_index_compare(
      &state.menu_layer->selection.index, &state.menu_layer->animation.new_selection.index) == 0;

  if (second_half) {
    if (!reached_second_half_before) {
      const MenuIndex prev_index = state.menu_layer->selection.index;
      state.menu_layer->selection = state.menu_layer->animation.new_selection;
      prv_announce_selection_changed(state.menu_layer, prev_index);
    }
    // this favors robustness over efficiency - the functions might be called multiple times
    // but instead of keeping track (which is more difficult that it seems)
    // we simply call them too often
    prv_menu_layer_update_selection_scroll_position(state.menu_layer, MenuRowAlignCenter, false);
    prv_menu_layer_update_selection_highlight(state.menu_layer, state.up, false, false);
    state.menu_layer->inverter.layer.frame.size.h += abs_content_offset;
    state.menu_layer->inverter.layer.bounds.size = state.menu_layer->inverter.layer.frame.size;

    // when scrolling up, bounce back at the top (otherwise at the bottom)
    if (!state.up) {
      state.menu_layer->inverter.layer.frame.origin.y -= abs_content_offset;
    }
  }
  layer_mark_dirty(&state.menu_layer->scroll_layer.layer);
}

void prv_center_focus_animation_update_in_and_out(Animation *animation,
                                                  const AnimationProgress progress) {
  const AnimationProgress half_progress = ANIMATION_NORMALIZED_MAX / 2;
  const bool second_half = progress >= half_progress;
  const AnimationProgress adjusted_progress = second_half ?
      animation_timing_scaled(progress, half_progress, ANIMATION_NORMALIZED_MAX) :
      animation_timing_scaled(progress, 0, half_progress);
  prv_center_focus_animation_update_impl(animation, second_half, adjusted_progress);
}

void prv_center_focus_animation_update_out_only(Animation *animation,
                                                const AnimationProgress progress) {
  // anwalys only render the bounce back
  prv_center_focus_animation_update_impl(animation, true, progress);
}

static void prv_center_focus_animation_teardown(Animation *animation) {
  // usually a "redundant" call. Just in case the animation gets cancelled before finish
  prv_center_focus_animation_update_in_and_out(animation, ANIMATION_NORMALIZED_MAX);
}

static void prv_schedule_center_focus_animation(MenuLayer *menu_layer, bool up,
                                                const MenuCellSpan *prev_selection,
                                                bool was_animating) {
  // we reconfigure the current index to be the previous index so that all parties in the ongoing
  // animation will continue to reply with the proper values with respect to the selection
  // half-way through the animation we then switch (back) to the new index
  menu_layer->animation.new_selection = menu_layer->selection;
  menu_layer->selection = *prev_selection;

  // force selection + scrolling to be at the right spot, not animated since the actual animation
  // for center_focused is done via rendering offset below
  const bool selection_animated = false;
  prv_menu_layer_update_selection_highlight(menu_layer, up, selection_animated,
                                            true /* change_ongoing_animation */);
  prv_menu_layer_update_selection_scroll_position(menu_layer, MenuRowAlignNone, selection_animated);

  static const PropertyAnimationImplementation s_center_focus_selection_animation_in_out_impl = {
    .base = {
      .setup = prv_center_focus_animation_setup,
      .update = prv_center_focus_animation_update_in_and_out,
      .teardown = prv_center_focus_animation_teardown,
    }
  };
  static const PropertyAnimationImplementation s_center_focus_selection_animation_out_only_impl = {
    .base = {
      .setup = prv_center_focus_animation_setup,
      .update = prv_center_focus_animation_update_out_only,
      .teardown = prv_center_focus_animation_teardown,
    }
  };
  // when we were animating already, use the implementation that's only showing the bounce back
  const PropertyAnimationImplementation *impl = was_animating ?
                                                &s_center_focus_selection_animation_out_only_impl :
                                                &s_center_focus_selection_animation_in_out_impl;
  PropertyAnimation *const prop_anim = property_animation_create(impl, menu_layer, NULL, NULL);
  // we're (ab)using the .to value to store the direction, see prv_center_focus_animation_state()
  property_animation_to(prop_anim, &up, sizeof(up), true);
  Animation *const anim = property_animation_get_animation(prop_anim);
  menu_layer->animation.animation = anim;

  // number of frames measured in the video
  const uint32_t full_duration_ms = ANIMATION_TARGET_FRAME_INTERVAL_MS * 7;
  uint32_t duration = full_duration_ms;
  if (was_animating) {
    // only show second half of animation if uses presses repetitive
    // as it's only the bounce back, then
    duration /= 2;
    animation_set_delay(anim, duration);
  }
  animation_set_duration(anim, duration);
  animation_set_curve(anim, AnimationCurveEaseInOut);
  animation_schedule(anim);

  if (was_animating) {
    // create visual state that's already reflecting the beginning of the "out" animation
    prv_center_focus_animation_update_out_only(anim, 0);
  }
}

static void prv_apply_selection_change(MenuLayer *menu_layer, MenuRowAlign scroll_align, bool up,
                                       bool did_change, const MenuCellSpan *prev_selection,
                                       bool was_animating, bool animated) {
  if (menu_layer->center_focused && animated) {
    prv_schedule_center_focus_animation(menu_layer, up, prev_selection, was_animating);
  } else {
    prv_menu_layer_update_selection_highlight(menu_layer, up, animated, true);
    prv_menu_layer_update_selection_scroll_position(menu_layer, scroll_align, animated);

    // only call this here, on animated center focus, the announcement will happen in-between
    // as we change the selection index for real
    if (did_change) {
      prv_announce_selection_changed(menu_layer, prev_selection->index);
    }
  }
}

typedef struct {
  bool was_animating;
  MenuCellSpan prev_selection;
} MenuLayerBeforeSelectionChangeState;

static MenuLayerBeforeSelectionChangeState prv_capture_state_and_cancel_center_focus_animation(
    MenuLayer *menu_layer) {
  // it's critical to cancel the animation for center focus here so that any potential in-between
  // selection state will be cleaned up
  const bool was_animating = menu_layer->center_focused ?
                             prv_cancel_selection_animation(menu_layer) :
                             false;
  return (MenuLayerBeforeSelectionChangeState) {
    .was_animating = was_animating,
    .prev_selection = menu_layer->selection,
  };
}

void menu_layer_set_selected_index(MenuLayer *menu_layer, MenuIndex index, MenuRowAlign scroll_align, bool animated) {
  const MenuLayerBeforeSelectionChangeState before_state =
      prv_capture_state_and_cancel_center_focus_animation(menu_layer);

  // Keep the selection within a valid range
  const uint16_t num_sections = prv_menu_layer_get_num_sections(menu_layer);
  if (index.section >= num_sections) {
    index.section = num_sections - 1;
  }
  // check to make sure this callback has been set, return early if not
  if (menu_layer->callbacks.get_num_rows == NULL) {
    PBL_LOG(LOG_LEVEL_ERROR, "Please set menu layer callbacks before running menu_layer_set_selected_index.");
    return;
  }

  const uint16_t num_rows = menu_layer->callbacks.get_num_rows(menu_layer, index.section, menu_layer->callback_context);
  if (index.row >= num_rows) {
    index.row = num_rows - 1;
  }

  // when called from iteration triggered by menu_layer_set_selected_next() the
  // selection.index.section could be MENU_INDEX_NOT_FOUND (a very large value)
  // in this case, walk forward from {0, 0| to avoid a very long loop run
  const bool is_invalid_section = menu_layer->selection.index.section == MENU_INDEX_NOT_FOUND;
  const int16_t comp = is_invalid_section ? 1 :
                       menu_index_compare(&index, &menu_layer->selection.index);
  MenuSelectIndexIterator it = {
    .it = {
      .menu_layer = menu_layer,
      .row_callback_after_geometry = prv_menu_layer_iterator_selection_index_callback,
      .section_callback = prv_menu_layer_iterator_noop_callback,
      .should_continue = true,
      .cursor = is_invalid_section ? (MenuCellSpan){} : menu_layer->selection,
    },
    .selection = {
      .index = index,
    },
    .did_change_selection = false,
  };

  prv_walk_with_iterator((int8_t)comp, &it.it);

  const bool up = (comp == -1);
  prv_apply_selection_change(menu_layer, scroll_align, up, it.did_change_selection,
                             &before_state.prev_selection, before_state.was_animating, animated);
}

typedef struct MenuSelectNextIterator {
  MenuIterator it;
  uint8_t count;
  bool did_change_selection:1;
} MenuSelectNextIterator;

static void prv_menu_layer_iterator_selection_next_callback(MenuIterator *iterator) {
  MenuSelectNextIterator *it = (MenuSelectNextIterator*)iterator;
  MenuLayer *menu_layer = it->it.menu_layer;
  if (it->count == 1) {
    MenuLayerSelectionWillChangeCallback cb = menu_layer->callbacks.selection_will_change;
    it->it.should_continue = false;
    it->did_change_selection = true;
    if (cb) {
      MenuIndex new_index = it->it.cursor.index;
      cb(menu_layer, &new_index, menu_layer->selection.index, menu_layer->callback_context);
      if (menu_index_compare(&new_index, &menu_layer->selection.index) == 0) {
        // locked into old index
      } else if (menu_index_compare(&new_index, &it->it.cursor.index) == 0) {
        // new index is the index we wanted to select
        menu_layer->selection = it->it.cursor;
      } else {
        // when center focused, animation will be scheduled at the very end
        // see prv_apply_selection_change()
        const bool animated = !menu_layer->center_focused;
        // Specified an alternate index
        // This is safe since menu_layer_set_selected_index will not trigger the
        // SelectionWillChangeCallback again.
        menu_layer_set_selected_index(menu_layer, new_index, MenuRowAlignNone, animated);
        it->did_change_selection = false;
      }
    } else {
      menu_layer->selection = it->it.cursor;
    }
  } else {
    ++it->count;
  }
}

void menu_layer_set_selected_next(MenuLayer *menu_layer, bool up,
                                  MenuRowAlign scroll_align, bool animated) {
  const MenuLayerBeforeSelectionChangeState before_state =
      prv_capture_state_and_cancel_center_focus_animation(menu_layer);

  MenuSelectNextIterator it = {
    .it = {
      .menu_layer = menu_layer,
      .row_callback_after_geometry = prv_menu_layer_iterator_selection_next_callback,
      .section_callback = prv_menu_layer_iterator_noop_callback,
      .should_continue = true,
      .cursor = menu_layer->selection,
    },
    .count = up ? 1 : 0, // see asymmetry note with menu_layer_walk_downward_from_iterator()
    .did_change_selection = false,
  };

  prv_walk_with_iterator((int8_t)(up ? -1 : 1), &it.it);

  prv_apply_selection_change(menu_layer, scroll_align, up, it.did_change_selection,
                             &before_state.prev_selection, before_state.was_animating, animated);
}

MenuIndex menu_layer_get_selected_index(const MenuLayer *menu_layer) {
  return menu_layer->selection.index;
}

bool menu_layer_is_index_selected(const MenuLayer *menu_layer, MenuIndex *index) {
  MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
  return menu_index_compare(&selected_index, index) == 0;
}

//! indicates that the data behind the menu has changed and needs a re-draw
void menu_layer_reload_data(MenuLayer *menu_layer) {
  menu_layer_update_caches(menu_layer);
}

bool menu_cell_layer_is_highlighted(const Layer *cell_layer) {
  return cell_layer->is_highlighted;
}

void menu_layer_set_normal_colors(MenuLayer *menu_layer, GColor background, GColor foreground) {
  menu_layer->normal_colors[MenuLayerColorBackground] = background;
  menu_layer->normal_colors[MenuLayerColorForeground] = foreground;
}

void menu_layer_set_highlight_colors(MenuLayer *menu_layer, GColor background, GColor foreground) {
  menu_layer->highlight_colors[MenuLayerColorBackground] = background;
  menu_layer->highlight_colors[MenuLayerColorForeground] = foreground;
}

bool menu_layer_get_center_focused(MenuLayer *menu_layer) {
  return menu_layer->center_focused;
}

void menu_layer_set_center_focused(MenuLayer *menu_layer, bool center_focused) {
  if (!menu_layer) {
    return;
  }
  prv_set_center_focused(menu_layer, center_focused);
  menu_layer_update_caches(menu_layer);
}
