/*
 * Who's home — a Pebble watchapp showing the household floorplan and who is in each
 * room, from identity's GET /floorplan/presence.
 *
 * IT IS AN APP, NOT A FACE, AND THE BUTTONS ARE WHY
 * -------------------------------------------------
 * Watchfaces receive no button events (Select opens the app menu, Up/Down are system
 * shortcuts) and Pebble restricts touch to apps as well. Cycling through rooms is the
 * point, so this is a watchapp: launched from the menu rather than shown by raising a
 * wrist, and its JS — therefore its data — only lives while it is open.
 *
 * That last part shapes the UI: every launch begins with a fetch. So the last known
 * plan is persisted and drawn IMMEDIATELY with its age, then repainted when the fetch
 * lands. A spinner on a screen that already has something true to say is a worse
 * answer than slightly stale truth, clearly labelled.
 *
 * RESOLUTION-INDEPENDENT ON PURPOSE
 * ----------------------------------
 * Nothing here hardcodes 260x260 or 200x228. Every position derives from
 * layer_get_bounds(), because the target is whatever Pebble is on the wrist and the new
 * Round 2's platform identifier is not something this file should be asserting. The
 * phone inscribes the plan in a circle for round screens (see index.js); the watch just
 * scales 0..255 into its own bounds.
 *
 * The wire format is WIREFORMAT.md. This file is the reader; src/pkjs/index.js is the
 * writer. Change one, change both, and bump the version byte — a silent mismatch
 * decodes into garbage rooms rather than failing.
 */

#include <pebble.h>

#define WIRE_VERSION 1

#define MAX_ROOMS 16
#define MAX_VERTICES 20
#define MAX_OCCUPANTS 6
#define MAX_TARGETS 4
#define MAX_UNPLACED 8
#define NAME_LEN 25

// identity's PERSON_COLORS, same order as the JS table. Every value sits on the
// 2-bit-per-channel lattice a colour Pebble renders natively, so what is drawn here is
// byte-identical to the swatch in the admin panel rather than a dithered approximation.
static GColor person_colour(uint8_t index) {
  switch (index) {
    case 0: return GColorFromRGB(255, 0, 0);
    case 1: return GColorFromRGB(0, 85, 255);
    case 2: return GColorFromRGB(255, 170, 0);
    case 3: return GColorFromRGB(0, 170, 0);
    case 4: return GColorFromRGB(170, 0, 255);
    case 5: return GColorFromRGB(0, 170, 170);
    case 6: return GColorFromRGB(255, 85, 170);
    case 7: return GColorFromRGB(170, 85, 0);
    default: return GColorLightGray;
  }
}

typedef struct {
  uint8_t colour;
  char initial;
  bool has_position;
  uint8_t px, py;
  char name[NAME_LEN];
} Occupant;

typedef struct {
  char name[NAME_LEN];
  uint8_t state;            // 0 empty, 1 occupied, 2 unknown
  uint8_t vertex_count;
  uint8_t vx[MAX_VERTICES], vy[MAX_VERTICES];
  uint8_t target_count;
  uint8_t tx[MAX_TARGETS], ty[MAX_TARGETS];
  uint8_t occupant_count;
  Occupant occupants[MAX_OCCUPANTS];
} Room;

static Room s_rooms[MAX_ROOMS];
static uint8_t s_room_count;
static Occupant s_unplaced[MAX_UNPLACED];
static uint8_t s_unplaced_count;
static bool s_have_plan;
static time_t s_updated_at;
static char s_status[64];

// Which room the detail window is showing. s_room_count means the "somewhere in the
// house" entry, which is deliberately part of the same cycle — the people the system
// cannot place are exactly who you picked the watch up to find.
static int s_detail_index;

static Window *s_plan_window;
static Layer *s_plan_layer;
static Window *s_detail_window;
static Layer *s_detail_layer;

static const uint32_t PERSIST_PLAN = 1;

// --- parsing --------------------------------------------------------------------------
typedef struct {
  const uint8_t *data;
  uint16_t length;
  uint16_t pos;
  bool overrun;
} Reader;

static uint8_t r_u8(Reader *r) {
  if (r->pos >= r->length) {
    // Every read past the end sets this once and keeps returning zero. The parse is
    // abandoned wholesale afterwards: a half-decoded plan looks like a real plan with
    // rooms missing, which is the one failure a person could not detect by looking.
    r->overrun = true;
    return 0;
  }
  return r->data[r->pos++];
}

static void r_str(Reader *r, char *out, size_t cap) {
  uint8_t len = r_u8(r);
  size_t copied = 0;
  for (uint8_t i = 0; i < len; i++) {
    uint8_t c = r_u8(r);
    if (copied + 1 < cap) out[copied++] = (char)c;
  }
  out[copied] = '\0';
}

static bool parse_plan(const uint8_t *data, uint16_t length) {
  Reader r = { .data = data, .length = length, .pos = 0, .overrun = false };

  uint8_t version = r_u8(&r);
  if (version != WIRE_VERSION) {
    snprintf(s_status, sizeof(s_status), "Phone app is a different version");
    return false;
  }

  uint8_t rooms = r_u8(&r);
  uint8_t unplaced = r_u8(&r);
  r_u8(&r);  // flags — positions_available, currently implied by the targets themselves

  s_room_count = rooms > MAX_ROOMS ? MAX_ROOMS : rooms;
  s_unplaced_count = unplaced > MAX_UNPLACED ? MAX_UNPLACED : unplaced;

  for (uint8_t i = 0; i < rooms; i++) {
    Room scratch;
    Room *room = (i < s_room_count) ? &s_rooms[i] : &scratch;
    memset(room, 0, sizeof(Room));

    r_str(&r, room->name, NAME_LEN);
    room->state = r_u8(&r);

    uint8_t vertices = r_u8(&r);
    room->vertex_count = vertices > MAX_VERTICES ? MAX_VERTICES : vertices;
    for (uint8_t v = 0; v < vertices; v++) {
      uint8_t x = r_u8(&r), y = r_u8(&r);
      if (v < room->vertex_count) { room->vx[v] = x; room->vy[v] = y; }
    }

    uint8_t targets = r_u8(&r);
    room->target_count = targets > MAX_TARGETS ? MAX_TARGETS : targets;
    for (uint8_t t = 0; t < targets; t++) {
      uint8_t x = r_u8(&r), y = r_u8(&r);
      if (t < room->target_count) { room->tx[t] = x; room->ty[t] = y; }
    }

    uint8_t occupants = r_u8(&r);
    room->occupant_count = occupants > MAX_OCCUPANTS ? MAX_OCCUPANTS : occupants;
    for (uint8_t o = 0; o < occupants; o++) {
      Occupant person;
      memset(&person, 0, sizeof(person));
      person.colour = r_u8(&r);
      person.initial = (char)r_u8(&r);
      uint8_t flags = r_u8(&r);
      person.has_position = (flags & 1) != 0;
      if (person.has_position) { person.px = r_u8(&r); person.py = r_u8(&r); }
      r_str(&r, person.name, NAME_LEN);
      if (o < room->occupant_count) room->occupants[o] = person;
    }
  }

  for (uint8_t i = 0; i < unplaced; i++) {
    Occupant person;
    memset(&person, 0, sizeof(person));
    person.colour = r_u8(&r);
    person.initial = (char)r_u8(&r);
    r_str(&r, person.name, NAME_LEN);
    if (i < s_unplaced_count) s_unplaced[i] = person;
  }

  if (r.overrun) {
    s_room_count = 0;
    s_unplaced_count = 0;
    snprintf(s_status, sizeof(s_status), "Truncated data — nothing shown");
    return false;
  }

  s_have_plan = true;
  s_updated_at = time(NULL);
  return true;
}

// --- drawing --------------------------------------------------------------------------
static GPoint to_screen(GRect bounds, uint8_t x, uint8_t y) {
  return GPoint(bounds.origin.x + (bounds.size.w * x) / 255,
                bounds.origin.y + (bounds.size.h * y) / 255);
}

static void draw_marker(GContext *ctx, GPoint at, uint8_t colour, char initial, uint8_t radius) {
  graphics_context_set_fill_color(ctx, person_colour(colour));
  graphics_fill_circle(ctx, at, radius);
  graphics_context_set_stroke_color(ctx, GColorBlack);
  graphics_draw_circle(ctx, at, radius);

  // Dark glyph on the person's colour. Every value in the palette is mid-to-bright, so
  // a white letter would vanish on the amber — the same reason the admin panel and the
  // 3D plan both draw the initial in near-black.
  char text[2] = { initial ? initial : '?', '\0' };
  graphics_context_set_text_color(ctx, GColorBlack);
  graphics_draw_text(ctx, text, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD),
                     GRect(at.x - radius, at.y - radius + 1, radius * 2, radius * 2),
                     GTextOverflowModeFill, GTextAlignmentCenter, NULL);
}

static void draw_plan(Layer *layer, GContext *ctx) {
  GRect bounds = layer_get_bounds(layer);

  graphics_context_set_fill_color(ctx, GColorBlack);
  graphics_fill_rect(ctx, bounds, 0, GCornerNone);

  if (!s_have_plan || s_room_count == 0) {
    graphics_context_set_text_color(ctx, GColorWhite);
    graphics_draw_text(ctx, s_status[0] ? s_status : "Loading…",
                       fonts_get_system_font(FONT_KEY_GOTHIC_18),
                       GRect(bounds.origin.x + 10, bounds.origin.y + bounds.size.h / 2 - 30,
                             bounds.size.w - 20, 60),
                       GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
    return;
  }

  // A little inset so a room edge never lands on the bezel — on a round screen the
  // outermost pixels are exactly where you least want a wall. The extra room at the
  // bottom is for the status line: on a circle a full-width footer would be clipped,
  // so it is centred and short.
  GRect area = grect_inset(bounds, GEdgeInsets(6));
  area.size.h -= 18;

  for (uint8_t i = 0; i < s_room_count; i++) {
    Room *room = &s_rooms[i];
    if (room->vertex_count < 3) continue;

    GPoint points[MAX_VERTICES];
    for (uint8_t v = 0; v < room->vertex_count; v++) {
      points[v] = to_screen(area, room->vx[v], room->vy[v]);
    }
    GPathInfo info = { .num_points = room->vertex_count, .points = points };
    GPath *path = gpath_create(&info);

    // OCCUPANCY READS IN LIGHTNESS, NOT HUE. The occupant markers already use colour to
    // mean *who*; if the room fill competed on the same channel neither would be
    // legible on a 64-colour panel. Lit rooms are the bright shapes.
    graphics_context_set_fill_color(ctx, room->state == 1 ? GColorWhite : GColorOxfordBlue);
    gpath_draw_filled(ctx, path);

    graphics_context_set_stroke_color(ctx, room->state == 1 ? GColorWhite : GColorLightGray);
    gpath_draw_outline(ctx, path);
    gpath_destroy(path);

    // The third state — drawn, but HA never reports this area — gets a dotted edge.
    // "Nobody is in the study" and "nothing can see the study" are different sentences,
    // and rendering the second as the first is a quiet lie.
    if (room->state == 2) {
      graphics_context_set_stroke_color(ctx, GColorDarkGray);
      for (uint8_t v = 0; v < room->vertex_count; v++) {
        GPoint a = points[v];
        GPoint b = points[(v + 1) % room->vertex_count];
        graphics_draw_line(ctx, a, GPoint((a.x + b.x) / 2, (a.y + b.y) / 2));
      }
    }
  }

  // The age of what is on screen, always. The app's JS only lives while it is open, so
  // a launch draws the last known plan before the fetch lands — and a stale plan that
  // does not say it is stale is the one way this app could mislead somebody.
  char footer[48];
  if (s_status[0]) {
    snprintf(footer, sizeof(footer), "%s", s_status);
  } else {
    int minutes = (int)((time(NULL) - s_updated_at) / 60);
    if (minutes < 1) snprintf(footer, sizeof(footer), "just now");
    else if (minutes < 60) snprintf(footer, sizeof(footer), "%d min ago", minutes);
    else snprintf(footer, sizeof(footer), "%dh ago", minutes / 60);
  }
  graphics_context_set_text_color(ctx, GColorLightGray);
  graphics_draw_text(ctx, footer, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD),
                     GRect(bounds.origin.x + 8, bounds.origin.y + bounds.size.h - 20,
                           bounds.size.w - 16, 18),
                     GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL);

  // Markers after every room, so one is never painted over by a later room's fill.
  for (uint8_t i = 0; i < s_room_count; i++) {
    Room *room = &s_rooms[i];
    if (room->vertex_count < 3) continue;

    int32_t cx = 0, cy = 0;
    for (uint8_t v = 0; v < room->vertex_count; v++) { cx += room->vx[v]; cy += room->vy[v]; }
    GPoint centre = to_screen(area, cx / room->vertex_count, cy / room->vertex_count);

    // Anonymous radar targets: somebody is there and nobody knows who. A hollow ring,
    // never an initial — every visual language this app has for a PERSON is reserved
    // for people it can name.
    for (uint8_t t = 0; t < room->target_count; t++) {
      GPoint at = to_screen(area, room->tx[t], room->ty[t]);
      graphics_context_set_stroke_color(ctx, GColorDarkGray);
      graphics_draw_circle(ctx, at, 5);
    }

    uint8_t placed = 0;
    for (uint8_t o = 0; o < room->occupant_count; o++) {
      Occupant *person = &room->occupants[o];
      if (person->has_position) {
        draw_marker(ctx, to_screen(area, person->px, person->py), person->colour, person->initial, 8);
      } else {
        // Fan the unpositioned ones around the room's centre so two people in one room
        // do not draw on top of each other.
        int16_t offset = (int16_t)(placed * 17) - 8;
        draw_marker(ctx, GPoint(centre.x + offset, centre.y), person->colour, person->initial, 8);
        placed++;
      }
    }
  }
}

static void draw_detail(Layer *layer, GContext *ctx) {
  GRect bounds = layer_get_bounds(layer);
  graphics_context_set_fill_color(ctx, GColorBlack);
  graphics_fill_rect(ctx, bounds, 0, GCornerNone);
  graphics_context_set_text_color(ctx, GColorWhite);

  const char *title;
  Occupant *people;
  uint8_t count;
  if (s_detail_index >= s_room_count) {
    title = "Somewhere in the house";
    people = s_unplaced;
    count = s_unplaced_count;
  } else {
    title = s_rooms[s_detail_index].name;
    people = s_rooms[s_detail_index].occupants;
    count = s_rooms[s_detail_index].occupant_count;
  }

  // Round screens clip the corners, so everything is centred and inset rather than
  // left-aligned against an edge that may not exist.
  GRect area = grect_inset(bounds, GEdgeInsets(10));
  graphics_draw_text(ctx, title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
                     GRect(area.origin.x, area.origin.y + 6, area.size.w, 30),
                     GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL);

  int16_t y = area.origin.y + 42;
  if (count == 0) {
    // "Nobody here" is a finding, not an empty screen. A blank list reads as a fault.
    graphics_draw_text(ctx, s_detail_index >= s_room_count ? "Everyone is placed" : "Nobody here",
                       fonts_get_system_font(FONT_KEY_GOTHIC_18),
                       GRect(area.origin.x, y, area.size.w, 24),
                       GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL);
    return;
  }

  for (uint8_t i = 0; i < count && y < area.origin.y + area.size.h - 20; i++) {
    GPoint dot = GPoint(area.origin.x + 14, y + 10);
    graphics_context_set_fill_color(ctx, person_colour(people[i].colour));
    graphics_fill_circle(ctx, dot, 6);

    // The dot stays even though there is room for the name: it is what ties this line
    // back to the marker on the plan, and to the same person in the admin panel.
    char line[NAME_LEN + 2];
    if (people[i].name[0]) {
      snprintf(line, sizeof(line), "%s", people[i].name);
    } else {
      snprintf(line, sizeof(line), "%c", people[i].initial ? people[i].initial : '?');
    }
    graphics_context_set_text_color(ctx, GColorWhite);
    graphics_draw_text(ctx, line, fonts_get_system_font(FONT_KEY_GOTHIC_18),
                       GRect(area.origin.x + 26, y, area.size.w - 30, 22),
                       GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
    y += 22;
  }
}

// --- windows --------------------------------------------------------------------------
static void detail_cycle(int delta) {
  int total = s_room_count + (s_unplaced_count ? 1 : 0);
  if (total <= 0) return;
  s_detail_index = (s_detail_index + delta + total) % total;  // wraps at both ends
  layer_mark_dirty(s_detail_layer);
}

static void detail_up(ClickRecognizerRef recognizer, void *context) { detail_cycle(-1); }
static void detail_down(ClickRecognizerRef recognizer, void *context) { detail_cycle(1); }

static void detail_click_config(void *context) {
  window_single_click_subscribe(BUTTON_ID_UP, detail_up);
  window_single_click_subscribe(BUTTON_ID_DOWN, detail_down);
}

static void detail_load(Window *window) {
  s_detail_layer = layer_create(layer_get_bounds(window_get_root_layer(window)));
  layer_set_update_proc(s_detail_layer, draw_detail);
  layer_add_child(window_get_root_layer(window), s_detail_layer);
}

static void detail_unload(Window *window) { layer_destroy(s_detail_layer); }

static void plan_select(ClickRecognizerRef recognizer, void *context) {
  if (!s_have_plan || s_room_count == 0) return;
  s_detail_index = 0;
  window_stack_push(s_detail_window, true);
}

// Up/Down on the plan screen ask the phone for a refresh. Cheap, and it is what a
// person reaches for when the age line says the data is old.
static void plan_refresh(ClickRecognizerRef recognizer, void *context) {
  DictionaryIterator *out;
  if (app_message_outbox_begin(&out) == APP_MSG_OK) {
    dict_write_uint8(out, MESSAGE_KEY_STATUS, 1);
    app_message_outbox_send();
  }
  snprintf(s_status, sizeof(s_status), "Refreshing…");
  layer_mark_dirty(s_plan_layer);
}

static void plan_click_config(void *context) {
  window_single_click_subscribe(BUTTON_ID_SELECT, plan_select);
  window_single_click_subscribe(BUTTON_ID_UP, plan_refresh);
  window_single_click_subscribe(BUTTON_ID_DOWN, plan_refresh);
}

static void plan_load(Window *window) {
  s_plan_layer = layer_create(layer_get_bounds(window_get_root_layer(window)));
  layer_set_update_proc(s_plan_layer, draw_plan);
  layer_add_child(window_get_root_layer(window), s_plan_layer);
}

static void plan_unload(Window *window) { layer_destroy(s_plan_layer); }

// --- app message ----------------------------------------------------------------------
static void inbox_received(DictionaryIterator *iterator, void *context) {
  Tuple *status = dict_find(iterator, MESSAGE_KEY_STATUS);
  if (status && status->type == TUPLE_CSTRING) {
    snprintf(s_status, sizeof(s_status), "%s", status->value->cstring);
  }

  Tuple *plan = dict_find(iterator, MESSAGE_KEY_PLAN);
  if (plan && plan->type == TUPLE_BYTE_ARRAY) {
    if (parse_plan(plan->value->data, plan->length)) {
      persist_write_data(PERSIST_PLAN, plan->value->data, plan->length);
    }
  }

  layer_mark_dirty(s_plan_layer);
  if (s_detail_layer) layer_mark_dirty(s_detail_layer);
}

static void inbox_dropped(AppMessageResult reason, void *context) {
  snprintf(s_status, sizeof(s_status), "Message dropped");
  layer_mark_dirty(s_plan_layer);
}

static void load_persisted(void) {
  if (!persist_exists(PERSIST_PLAN)) return;
  // Drawn immediately, before the fetch lands. The app's JS only lives while it is
  // open, so every launch starts with a round trip — showing the last known plan beats
  // showing a spinner, as long as it is labelled, which the status line does.
  uint8_t buffer[2048];
  int length = persist_read_data(PERSIST_PLAN, buffer, sizeof(buffer));
  if (length > 0) {
    parse_plan(buffer, (uint16_t)length);
    snprintf(s_status, sizeof(s_status), "Last known — refreshing…");
  }
}

static void init(void) {
  snprintf(s_status, sizeof(s_status), "Loading…");
  load_persisted();

  s_plan_window = window_create();
  window_set_window_handlers(s_plan_window, (WindowHandlers){ .load = plan_load, .unload = plan_unload });
  window_set_click_config_provider(s_plan_window, plan_click_config);

  s_detail_window = window_create();
  window_set_window_handlers(s_detail_window, (WindowHandlers){ .load = detail_load, .unload = detail_unload });
  window_set_click_config_provider(s_detail_window, detail_click_config);

  app_message_register_inbox_received(inbox_received);
  app_message_register_inbox_dropped(inbox_dropped);
  // Inbox large enough for the whole plan; outbox tiny, because the only thing this
  // ever sends is "please refresh".
  app_message_open(2048, 64);

  window_stack_push(s_plan_window, true);
}

static void deinit(void) {
  window_destroy(s_plan_window);
  window_destroy(s_detail_window);
}

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