#include <pebble.h>

enum {
  KEY_TIME    = 0,  // Inbound: current time string from companion app (unused — see KEY_SCREEN)
  KEY_COMMAND = 1,  // Outbound: button name sent to companion app
  KEY_SCREEN  = 2   // Inbound: C64 text screen (40x25, ASCII, rows separated by '\n')
};

#define SCREEN_FONT FONT_KEY_GOTHIC_24

// Curated navigation-only key set for the SELECT wheel. Movement in this
// game is mostly done with number keys, plus RETURN/SPACE to confirm.
static const char *const k_wheel_items[] = {
  "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "RETURN", "SPACE"
};
#define WHEEL_ITEM_COUNT ((int)(sizeof(k_wheel_items) / sizeof(k_wheel_items[0])))

static Window      *s_main_window;
static ScrollLayer  *s_scroll_layer;
static TextLayer    *s_screen_layer;

static Window      *s_wheel_window;
static TextLayer    *s_wheel_label_layer;
static TextLayer    *s_wheel_hint_layer;
static int           s_wheel_index = 0;

#define SPLASH_DURATION_MS 1800

static Window      *s_splash_window;
static BitmapLayer  *s_splash_bitmap_layer;
static GBitmap      *s_splash_bitmap;

// Worst case 40*25 cells at 2 UTF-8 bytes each (umlaut overrides) + 25
// row-separator newlines + NUL, matching the companion app's getScreenText()
// output (see vice_jni.c).
static char s_screen_buffer[2080] = "Waiting for C64 screen...";

static void send_key_to_phone(const char *key_str) {
  DictionaryIterator *iter;
  app_message_outbox_begin(&iter);
  if (iter == NULL) return;
  dict_write_cstring(iter, KEY_COMMAND, key_str);
  app_message_outbox_send();
}

// Resize the text layer and scroll layer's content size to fit s_screen_buffer
// at the current font/width. Must run whenever the buffer's text changes —
// the C64 screen content (and therefore wrapped height) varies frame to frame.
static void update_screen_layout(void) {
  GRect bounds = layer_get_bounds(scroll_layer_get_layer(s_scroll_layer));
  GFont font = fonts_get_system_font(SCREEN_FONT);
  GRect measure_box = GRect(0, 0, bounds.size.w, 4000);
  GSize content_size = graphics_text_layout_get_content_size(
      s_screen_buffer, font, measure_box, GTextOverflowModeWordWrap, GTextAlignmentLeft);
  // Floor content height at the viewport height so short text doesn't shrink
  // the scrollable area below what's visible.
  int16_t height = content_size.h > bounds.size.h ? content_size.h : bounds.size.h;

  layer_set_frame(text_layer_get_layer(s_screen_layer), GRect(0, 0, bounds.size.w, height));
  scroll_layer_set_content_size(s_scroll_layer, GSize(bounds.size.w, height));
}

static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
  Tuple *screen_tuple = dict_find(iterator, KEY_SCREEN);
  if (screen_tuple && screen_tuple->type == TUPLE_CSTRING) {
    snprintf(s_screen_buffer, sizeof(s_screen_buffer), "%s", screen_tuple->value->cstring);
    text_layer_set_text(s_screen_layer, s_screen_buffer);
    update_screen_layout();
    layer_mark_dirty(text_layer_get_layer(s_screen_layer));
  }
}

static void update_wheel_label(void) {
  text_layer_set_text(s_wheel_label_layer, k_wheel_items[s_wheel_index]);
}

static void wheel_up_click_handler(ClickRecognizerRef recognizer, void *context) {
  s_wheel_index = (s_wheel_index - 1 + WHEEL_ITEM_COUNT) % WHEEL_ITEM_COUNT;
  update_wheel_label();
}

static void wheel_down_click_handler(ClickRecognizerRef recognizer, void *context) {
  s_wheel_index = (s_wheel_index + 1) % WHEEL_ITEM_COUNT;
  update_wheel_label();
}

// Sends the highlighted item and pops back to the screen mirror. The BACK
// button is left unsubscribed so its default behavior (pop the window) acts
// as "cancel" for free.
static void wheel_select_click_handler(ClickRecognizerRef recognizer, void *context) {
  send_key_to_phone(k_wheel_items[s_wheel_index]);
  window_stack_pop(true);
}

static void wheel_click_config_provider(void *context) {
  window_single_click_subscribe(BUTTON_ID_UP,     wheel_up_click_handler);
  window_single_click_subscribe(BUTTON_ID_DOWN,   wheel_down_click_handler);
  window_single_click_subscribe(BUTTON_ID_SELECT, wheel_select_click_handler);
}

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

  s_wheel_label_layer = text_layer_create(GRect(0, bounds.size.h / 2 - 24, bounds.size.w, 48));
  text_layer_set_font(s_wheel_label_layer, fonts_get_system_font(FONT_KEY_BITHAM_30_BLACK));
  text_layer_set_text_alignment(s_wheel_label_layer, GTextAlignmentCenter);
  update_wheel_label();
  layer_add_child(window_layer, text_layer_get_layer(s_wheel_label_layer));

  s_wheel_hint_layer = text_layer_create(GRect(0, bounds.size.h - 30, bounds.size.w, 30));
  text_layer_set_font(s_wheel_hint_layer, fonts_get_system_font(FONT_KEY_GOTHIC_14));
  text_layer_set_text_alignment(s_wheel_hint_layer, GTextAlignmentCenter);
  text_layer_set_text(s_wheel_hint_layer, "UP/DOWN spin - SELECT send");
  layer_add_child(window_layer, text_layer_get_layer(s_wheel_hint_layer));
}

static void wheel_window_unload(Window *window) {
  text_layer_destroy(s_wheel_label_layer);
  text_layer_destroy(s_wheel_hint_layer);
}

static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
  window_stack_push(s_wheel_window, true);
}

// Passed to scroll_layer_set_callbacks(): the ScrollLayer's own click config
// provider calls this after wiring UP/DOWN to scroll, so we only need to add
// SELECT here — UP/DOWN stay dedicated to panning the text view.
static void scroll_click_config_provider(void *context) {
  window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
}

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

  s_splash_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_SPLASH);
  s_splash_bitmap_layer = bitmap_layer_create(bounds);
  bitmap_layer_set_bitmap(s_splash_bitmap_layer, s_splash_bitmap);
  // Source image is roughly square and smaller than every target screen, so
  // center it without stretching rather than filling/distorting the frame.
  bitmap_layer_set_alignment(s_splash_bitmap_layer, GAlignCenter);
  bitmap_layer_set_background_color(s_splash_bitmap_layer, GColorBlack);
  layer_add_child(window_layer, bitmap_layer_get_layer(s_splash_bitmap_layer));
}

static void splash_window_unload(Window *window) {
  bitmap_layer_destroy(s_splash_bitmap_layer);
  gbitmap_destroy(s_splash_bitmap);
}

// main_window is pushed underneath splash_window at startup (see init()), so
// popping splash here just reveals it — the stack is never empty, which
// matters because removing the last window on the stack kills the app.
static void splash_timeout_handler(void *data) {
  window_stack_remove(s_splash_window, true);
}

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

  s_scroll_layer = scroll_layer_create(bounds);
  scroll_layer_set_click_config_onto_window(s_scroll_layer, window);
  scroll_layer_set_callbacks(s_scroll_layer, (ScrollLayerCallbacks) {
    .click_config_provider = scroll_click_config_provider
  });

  // The 40-column C64 grid is not preserved — the companion app's text
  // already has '\n' per row, but at this font size lines reflow
  // (word-wrap) rather than line up like the original screen.
  s_screen_layer = text_layer_create(GRect(0, 0, bounds.size.w, bounds.size.h));
  text_layer_set_text(s_screen_layer, s_screen_buffer);
  text_layer_set_font(s_screen_layer, fonts_get_system_font(SCREEN_FONT));
  text_layer_set_text_alignment(s_screen_layer, GTextAlignmentLeft);
  scroll_layer_add_child(s_scroll_layer, text_layer_get_layer(s_screen_layer));

  update_screen_layout();
  layer_add_child(window_layer, scroll_layer_get_layer(s_scroll_layer));
}

static void main_window_unload(Window *window) {
  text_layer_destroy(s_screen_layer);
  scroll_layer_destroy(s_scroll_layer);
}

static void init(void) {
  s_main_window = window_create();
  window_set_window_handlers(s_main_window, (WindowHandlers) {
    .load   = main_window_load,
    .unload = main_window_unload
  });
  window_stack_push(s_main_window, true);

  s_wheel_window = window_create();
  window_set_click_config_provider(s_wheel_window, wheel_click_config_provider);
  window_set_window_handlers(s_wheel_window, (WindowHandlers) {
    .load   = wheel_window_load,
    .unload = wheel_window_unload
  });

  s_splash_window = window_create();
  window_set_window_handlers(s_splash_window, (WindowHandlers) {
    .load   = splash_window_load,
    .unload = splash_window_unload
  });
  // Pushed on top of the already-present main_window, not in place of an
  // empty stack — see the note on splash_timeout_handler below.
  window_stack_push(s_splash_window, true);
  app_timer_register(SPLASH_DURATION_MS, splash_timeout_handler, NULL);

  app_message_register_inbox_received(inbox_received_callback);
  // Inbox must hold the full 40x25 screen text (up to ~2080 bytes with UTF-8
  // umlaut overrides) plus dictionary overhead; outbox only ever carries a
  // short button-name string.
  app_message_open(2200, 64);
}

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

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