#include <pebble.h>

#define MAX_ROWS 4
#define ANIMATION_INTERVAL_MS 33
#define REFRESH_SWEEP_FRAMES 20
#define SLIDE_OUT_FRAMES 4
#define SLIDE_IN_FRAMES 5
#define SLIDE_OUT_DISTANCE_PX 18
#define SLIDE_IN_DISTANCE_PX 14
#define BAR_FILL_FRAMES 10
#define BAR_STAGGER_FRAMES 3
#define BOUNCE_FRAMES 3
#define BOUNCE_DISTANCE_PX 5
#define STAMP_FRAMES 4
#define PRESS_KICK_FRAMES 5
#define PRESS_KICK_DISTANCE_PX 12
#define PRESS_SHAKE_FRAMES 6
#define PRESS_SHAKE_DISTANCE_PX 12

// Page navigation choreography: the card content slides out in the travel
// direction while we wait for the phone, the new card slides in from the
// opposite side, then the quota bars pour in top-down. On failure the card
// slides back into place. When the page order wraps around, the incoming
// card overshoots and rubber-bands back to signal the seam in the loop.
typedef enum {
  MOTION_IDLE,
  MOTION_SLIDE_OUT,
  MOTION_WAITING,
  MOTION_SLIDE_IN,
  MOTION_BOUNCE,
  MOTION_BARS,
} MotionPhase;

// Refresh feedback rides the horizontal axis, which paging never uses, so the
// select button (middle-right of the body) feels like a plunger you pump data
// out of the phone with. KICK is the press stroke: the card content jolts left,
// away from the finger, then springs back. SHAKE is the failure recoil: a
// decaying side-to-side shudder, readable without reading the status text. Both
// run as a side-channel alongside MotionPhase, like the in-flight dots, so they
// compose with paging and the bar cascade instead of replacing them.
typedef enum {
  PRESS_NONE,
  PRESS_KICK,
  PRESS_SHAKE,
} PressPhase;

typedef struct {
  char label[24];
  char detail[28];
  int percent;
  int status_rank;
  bool has_bar;
} QuotaRow;

static Window *s_main_window;
static Layer *s_card_layer;
static AppTimer *s_animation_timer;

static int s_page_index = 0;
static int s_page_count = 0;
static int s_row_count = 0;
static MotionPhase s_motion_phase = MOTION_IDLE;
static int s_motion_frame = 0;
static int s_slide_offset = 0;
static int s_slide_from = 0;
static int s_slide_to = 0;
static int s_slide_direction = 1;
static int s_bar_frame = BAR_FILL_FRAMES + BAR_STAGGER_FRAMES * (MAX_ROWS - 1);
static int s_refresh_frame = 0;
static bool s_is_refreshing = false;
static bool s_wrap_pending = false;
static PressPhase s_press_phase = PRESS_NONE;
static int s_press_frame = 0;
static int s_press_offset_x = 0;

static char s_title_text[40] = "Pebblexus";
static char s_group_text[40] = "";
static char s_reset_text[40] = "Set URL and admin key";
static char s_status_text[16] = "Waiting";
static char s_updated_text[16] = "";
static QuotaRow s_rows[MAX_ROWS];

static uint32_t row_label_key(int row_index) {
  switch (row_index) {
  case 0:
    return MESSAGE_KEY_ROW_0_LABEL;
  case 1:
    return MESSAGE_KEY_ROW_1_LABEL;
  case 2:
    return MESSAGE_KEY_ROW_2_LABEL;
  default:
    return MESSAGE_KEY_ROW_3_LABEL;
  }
}

static uint32_t row_detail_key(int row_index) {
  switch (row_index) {
  case 0:
    return MESSAGE_KEY_ROW_0_DETAIL;
  case 1:
    return MESSAGE_KEY_ROW_1_DETAIL;
  case 2:
    return MESSAGE_KEY_ROW_2_DETAIL;
  default:
    return MESSAGE_KEY_ROW_3_DETAIL;
  }
}

static uint32_t row_percent_key(int row_index) {
  switch (row_index) {
  case 0:
    return MESSAGE_KEY_ROW_0_PERCENT;
  case 1:
    return MESSAGE_KEY_ROW_1_PERCENT;
  case 2:
    return MESSAGE_KEY_ROW_2_PERCENT;
  default:
    return MESSAGE_KEY_ROW_3_PERCENT;
  }
}

static uint32_t row_status_key(int row_index) {
  switch (row_index) {
  case 0:
    return MESSAGE_KEY_ROW_0_STATUS;
  case 1:
    return MESSAGE_KEY_ROW_1_STATUS;
  case 2:
    return MESSAGE_KEY_ROW_2_STATUS;
  default:
    return MESSAGE_KEY_ROW_3_STATUS;
  }
}

static uint32_t row_has_bar_key(int row_index) {
  switch (row_index) {
  case 0:
    return MESSAGE_KEY_ROW_0_HAS_BAR;
  case 1:
    return MESSAGE_KEY_ROW_1_HAS_BAR;
  case 2:
    return MESSAGE_KEY_ROW_2_HAS_BAR;
  default:
    return MESSAGE_KEY_ROW_3_HAS_BAR;
  }
}

static int clamp_percent(int percent) {
  if (percent < 0)
    return 0;
  if (percent > 100)
    return 100;
  return percent;
}

static GColor color_background(void) { return PBL_IF_COLOR_ELSE(GColorWhite, GColorWhite); }

static GColor color_card_outline(void) { return PBL_IF_COLOR_ELSE(GColorFromHEX(0xAAFFFF), GColorBlack); }

static GColor color_text_primary(void) { return GColorBlack; }

static GColor color_text_secondary(void) { return PBL_IF_COLOR_ELSE(GColorCobaltBlue, GColorBlack); }

static GColor color_text_muted(void) { return PBL_IF_COLOR_ELSE(GColorDarkGray, GColorBlack); }

static GColor color_text_soft(void) { return PBL_IF_COLOR_ELSE(GColorDukeBlue, GColorBlack); }

static GColor color_track(void) { return PBL_IF_COLOR_ELSE(GColorLightGray, GColorBlack); }

static GColor color_card_highlight(void) { return PBL_IF_COLOR_ELSE(GColorVividCerulean, GColorBlack); }

static GColor color_warning_soft(void) { return PBL_IF_COLOR_ELSE(GColorIcterine, GColorWhite); }

static GColor color_ok_soft(void) { return PBL_IF_COLOR_ELSE(GColorMintGreen, GColorWhite); }

static GColor color_for_status(int status_rank) {
#ifdef PBL_COLOR
  if (status_rank >= 4)
    return GColorRed;
  if (status_rank >= 3)
    return GColorOrange;
  return GColorJaegerGreen;
#else
  return GColorBlack;
#endif
}

static GColor color_status_fill(void) {
  if (strcmp(s_status_text, "CRITICAL") == 0)
    return PBL_IF_COLOR_ELSE(GColorMelon, GColorWhite);
  if (strcmp(s_status_text, "WARNING") == 0)
    return color_warning_soft();
  return color_ok_soft();
}

static GColor color_for_status_text(void) {
  if (strcmp(s_status_text, "CRITICAL") == 0)
    return color_for_status(4);
  if (strcmp(s_status_text, "WARNING") == 0)
    return color_for_status(3);
  return color_for_status(1);
}

static int ease_quad_in(int from, int to, int frame, int frame_count) {
  if (frame <= 0)
    return from;
  if (frame >= frame_count)
    return to;
  int t = (frame * 100) / frame_count;
  return from + ((to - from) * t * t) / 10000;
}

static int ease_quad_out(int from, int to, int frame, int frame_count) {
  if (frame <= 0)
    return from;
  if (frame >= frame_count)
    return to;
  int t = (frame * 100) / frame_count;
  return from - ((to - from) * t * (t - 200)) / 10000;
}

static int current_slide_offset(void) { return s_slide_offset; }

static int bar_cascade_total_frames(void) {
  int rows = s_row_count > MAX_ROWS ? MAX_ROWS : s_row_count;
  if (rows < 1)
    rows = 1;
  int cascade_frames = BAR_FILL_FRAMES + BAR_STAGGER_FRAMES * (rows - 1);
  int stamp_end_frame = BAR_FILL_FRAMES + STAMP_FRAMES;
  return cascade_frames > stamp_end_frame ? cascade_frames : stamp_end_frame;
}

static int current_bar_percent(int row_index, int percent) {
  int clamped = clamp_percent(percent);
  if (s_motion_phase == MOTION_SLIDE_IN || s_motion_phase == MOTION_BOUNCE)
    return 0;
  if (s_motion_phase != MOTION_BARS)
    return clamped;
  int frame = s_bar_frame - (row_index * BAR_STAGGER_FRAMES);
  return ease_quad_out(0, clamped, frame, BAR_FILL_FRAMES);
}

static bool status_is_alarming(void) {
  return strcmp(s_status_text, "CRITICAL") == 0 || strcmp(s_status_text, "WARNING") == 0;
}

// The status pill stamps in as the worst quota bar lands: briefly oversized,
// then settled. Only when the news warrants it.
static int status_pill_inflation(void) {
  if (s_motion_phase != MOTION_BARS || !status_is_alarming())
    return 0;
  int stamp_frame = s_bar_frame - BAR_FILL_FRAMES;
  if (stamp_frame < 0 || stamp_frame >= STAMP_FRAMES)
    return 0;
  return stamp_frame < STAMP_FRAMES / 2 ? 2 : 1;
}

static void mark_card_dirty(void) {
  if (s_card_layer != NULL)
    layer_mark_dirty(s_card_layer);
}

static void motion_timer_callback(void *data) {
  s_animation_timer = NULL;
  bool needs_tick = false;

  switch (s_motion_phase) {
  case MOTION_SLIDE_OUT:
    s_motion_frame += 1;
    s_slide_offset = ease_quad_in(s_slide_from, s_slide_to, s_motion_frame, SLIDE_OUT_FRAMES);
    if (s_motion_frame >= SLIDE_OUT_FRAMES) {
      s_motion_phase = MOTION_WAITING;
    } else {
      needs_tick = true;
    }
    break;
  case MOTION_SLIDE_IN:
    s_motion_frame += 1;
    s_slide_offset = ease_quad_out(s_slide_from, s_slide_to, s_motion_frame, SLIDE_IN_FRAMES);
    if (s_motion_frame >= SLIDE_IN_FRAMES) {
      if (s_slide_to != 0) {
        s_motion_phase = MOTION_BOUNCE;
        s_motion_frame = 0;
        s_slide_from = s_slide_to;
        s_slide_to = 0;
      } else {
        s_motion_phase = MOTION_BARS;
        s_bar_frame = 0;
      }
    }
    needs_tick = true;
    break;
  case MOTION_BOUNCE:
    s_motion_frame += 1;
    s_slide_offset = ease_quad_out(s_slide_from, 0, s_motion_frame, BOUNCE_FRAMES);
    if (s_motion_frame >= BOUNCE_FRAMES) {
      s_motion_phase = MOTION_BARS;
      s_bar_frame = 0;
    }
    needs_tick = true;
    break;
  case MOTION_BARS:
    s_bar_frame += 1;
    if (s_bar_frame >= bar_cascade_total_frames()) {
      s_motion_phase = MOTION_IDLE;
    } else {
      needs_tick = true;
    }
    break;
  default:
    break;
  }

  if (s_press_phase == PRESS_KICK) {
    s_press_frame += 1;
    if (s_press_frame >= PRESS_KICK_FRAMES) {
      s_press_offset_x = 0;
      s_press_phase = PRESS_NONE;
    } else {
      s_press_offset_x = ease_quad_out(-PRESS_KICK_DISTANCE_PX, 0, s_press_frame, PRESS_KICK_FRAMES);
      needs_tick = true;
    }
  } else if (s_press_phase == PRESS_SHAKE) {
    s_press_frame += 1;
    if (s_press_frame >= PRESS_SHAKE_FRAMES) {
      s_press_offset_x = 0;
      s_press_phase = PRESS_NONE;
    } else {
      int amplitude = (PRESS_SHAKE_DISTANCE_PX * (PRESS_SHAKE_FRAMES - s_press_frame)) / PRESS_SHAKE_FRAMES;
      s_press_offset_x = (s_press_frame % 2 == 0) ? amplitude : -amplitude;
      needs_tick = true;
    }
  }

  if (s_is_refreshing) {
    s_refresh_frame = (s_refresh_frame + 1) % REFRESH_SWEEP_FRAMES;
    needs_tick = true;
  }

  mark_card_dirty();
  if (needs_tick)
    s_animation_timer = app_timer_register(ANIMATION_INTERVAL_MS, motion_timer_callback, NULL);
}

static void ensure_motion_timer(void) {
  if (s_animation_timer == NULL)
    s_animation_timer = app_timer_register(ANIMATION_INTERVAL_MS, motion_timer_callback, NULL);
}

// Card departs in the travel direction and holds offset until the phone replies.
static void start_slide_out(int direction) {
  s_slide_direction = direction < 0 ? -1 : 1;
  s_slide_from = s_slide_offset;
  s_slide_to = -s_slide_direction * SLIDE_OUT_DISTANCE_PX;
  s_motion_phase = MOTION_SLIDE_OUT;
  s_motion_frame = 0;
  mark_card_dirty();
  ensure_motion_timer();
}

// New card arrives from the opposite side, settles, then bars pour in. On a
// wraparound it overshoots past its resting position and rubber-bands back.
static void start_slide_in(void) {
  s_slide_from = s_slide_direction * SLIDE_IN_DISTANCE_PX;
  s_slide_to = s_wrap_pending ? -s_slide_direction * BOUNCE_DISTANCE_PX : 0;
  s_wrap_pending = false;
  s_slide_offset = s_slide_from;
  s_motion_phase = MOTION_SLIDE_IN;
  s_motion_frame = 0;
  mark_card_dirty();
  ensure_motion_timer();
}

// The page never arrived: the card slides back into place.
static void start_slide_back(void) {
  s_wrap_pending = false;
  if (s_motion_phase != MOTION_SLIDE_OUT && s_motion_phase != MOTION_WAITING)
    return;
  s_slide_from = s_slide_offset;
  s_slide_to = 0;
  s_motion_phase = MOTION_SLIDE_IN;
  s_motion_frame = 0;
  mark_card_dirty();
  ensure_motion_timer();
}

// The select press lands instantly: the card jolts left on this frame, then the
// timer springs it back. The pump stroke that starts a refresh.
static void start_press_kick(void) {
  s_press_phase = PRESS_KICK;
  s_press_frame = 0;
  s_press_offset_x = -PRESS_KICK_DISTANCE_PX;
  mark_card_dirty();
  ensure_motion_timer();
}

// The pump came up empty: the card recoils side to side and settles. Failure
// you can feel without reading "No phone" or "Dropped".
static void start_press_shake(void) {
  s_press_phase = PRESS_SHAKE;
  s_press_frame = 0;
  s_press_offset_x = PRESS_SHAKE_DISTANCE_PX;
  mark_card_dirty();
  ensure_motion_timer();
}

// Arrival supersedes the press stroke so the bars cascade from rest, letting the
// press flow into the reward as one gesture.
static void clear_press_feedback(void) {
  s_press_phase = PRESS_NONE;
  s_press_frame = 0;
  s_press_offset_x = 0;
}

// Data arrived in place (initial load or select refresh): cascade the bars only.
static void start_bar_cascade(void) {
  s_wrap_pending = false;
  s_slide_offset = 0;
  clear_press_feedback();
  s_motion_phase = MOTION_BARS;
  s_motion_frame = 0;
  s_bar_frame = 0;
  mark_card_dirty();
  ensure_motion_timer();
}

static void snap_motion_idle(void) {
  s_wrap_pending = false;
  clear_press_feedback();
  s_motion_phase = MOTION_IDLE;
  s_motion_frame = 0;
  s_slide_offset = 0;
  s_bar_frame = bar_cascade_total_frames();
}

static void request_quota(void) {
  DictionaryIterator *iter;
  AppMessageResult result = app_message_outbox_begin(&iter);
  if (result != APP_MSG_OK || iter == NULL) {
    snprintf(s_status_text, sizeof(s_status_text), "Send fail");
    s_is_refreshing = false;
    start_press_shake();
    return;
  }

  dict_write_uint8(iter, MESSAGE_KEY_REQUEST_QUOTA, 1);
  dict_write_int32(iter, MESSAGE_KEY_PAGE_INDEX, s_page_index);
  result = app_message_outbox_send();
  if (result == APP_MSG_OK) {
    snprintf(s_status_text, sizeof(s_status_text), "Refreshing");
    s_updated_text[0] = '\0';
    s_is_refreshing = true;
    s_refresh_frame = 0;
    start_press_kick();
  } else {
    snprintf(s_status_text, sizeof(s_status_text), "Send fail");
    s_is_refreshing = false;
    start_press_shake();
  }
}

static void request_page(int page_index) {
  if (s_page_count <= 0)
    return;
  int direction = page_index >= s_page_index ? 1 : -1;
  bool wrapped = false;
  if (page_index < 0) {
    page_index = s_page_count - 1;
    direction = -1;
    wrapped = true;
  }
  if (page_index >= s_page_count) {
    page_index = 0;
    direction = 1;
    wrapped = true;
  }

  DictionaryIterator *iter;
  AppMessageResult result = app_message_outbox_begin(&iter);
  if (result != APP_MSG_OK || iter == NULL) {
    snprintf(s_status_text, sizeof(s_status_text), "Page fail");
    mark_card_dirty();
    return;
  }

  s_page_index = page_index;
  dict_write_int32(iter, MESSAGE_KEY_REQUEST_PAGE, page_index);
  result = app_message_outbox_send();
  if (result == APP_MSG_OK) {
    s_wrap_pending = wrapped;
    start_slide_out(direction);
  } else {
    snprintf(s_status_text, sizeof(s_status_text), "Send fail");
    mark_card_dirty();
  }
}

static void up_click_handler(ClickRecognizerRef recognizer, void *context) { request_page(s_page_index - 1); }

static void down_click_handler(ClickRecognizerRef recognizer, void *context) { request_page(s_page_index + 1); }

static void select_click_handler(ClickRecognizerRef recognizer, void *context) { request_quota(); }

static void click_config_provider(void *context) {
  window_single_repeating_click_subscribe(BUTTON_ID_UP, 180, up_click_handler);
  window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
  window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 180, down_click_handler);
}

static void draw_text(GContext *ctx, const char *text, GRect box, GFont font, GColor color, GTextAlignment alignment) {
  graphics_context_set_text_color(ctx, color);
  graphics_draw_text(ctx, text, font, box, GTextOverflowModeTrailingEllipsis, alignment, NULL);
}

static int text_width(const char *text, GFont font) {
  GSize size = graphics_text_layout_get_content_size(text, font, GRect(0, 0, 500, 30), GTextOverflowModeWordWrap,
                                                     GTextAlignmentLeft);
  return size.w;
}

static void draw_progress_bar(GContext *ctx, GRect frame, int row_index, int percent, GColor color) {
  graphics_context_set_fill_color(ctx, color_track());
  graphics_fill_rect(ctx, frame, 3, GCornersAll);

  int fill_width = (frame.size.w * current_bar_percent(row_index, percent)) / 100;
  if (fill_width < 2 && percent > 0)
    fill_width = 2;
  graphics_context_set_fill_color(ctx, color);
  graphics_fill_rect(ctx, GRect(frame.origin.x, frame.origin.y, fill_width, frame.size.h), 3, GCornersAll);
}

static void draw_status_pill(GContext *ctx, GRect frame) {
  int inflation = status_pill_inflation();
  GRect pill = GRect(frame.origin.x - inflation, frame.origin.y - inflation, frame.size.w + (inflation * 2),
                     frame.size.h + (inflation * 2));
  graphics_context_set_fill_color(ctx, color_status_fill());
  graphics_fill_rect(ctx, pill, 6, GCornersAll);
  graphics_context_set_stroke_color(ctx, color_for_status_text());
  graphics_draw_rect(ctx, pill);
  draw_text(ctx, s_status_text, GRect(frame.origin.x, frame.origin.y - 4, frame.size.w, frame.size.h + 4),
            fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), color_text_primary(), GTextAlignmentCenter);
}

// In-flight pump stroke: a short segment strokes back and forth along the
// horizontal pump axis inside the status corner the page count usually owns.
// It decelerates into each end like a piston hitting the end of its travel, so
// the wait reads as the phone being pumped, not a generic spinner. Shares the
// corner with the page count by mutual exclusion, so the two never overlap.
static void draw_refresh_pump(GContext *ctx, GRect box) {
  const int track_height = 6;
  const int segment_width = 14;
  int track_y = box.origin.y + (box.size.h - track_height) / 2;

  graphics_context_set_fill_color(ctx, color_track());
  graphics_fill_rect(ctx, GRect(box.origin.x, track_y, box.size.w, track_height), 3, GCornersAll);

  int travel = box.size.w - segment_width;
  int half = REFRESH_SWEEP_FRAMES / 2;
  int segment_x = s_refresh_frame < half ? ease_quad_out(0, travel, s_refresh_frame, half)
                                         : ease_quad_out(travel, 0, s_refresh_frame - half, half);
  graphics_context_set_fill_color(ctx, color_text_secondary());
  graphics_fill_rect(ctx, GRect(box.origin.x + segment_x, track_y, segment_width, track_height), 3, GCornersAll);
}

// Vertical rail along the right edge: pages travel up/down, so does the rail.
static void draw_page_dots(GContext *ctx, GRect frame) {
  if (s_page_count <= 1)
    return;

  int dots = s_page_count > 7 ? 7 : s_page_count;
  int total_height = dots * 7 - 3;
  int x = frame.origin.x + frame.size.w - 5;
  int y = frame.origin.y + (frame.size.h - total_height) / 2;

  for (int i = 0; i < dots; i += 1) {
    int active = i == s_page_index || (s_page_count > 7 && i == 6 && s_page_index >= 6);
    graphics_context_set_fill_color(ctx, active ? color_card_highlight() : color_text_muted());
    int radius = active ? 2 : 1;
    bool card_in_flight = s_motion_phase == MOTION_SLIDE_OUT || s_motion_phase == MOTION_WAITING ||
                          s_motion_phase == MOTION_SLIDE_IN || s_motion_phase == MOTION_BOUNCE;
    if (active && card_in_flight)
      radius = 3;
    graphics_fill_circle(ctx, GPoint(x, y + (i * 7)), radius);
  }
}

static void card_update_proc(Layer *layer, GContext *ctx) {
  GRect bounds = layer_get_bounds(layer);
  graphics_context_set_fill_color(ctx, color_background());
  graphics_fill_rect(ctx, bounds, 0, GCornerNone);

  const int margin = 10;
  int offset = current_slide_offset();
  int content_x = margin + s_press_offset_x;
  int content_w = bounds.size.w - (margin * 2);
  int y = 8 + offset;

  draw_text(ctx, s_title_text, GRect(content_x, y, content_w - 50, 29), fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
            color_text_primary(), GTextAlignmentLeft);

  char page_text[16];
  if (s_page_count > 0) {
    int display_page_index = s_page_index + 1;
    if (display_page_index < 1)
      display_page_index = 1;
    if (display_page_index > 99)
      display_page_index = 99;
    int display_page_count = s_page_count;
    if (display_page_count < 1)
      display_page_count = 1;
    if (display_page_count > 99)
      display_page_count = 99;
    snprintf(page_text, sizeof(page_text), "%d/%d", display_page_index, display_page_count);
  } else {
    snprintf(page_text, sizeof(page_text), "--");
  }
  GRect status_corner = GRect(content_x + content_w - 46, y + 2, 46, 24);
  if (s_is_refreshing) {
    draw_refresh_pump(ctx, status_corner);
  } else {
    draw_text(ctx, page_text, status_corner, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), color_text_secondary(),
              GTextAlignmentRight);
  }

  y += 26;
  if (s_status_text[0] != '\0') {
    draw_status_pill(ctx, GRect(content_x + content_w - 60, y + 1, 60, 18));
  }

  y += s_status_text[0] == '\0' ? 8 : 26;
  if (s_row_count <= 0) {
    graphics_context_set_fill_color(ctx, color_card_highlight());
    graphics_fill_circle(ctx, GPoint(content_x + (content_w / 2), y + 26), 15);
    graphics_context_set_fill_color(ctx, color_background());
    graphics_fill_circle(ctx, GPoint(content_x + (content_w / 2), y + 26), 9);
    draw_text(ctx, "Quota unavailable", GRect(content_x, y + 48, content_w, 34),
              fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD), color_text_primary(), GTextAlignmentCenter);
    draw_text(ctx, s_reset_text, GRect(content_x, y + 86, content_w, 46), fonts_get_system_font(FONT_KEY_GOTHIC_24),
              color_text_secondary(), GTextAlignmentCenter);
  } else {
    // Row layout heights (42/28/50 + 3px separators) are mirrored by the
    // phone-side page packing in src/pkjs/index.js; keep them in sync.
    GFont row_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
    int row_bottom_limit = bounds.size.h - 34 + offset;

    // One detail column for all bar rows keeps the bar tracks aligned.
    int detail_column_w = 28;
    for (int i = 0; i < s_row_count && i < MAX_ROWS; i += 1) {
      if (!s_rows[i].has_bar)
        continue;
      int detail_w = text_width(s_rows[i].detail, row_font);
      if (detail_w > detail_column_w)
        detail_column_w = detail_w;
    }
    if (detail_column_w > 100)
      detail_column_w = 100;

    int rows_drawn = 0;
    for (int i = 0; i < s_row_count && i < MAX_ROWS; i += 1) {
      QuotaRow *row = &s_rows[i];
      GColor accent = color_for_status(row->status_rank);
      if (row->has_bar) {
        if (y + 42 > row_bottom_limit)
          break;
        draw_text(ctx, row->label, GRect(content_x, y, content_w, 22), row_font, color_text_primary(),
                  GTextAlignmentLeft);
        y += 22;
        int bar_w = content_w - detail_column_w - 8;
        draw_text(ctx, row->detail, GRect(content_x + bar_w + 8, y - 3, detail_column_w, 22), row_font,
                  color_text_soft(), GTextAlignmentRight);
        draw_progress_bar(ctx, GRect(content_x, y + 6, bar_w, 6), i, row->percent, accent);
        y += 20;
      } else {
        int detail_w = text_width(row->detail, row_font);
        if (detail_w > 100)
          detail_w = 100;
        int label_w = content_w - detail_w - 8;
        bool two_line = text_width(row->label, row_font) > label_w;
        if (y + (two_line ? 50 : 28) > row_bottom_limit)
          break;
        if (two_line) {
          draw_text(ctx, row->label, GRect(content_x, y, content_w, 22), row_font, color_text_primary(),
                    GTextAlignmentLeft);
          y += 22;
          draw_text(ctx, row->detail, GRect(content_x, y, content_w, 22), row_font, color_text_primary(),
                    GTextAlignmentRight);
          y += 28;
        } else {
          draw_text(ctx, row->label, GRect(content_x, y, label_w, 22), row_font, color_text_primary(),
                    GTextAlignmentLeft);
          draw_text(ctx, row->detail, GRect(content_x + label_w + 8, y, detail_w, 22), row_font, color_text_primary(),
                    GTextAlignmentRight);
          y += 28;
        }
      }
      rows_drawn += 1;
      if (i + 1 < s_row_count && i + 1 < MAX_ROWS) {
        graphics_context_set_stroke_color(ctx, color_card_outline());
        graphics_draw_line(ctx, GPoint(content_x, y), GPoint(content_x + content_w, y));
        y += 3;
      }
    }

    int rows_visible = s_row_count > MAX_ROWS ? MAX_ROWS : s_row_count;
    if (rows_drawn < rows_visible || s_row_count > MAX_ROWS) {
      draw_text(ctx, "+ more", GRect(content_x, y, content_w, 22), fonts_get_system_font(FONT_KEY_GOTHIC_18),
                color_text_muted(), GTextAlignmentCenter);
    }
  }

  draw_page_dots(ctx, bounds);

  draw_text(ctx, s_reset_text, GRect(content_x, bounds.size.h - 32 + offset, content_w, 24),
            fonts_get_system_font(FONT_KEY_GOTHIC_18), color_text_muted(), GTextAlignmentCenter);
}

static void copy_string_tuple(DictionaryIterator *iterator, uint32_t key, char *buffer, size_t buffer_size) {
  Tuple *tuple = dict_find(iterator, key);
  if (tuple == NULL)
    return;
  snprintf(buffer, buffer_size, "%s", tuple->value->cstring);
}

static void read_row(DictionaryIterator *iterator, int row_index) {
  QuotaRow *row = &s_rows[row_index];
  copy_string_tuple(iterator, row_label_key(row_index), row->label, sizeof(row->label));
  copy_string_tuple(iterator, row_detail_key(row_index), row->detail, sizeof(row->detail));

  Tuple *percent_tuple = dict_find(iterator, row_percent_key(row_index));
  row->percent = percent_tuple != NULL ? clamp_percent((int)percent_tuple->value->int32) : 0;

  Tuple *status_tuple = dict_find(iterator, row_status_key(row_index));
  row->status_rank = status_tuple != NULL ? (int)status_tuple->value->int32 : 1;

  Tuple *has_bar_tuple = dict_find(iterator, row_has_bar_key(row_index));
  row->has_bar = has_bar_tuple != NULL && has_bar_tuple->value->int32 != 0;
}

static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
  Tuple *error_tuple = dict_find(iterator, MESSAGE_KEY_ERROR);
  if (error_tuple != NULL) {
    s_is_refreshing = false;
    s_row_count = 0;
    s_page_index = 0;
    s_page_count = 0;
    snprintf(s_title_text, sizeof(s_title_text), "Pebblexus");
    snprintf(s_reset_text, sizeof(s_reset_text), "%s", error_tuple->value->cstring);
    snprintf(s_status_text, sizeof(s_status_text), "Select retries");
    s_updated_text[0] = '\0';
    snap_motion_idle();
    mark_card_dirty();
    return;
  }

  static char s_last_data_status[16] = "";
  bool was_refreshing = s_is_refreshing;

  s_is_refreshing = false;
  copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_TITLE, s_title_text, sizeof(s_title_text));
  copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_LABEL, s_group_text, sizeof(s_group_text));
  copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_RESET, s_reset_text, sizeof(s_reset_text));
  copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_STATUS, s_status_text, sizeof(s_status_text));
  copy_string_tuple(iterator, MESSAGE_KEY_UPDATED_AT, s_updated_text, sizeof(s_updated_text));

  // Redundant haptic feedback only when a refresh reveals a newly critical
  // page; browsing between known-critical pages stays silent.
  if (was_refreshing && strcmp(s_status_text, "CRITICAL") == 0 && strcmp(s_last_data_status, "CRITICAL") != 0)
    vibes_short_pulse();
  snprintf(s_last_data_status, sizeof(s_last_data_status), "%s", s_status_text);

  Tuple *page_index_tuple = dict_find(iterator, MESSAGE_KEY_PAGE_INDEX);
  if (page_index_tuple != NULL)
    s_page_index = (int)page_index_tuple->value->int32;

  Tuple *page_count_tuple = dict_find(iterator, MESSAGE_KEY_PAGE_COUNT);
  if (page_count_tuple != NULL)
    s_page_count = (int)page_count_tuple->value->int32;
  if (s_page_count < 0)
    s_page_count = 0;
  if (s_page_index >= s_page_count)
    s_page_index = s_page_count - 1;
  if (s_page_index < 0)
    s_page_index = 0;

  Tuple *row_count_tuple = dict_find(iterator, MESSAGE_KEY_ROW_COUNT);
  int row_count = row_count_tuple != NULL ? (int)row_count_tuple->value->int32 : 0;
  if (row_count < 0)
    row_count = 0;
  s_row_count = row_count;

  int rows_to_read = row_count > MAX_ROWS ? MAX_ROWS : row_count;
  for (int i = 0; i < rows_to_read; i += 1) {
    s_rows[i].label[0] = '\0';
    s_rows[i].detail[0] = '\0';
    s_rows[i].percent = 0;
    s_rows[i].status_rank = 1;
    s_rows[i].has_bar = false;
    read_row(iterator, i);
  }

  if (s_motion_phase == MOTION_SLIDE_OUT || s_motion_phase == MOTION_WAITING) {
    start_slide_in();
  } else {
    start_bar_cascade();
  }
}

static void inbox_dropped_callback(AppMessageResult reason, void *context) {
  bool was_refreshing = s_is_refreshing;
  s_is_refreshing = false;
  snprintf(s_status_text, sizeof(s_status_text), "Dropped");
  start_slide_back();
  if (was_refreshing)
    start_press_shake();
  mark_card_dirty();
}

static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context) {
  bool was_refreshing = s_is_refreshing;
  s_is_refreshing = false;
  snprintf(s_status_text, sizeof(s_status_text), "No phone");
  start_slide_back();
  if (was_refreshing)
    start_press_shake();
  mark_card_dirty();
}

static void main_window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  s_card_layer = layer_create(bounds);
  layer_set_update_proc(s_card_layer, card_update_proc);
  layer_add_child(window_layer, s_card_layer);
}

static void main_window_unload(Window *window) {
  if (s_animation_timer != NULL) {
    app_timer_cancel(s_animation_timer);
    s_animation_timer = NULL;
  }
  layer_destroy(s_card_layer);
}

static void init(void) {
  s_main_window = window_create();
  window_set_background_color(s_main_window, color_background());
  window_set_click_config_provider(s_main_window, click_config_provider);
  window_set_window_handlers(s_main_window, (WindowHandlers){.load = main_window_load, .unload = main_window_unload});

  app_message_register_inbox_received(inbox_received_callback);
  app_message_register_inbox_dropped(inbox_dropped_callback);
  app_message_register_outbox_failed(outbox_failed_callback);
  app_message_open(2048, 256);

  window_stack_push(s_main_window, true);
  request_quota();
}

static void deinit(void) { window_destroy(s_main_window); }

int main(void) {
  init();
  app_event_loop();
  deinit();
}
