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

#include "phone_ui.h"
#include "phone_formatting.h"

#include "applib/fonts/fonts.h"
#include "util/trig.h"
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/kino/kino_reel_pdci.h"
#include "applib/ui/kino/kino_reel/morph_square.h"
#include "applib/ui/kino/kino_reel/transform.h"
#include "applib/ui/kino/kino_reel/unfold.h"
#include "applib/ui/ui.h"
#include "applib/ui/window_private.h"
#include "applib/ui/window_stack.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/kernel_ui.h"
#include "kernel/ui/modals/modal_manager.h"
#include "kernel/ui/system_icons.h"
#include "kernel/pbl_malloc.h"
#include "popups/notifications/notifications_presented_list.h"
#include "resource/resource_ids.auto.h"
#include "services/common/analytics/analytics.h"
#include "services/common/i18n/i18n.h"
#include "services/common/evented_timer.h"
#include "services/common/regular_timer.h"
#include "services/common/light.h"
#include "services/normal/blob_db/ios_notif_pref_db.h"
#include "services/normal/notifications/alerts.h"
#include "services/normal/notifications/notification_constants.h"
#include "services/normal/phone_call.h"
#include "services/normal/timeline/timeline.h"
#include "services/normal/timeline/timeline_actions.h"
#include "services/normal/timeline/timeline_resources.h"
#include "shell/system_theme.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/time/time.h"

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#if CAPABILITY_HAS_VIBE_SCORES
#include "services/normal/vibes/vibe_client.h"
#include "services/normal/vibes/vibe_score.h"
#endif

#define DECLINE_DELAY_MS 2000
#define SMS_REPLY_DELAY_MS 1200
#define SMS_REPLY_IOS_DELAY_MS 600
#define ACCEPT_DELAY_MS 3000
#define CALL_END_DELAY_MS 5000
#define OUTGOING_CALL_DELAY_MS 5000
#define MISSED_CALL_DELAY_MS 180000

#define NAME_BUFFER_LENGTH 32
#define CALL_STATUS_BUFFER_LENGTH 32

#define DEFAULT_COLOR PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)
#define ACCEPT_COLOR PBL_IF_COLOR_ELSE(GColorIslamicGreen, GColorWhite)
#define DECLINE_COLOR PBL_IF_COLOR_ELSE(GColorRed, GColorWhite)

#define TEXT_MARGIN_WIDTH PBL_IF_RECT_ELSE(5, 10)
#define RIGHTSIDE_PADDING 18
#define TEXT_RIGHTSIDE_PADDING \
  PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH, ACTION_BAR_WIDTH + RIGHTSIDE_PADDING - TEXT_MARGIN_WIDTH)
#define ICON_WIDTH 80
// Center icon horizontally in content area (display width minus action bar)
#define ICON_POSITION_X \
  PBL_IF_RECT_ELSE(((DISP_COLS - ACTION_BAR_WIDTH - ICON_WIDTH) / 2), \
                   DISP_ROWS - (ACTION_BAR_WIDTH + RIGHTSIDE_PADDING) - ICON_WIDTH)

#if PBL_ROUND
#define ICON_POSITION_CENTERED_X (DISP_ROWS / 2 - ICON_WIDTH / 2)
#endif

static const int16_t DOT_SIZE = 8;
static const uint32_t UNFOLD_DURATION = 300;
static const int16_t UNFOLD_EXPAND = 8;

static const uint32_t ANIMATION_FRAME_MS = 36;

static const uint32_t SQUARE_ANIMATION_FRAMES = 10;
static const uint32_t BOUNCEBACK_ANIMATION_FRAMES = 2;
static const uint32_t COLOUR_ANIMATION_FRAMES = 4;
static const uint32_t DURATION_APPEAR_ANIMATION_FRAMES = 4;
static const uint32_t ACTION_BAR_DISAPPEAR_ANIMATION_FRAMES = 2;

static const int16_t BOUNCEBACK_DISTANCE = 6;
static const int16_t DURATION_ANIMATION_START_OFFSET = 30;

static const int16_t SINGLE_LINE_BOUND_OFFSET = 5;
static const int16_t SINGLE_LINE_BOUND_HEIGHT = 30;
static const int16_t DOUBLE_LINE_BOUND_OFFSET = 0;

//! Enumeration for the various action bar items in the phone ui
typedef enum {
  PhoneCallActions_None = 0,
  PhoneCallActions_Decline = 1 << 0,
  PhoneCallActions_Answer = 1 << 1,
  PhoneCallActions_Reply = 1 << 2,
} PhoneCallActions;

typedef struct {
  TimelineResourceSize icon_size;
  GPoint icon_pos;
  int16_t caller_id_pos_y;
  int16_t caller_id_height;
  int16_t status_pos_y;
  int16_t status_height;
  bool large_caller_id;
} PhoneStyle;

typedef enum {
  ACCEPTED,
  DECLINED,
  DISCONNECTED
} CallStatus;

typedef struct {
  Window window;
#if !PLATFORM_TINTIN
  struct {
    GColor left;
    GColor right;
    int16_t boundary;
  } bg_color;

  Animation *action_bar_animation;
  Animation *bg_color_animation;
  Animation *call_status_animation;
#endif

  ActionBarLayer action_bar;
  Layer core_ui_container;
  TextLayer caller_id_text_layer;
  TextLayer call_status_text_layer;
  StatusBarLayer status_bar;
  KinoLayer icon_layer;
  KinoReel *current_icon;
  ResourceId current_icon_id;
  bool hid_action_bar;

  GBitmap up_bitmap;
  GBitmap select_bitmap;
  GBitmap down_bitmap;
  ClickHandler up_action;
  ClickHandler select_action;
  ClickHandler down_action;

  const PhoneStyle *style;

  GFont name_font;
  GFont long_name_font;
  GFont status_font;

  char caller_id_text_buf[NAME_BUFFER_LENGTH];
  char call_status_text_buf[CALL_STATUS_BUFFER_LENGTH];
  EventedTimerID call_duration_timer;
  EventedTimerID window_pop_timer;
  time_t call_start_time;
#if CAPABILITY_HAS_VIBE_SCORES
  VibeScore *vibe_score;
#endif
  RegularTimerInfo ring_timer;
  bool show_ongoing_call_ui;

  // Incoming call reply data
  TimelineItem *call_response_item;
  bool waiting_for_action_result;
  bool open_reply_menu_on_pop;
  void *action_handle;
} PhoneUIData;

// Dynamic layout calculation for rectangular displays based on display height
// Content area starts after status bar (16px) and we need to fit:
// - Icon (80x80)
// - Caller ID text area
// - Status text area
// Extra vertical space on larger displays is distributed to improve spacing
#define PHONE_EXTRA_HEIGHT PBL_IF_RECT_ELSE((DISP_ROWS - 168), 0)

// Default style: icon near top, text below
#define PHONE_ICON_POS_Y_DEFAULT PBL_IF_RECT_ELSE((25 + PHONE_EXTRA_HEIGHT / 4), 22)
#define PHONE_CALLER_ID_POS_Y_DEFAULT PBL_IF_RECT_ELSE((102 + PHONE_EXTRA_HEIGHT / 2), 93)
#define PHONE_STATUS_POS_Y_DEFAULT PBL_IF_RECT_ELSE((142 + PHONE_EXTRA_HEIGHT * 3 / 4), 144)

// Large style: more compact icon placement, larger text area
#define PHONE_ICON_POS_Y_LARGE PBL_IF_RECT_ELSE((11 + PHONE_EXTRA_HEIGHT / 4), 22)
#define PHONE_CALLER_ID_POS_Y_LARGE PBL_IF_RECT_ELSE((80 + PHONE_EXTRA_HEIGHT / 2), 88)
#define PHONE_STATUS_POS_Y_LARGE PBL_IF_RECT_ELSE((138 + PHONE_EXTRA_HEIGHT * 3 / 4), 144)

static const PhoneStyle s_phone_style_default = {
  .icon_size = TimelineResourceSizeLarge,
  .icon_pos = { ICON_POSITION_X, PHONE_ICON_POS_Y_DEFAULT },
  .caller_id_pos_y = PHONE_CALLER_ID_POS_Y_DEFAULT,
  .caller_id_height = 50,
  .status_pos_y = PHONE_STATUS_POS_Y_DEFAULT,
  .status_height = 20,
};

static const PhoneStyle s_phone_style_large = {
  .icon_size = PBL_IF_RECT_ELSE(TimelineResourceSizeSmall, TimelineResourceSizeLarge),
  .icon_pos = { ICON_POSITION_X, PHONE_ICON_POS_Y_LARGE },
  .caller_id_pos_y = PHONE_CALLER_ID_POS_Y_LARGE,
  .caller_id_height = 60,
  .status_pos_y = PHONE_STATUS_POS_Y_LARGE,
  .status_height = 20,
  .large_caller_id = true,
};

static const PhoneStyle *s_phone_styles[NumPreferredContentSizes] = {
  [PreferredContentSizeSmall] = &s_phone_style_default,
  [PreferredContentSizeMedium] = &s_phone_style_default,
  [PreferredContentSizeLarge] = &s_phone_style_large,
  [PreferredContentSizeExtraLarge] = &s_phone_style_large,
};

static PhoneUIData *s_phone_ui_data;
static void prv_window_pop(void);
static void prv_window_pop_with_delay(uint32_t delay_ms);
static void prv_action_bar_setup(PhoneCallActions actions);

static void prv_set_answer_window(void) {
  modal_window_push(&s_phone_ui_data->window, ModalPriorityPhone, false /* don't animate */);
}

static void prv_set_reply_window(void) {
  modal_window_push(&s_phone_ui_data->window, ModalPriorityNotification, false /* don't animate */);
}

//! Icon setters.
// This one will make sure the *previously* set icon resource is destroyed.
// The final icon set must still be destroyed alongside the reel.
static void prv_set_icon_resource(TimelineResourceId timeline_res_id) {
  TimelineResourceInfo timeline_res = {
    .res_id = timeline_res_id,
  };
  AppResourceInfo icon_res_info;
  timeline_resources_get_id(&timeline_res, s_phone_ui_data->style->icon_size, &icon_res_info);
  const ResourceId resource = icon_res_info.res_id;

  // Resetting the same icon shouldn't be animated.
  if (resource == s_phone_ui_data->current_icon_id) {
    return;
  }

  KinoReel *new_image = kino_reel_create_with_resource(resource);
#if !PLATFORM_TINTIN
  KinoReel *old_image = s_phone_ui_data->current_icon;
  kino_layer_pause(&s_phone_ui_data->icon_layer);

  KinoReel *icon_reel = kino_reel_morph_square_create(old_image, true);
  kino_reel_transform_set_to_reel(icon_reel, new_image, false);
  kino_reel_transform_set_transform_duration(icon_reel,
                                             SQUARE_ANIMATION_FRAMES * ANIMATION_FRAME_MS);
  kino_layer_set_reel(&s_phone_ui_data->icon_layer, icon_reel, true);
  kino_layer_play(&s_phone_ui_data->icon_layer);
  s_phone_ui_data->current_icon = new_image;
  s_phone_ui_data->current_icon_id = resource;
#else
  kino_layer_set_reel(&s_phone_ui_data->icon_layer, new_image, true);
  s_phone_ui_data->current_icon = new_image;
  s_phone_ui_data->current_icon_id = resource;
#endif
}

// This will do the wrong thing if called after the action bar is removed, due to the absolute
// coordinate scheme being offset.
static void prv_unfold_icon_resource(TimelineResourceId timeline_res_id) {
  TimelineResourceInfo timeline_res = {
    .res_id = timeline_res_id,
  };
  AppResourceInfo icon_res_info;
  timeline_resources_get_id(&timeline_res, s_phone_ui_data->style->icon_size, &icon_res_info);
  const ResourceId resource = icon_res_info.res_id;

  KinoReel *image = kino_reel_create_with_resource(resource);
#if !PLATFORM_TINTIN
  GRect layer_frame = s_phone_ui_data->icon_layer.layer.frame;
  GSize size = kino_reel_get_size(image);
  GRect icon_from = {
      .origin.x = layer_frame.origin.x + (layer_frame.size.w - DOT_SIZE) / 2,
      .origin.y = layer_frame.origin.y + (layer_frame.size.h - DOT_SIZE) / 2,
      .size = { DOT_SIZE, DOT_SIZE },
  };
  GRect icon_to = {
      .origin.x = layer_frame.origin.x + (layer_frame.size.w - size.w) / 2,
      .origin.y = layer_frame.origin.y + (layer_frame.size.h - size.h) / 2,
      .size = size,
  };
  KinoReel *kino_reel = kino_reel_unfold_create(image, false, layer_frame, 0,
                                                UNFOLD_DEFAULT_NUM_DELAY_GROUPS,
                                                UNFOLD_DEFAULT_GROUP_DELAY);
  kino_reel_transform_set_from_frame(kino_reel, icon_from);
  kino_reel_transform_set_to_frame(kino_reel, icon_to);
  kino_reel_transform_set_transform_duration(kino_reel, UNFOLD_DURATION);
  kino_reel_scale_segmented_set_deflate_effect(kino_reel, UNFOLD_EXPAND);
  kino_layer_set_reel(&s_phone_ui_data->icon_layer, kino_reel, true);
  kino_layer_play(&s_phone_ui_data->icon_layer);
  s_phone_ui_data->current_icon = image;
  s_phone_ui_data->current_icon_id = resource;
#else
  kino_layer_set_reel(&s_phone_ui_data->icon_layer, image, true);
  s_phone_ui_data->current_icon = image;
  s_phone_ui_data->current_icon_id = resource;
#endif
}

#if !PLATFORM_TINTIN
static void prv_update_color_boundary(void *subject, int16_t boundary) {
  s_phone_ui_data->bg_color.boundary = boundary;
  layer_mark_dirty(&s_phone_ui_data->window.layer);
}

static int16_t prv_get_color_boundary(void *subject) {
  return s_phone_ui_data->bg_color.boundary;
}

static const PropertyAnimationImplementation s_color_slide_animation_impl = {
    .base = {
        .update = (AnimationUpdateImplementation)property_animation_update_int16,
    },
    .accessors = {
        .getter = { .int16 = (const Int16Getter)prv_get_color_boundary },
        .setter = { .int16 = (const Int16Setter)prv_update_color_boundary, },
    },
};
#endif

static void prv_set_window_color(GColor color, bool left_to_right) {
#if !PLATFORM_TINTIN
  Animation *color_animation;
  int16_t width = s_phone_ui_data->window.layer.bounds.size.w;
  int16_t zero = 0;

  animation_unschedule(s_phone_ui_data->bg_color_animation);

  // Take whichever side is more complete as our starting colour.
  if (s_phone_ui_data->bg_color.boundary > width / 2) {
    s_phone_ui_data->bg_color.right = s_phone_ui_data->bg_color.left;
  } else {
    s_phone_ui_data->bg_color.left = s_phone_ui_data->bg_color.right;
  }

  if (left_to_right) {
    s_phone_ui_data->bg_color.left = color;
    color_animation = property_animation_get_animation(
        property_animation_create(&s_color_slide_animation_impl, NULL, &zero, &width));
    s_phone_ui_data->bg_color.boundary = 0;
  } else {
    s_phone_ui_data->bg_color.right = color;
    color_animation = property_animation_get_animation(
        property_animation_create(&s_color_slide_animation_impl, NULL, &width, &zero));
    s_phone_ui_data->bg_color.boundary = width;
  }
  s_phone_ui_data->bg_color_animation = color_animation;
  animation_set_duration(color_animation, COLOUR_ANIMATION_FRAMES * ANIMATION_FRAME_MS);
  animation_set_curve(color_animation, AnimationCurveEaseIn);
  animation_schedule(color_animation);
#else
  layer_mark_dirty(&s_phone_ui_data->window.layer);
#endif
}

// Names can sometimes actually be phone numbers. We're assuming that phone numbers will always
// match /^[() +0-9-]+$/
static bool prv_is_string_a_phone_number(const char *name) {
  size_t length = strlen(name);

  // Blocked/unknown numbers on Android start with a '-'
  if ((name[0] == '-') || (length == 0)) {
    return false;
  }

  for (size_t i = 0; i < length; ++i) {
    unsigned char chr = name[i];
    if ((chr != '(') && (chr != ')') && (chr != '+') && (chr != ' ') && (chr != '-') && (chr != '.')
        && !isdigit(chr)) {
      return false;
    }
  }
  return true;
}

static bool prv_has_long_name(GFont font) {
  // Figure out if it's a "long name"
  // (i.e. one that won't fit a single line at the default font size).
  const int16_t fudge_some_pixels = 30;
  const bool line_contains_newline = (strchr(s_phone_ui_data->caller_id_text_buf, '\n') != NULL);
  int16_t test_width = s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w
                        + fudge_some_pixels;
  GSize text_size = graphics_text_layout_get_max_used_size(
      kernel_ui_get_graphics_context(),
      s_phone_ui_data->caller_id_text_buf,
      font,
      GRect(0, 0, test_width, SINGLE_LINE_BOUND_HEIGHT),
      s_phone_ui_data->caller_id_text_layer.overflow_mode,
      GTextAlignmentLeft, NULL);
  return (text_size.w > s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w) ||
          line_contains_newline;
}

//! Text setters
static void prv_set_caller_id_text(PebblePhoneCaller *caller) {
  if (!caller->name && !caller->number) {
    return;
  }

  const char *caller_text = caller->name ?: caller->number;
  // Occasionally a name comes in as a number, and vice versa
  const bool is_phone_number = prv_is_string_a_phone_number(caller_text);
  GFont caller_id_font = NULL;
  int lines = 1;
  if (is_phone_number) {
    phone_format_phone_number(caller_text, s_phone_ui_data->caller_id_text_buf, NAME_BUFFER_LENGTH);
    text_layer_set_overflow_mode(&s_phone_ui_data->caller_id_text_layer, GTextOverflowModeWordWrap);
  } else {
    phone_format_caller_name(caller_text, s_phone_ui_data->caller_id_text_buf, NAME_BUFFER_LENGTH);
  }

  if (s_phone_ui_data->style->large_caller_id) {
    caller_id_font = s_phone_ui_data->name_font;
    lines++;
  } else if (prv_has_long_name(s_phone_ui_data->name_font)) {
    caller_id_font = s_phone_ui_data->long_name_font;
    lines++;
  } else {
    caller_id_font = s_phone_ui_data->name_font;
  }

  text_layer_set_font(&s_phone_ui_data->caller_id_text_layer, caller_id_font);
  if (lines == 1) {
    s_phone_ui_data->caller_id_text_layer.layer.bounds.origin.y = SINGLE_LINE_BOUND_OFFSET;
  } else {
    s_phone_ui_data->caller_id_text_layer.layer.bounds.origin.y = DOUBLE_LINE_BOUND_OFFSET;
  }
  s_phone_ui_data->caller_id_text_layer.layer.bounds.size.h =
      lines * fonts_get_font_height(caller_id_font);
  text_layer_set_text(&s_phone_ui_data->caller_id_text_layer, s_phone_ui_data->caller_id_text_buf);
}

//! Window background rendering
static void prv_window_update_proc(Layer *layer, GContext *ctx) {
#if !PLATFORM_TINTIN
  graphics_context_set_fill_color(ctx, s_phone_ui_data->bg_color.left);
  graphics_fill_rect(ctx, &GRect(0, 0, s_phone_ui_data->bg_color.boundary, layer->bounds.size.h));
  graphics_context_set_fill_color(ctx, s_phone_ui_data->bg_color.right);
  graphics_fill_rect(ctx, &GRect(s_phone_ui_data->bg_color.boundary, 0, layer->bounds.size.w,
                                 layer->bounds.size.h));
#else
  graphics_context_set_fill_color(ctx, DEFAULT_COLOR);
  graphics_fill_rect(ctx, &layer->bounds);
#endif
}

//! Ring functionality
static void prv_ring(void *unused) {
  PBL_LOG(LOG_LEVEL_DEBUG, "RING");
  if (alerts_should_vibrate_for_type(AlertPhoneCall)) {
#if CAPABILITY_HAS_VIBE_SCORES
    if (!s_phone_ui_data || !s_phone_ui_data->vibe_score) {
      // There is a mutex-related issue that can appear where the timer callback will execute after
      // phone_ui cancels the timer and frees the vibe_score / s_phone_ui_data. Thus, bail early
      // if we detect this bad state.
      // See PBL-35548
      return;
    }
    vibe_score_do_vibe(s_phone_ui_data->vibe_score);
#else
    vibes_long_pulse();
#endif
  }
  if (alerts_should_enable_backlight_for_type(AlertPhoneCall)) {
    light_enable_interaction();
  }
}

static void prv_start_ringing(void) {
  alerts_incoming_alert_analytics();
  s_phone_ui_data->ring_timer = (const RegularTimerInfo) {
    .cb = prv_ring,
  };
  unsigned int vibe_repeat_interval_sec;
#if CAPABILITY_HAS_VIBE_SCORES
  s_phone_ui_data->vibe_score = vibe_client_get_score(VibeClient_PhoneCalls);
  if (!s_phone_ui_data->vibe_score) {
    return;
  }
  unsigned int vibe_interval_ms = vibe_score_get_duration_ms(s_phone_ui_data->vibe_score) +
      vibe_score_get_repeat_delay_ms(s_phone_ui_data->vibe_score);
  vibe_repeat_interval_sec = DIVIDE_CEIL(vibe_interval_ms, MS_PER_SECOND);
#else
  vibe_repeat_interval_sec = 2;
#endif
  prv_ring(NULL);
  regular_timer_add_multisecond_callback(&s_phone_ui_data->ring_timer, vibe_repeat_interval_sec);
}

static void prv_stop_ringing(void) {
  regular_timer_remove_callback(&s_phone_ui_data->ring_timer);
#if CAPABILITY_HAS_VIBE_SCORES
  if (s_phone_ui_data->vibe_score) {
    vibe_score_destroy(s_phone_ui_data->vibe_score);
    s_phone_ui_data->vibe_score = NULL;
  }
#endif
  vibes_cancel();
}


//! Call duration related functions
static void prv_show_call_status(void) {
  layer_set_hidden(&s_phone_ui_data->call_status_text_layer.layer, false);
#if !PLATFORM_TINTIN
  s_phone_ui_data->call_status_text_layer.layer.bounds.origin.y = DURATION_ANIMATION_START_OFFSET;
  Animation *upward = property_animation_get_animation(
      property_animation_create_bounds_origin(&s_phone_ui_data->call_status_text_layer.layer,
                                              &GPoint(0, DURATION_ANIMATION_START_OFFSET),
                                              &GPoint(0, -BOUNCEBACK_DISTANCE)));
  animation_set_curve(upward, AnimationCurveEaseIn);
  animation_set_duration(upward, DURATION_APPEAR_ANIMATION_FRAMES * ANIMATION_FRAME_MS);

  Animation *bounceback = property_animation_get_animation(
      property_animation_create_bounds_origin(&s_phone_ui_data->call_status_text_layer.layer,
                                              &GPoint(0, -BOUNCEBACK_DISTANCE),
                                              &GPointZero));
  animation_set_curve(bounceback, AnimationCurveEaseOut);
  animation_set_duration(bounceback, BOUNCEBACK_ANIMATION_FRAMES * ANIMATION_FRAME_MS);

  Animation *animation = animation_sequence_create(upward, bounceback, NULL);
  s_phone_ui_data->call_status_animation = animation;
  animation_schedule(animation);
#else
  s_phone_ui_data->call_status_text_layer.layer.bounds.origin = GPointZero;
#endif
}

static void prv_update_call_time(void *unused) {
  if (s_phone_ui_data == NULL) {
    return;
  }

  if (layer_get_hidden(&s_phone_ui_data->call_status_text_layer.layer)) {
    prv_show_call_status();
  }
  const time_t duration = rtc_get_time() - s_phone_ui_data->call_start_time;
  const int seconds = duration % SECONDS_PER_MINUTE;
  int minutes = (duration - seconds) / SECONDS_PER_MINUTE;
  if (minutes >= MINUTES_PER_HOUR) {
    const int hours = minutes / MINUTES_PER_HOUR;
    minutes = minutes % MINUTES_PER_HOUR;
    sniprintf(s_phone_ui_data->call_status_text_buf, CALL_STATUS_BUFFER_LENGTH,
        "%u:%02u:%02u", hours, minutes, seconds);
  } else {
    sniprintf(s_phone_ui_data->call_status_text_buf, CALL_STATUS_BUFFER_LENGTH,
        "%u:%02u", minutes, seconds);
  }
  text_layer_set_text(&s_phone_ui_data->call_status_text_layer,
                      s_phone_ui_data->call_status_text_buf);
}

static void prv_start_call_duration_timer(void) {
  if (s_phone_ui_data->call_start_time == 0) {
    s_phone_ui_data->call_start_time = rtc_get_time();
  }

  s_phone_ui_data->call_duration_timer = evented_timer_register(
      1000, true /* repeating */, prv_update_call_time, NULL);

  // Update call time immediately
  prv_update_call_time(NULL);
}

static void prv_stop_call_duration_timer(void) {
  evented_timer_cancel(s_phone_ui_data->call_duration_timer);
  s_phone_ui_data->call_duration_timer = EVENTED_TIMER_INVALID_ID;
}

static void prv_set_status_text(CallStatus status) {
  if (status == ACCEPTED) {
    i18n_get_with_buffer("Call Accepted", s_phone_ui_data->call_status_text_buf,
                         CALL_STATUS_BUFFER_LENGTH);
  } else if (status == DISCONNECTED) {
    i18n_get_with_buffer("Disconnected", s_phone_ui_data->call_status_text_buf,
                         CALL_STATUS_BUFFER_LENGTH);
  } else {
    if (s_phone_ui_data->call_start_time) {
      i18n_get_with_buffer("Call Ended", s_phone_ui_data->call_status_text_buf,
                           CALL_STATUS_BUFFER_LENGTH);
    } else {
      i18n_get_with_buffer("Call Declined", s_phone_ui_data->call_status_text_buf,
                           CALL_STATUS_BUFFER_LENGTH);
    }
  }

  text_layer_set_text(&s_phone_ui_data->call_status_text_layer,
                      s_phone_ui_data->call_status_text_buf);
  prv_show_call_status();
}

// Handles cleanup when the SMS reply menu closes
static void prv_action_menu_did_close(ActionMenu *action_menu, const ActionMenuItem *item,
                                      void *context) {
  timeline_item_destroy(context);
}

static void prv_ancs_response_action_result_handler(bool success, void *timeline_item) {
  timeline_item_destroy(timeline_item);

  // We got the action result for our iOS reply. We can now close the phone ui window because we
  // are displaying the action menu (but only if the original window hasn't already been torn down)
  if (s_phone_ui_data && s_phone_ui_data->waiting_for_action_result) {
    prv_window_pop();
  }
}

// Creates a new reply action menu and pushes it with notification modal priority
static void prv_open_reply_action_menu(void *unused) {
  // Drop the call window priority so we properly animate in the menu
  prv_set_reply_window();

  // The timeline item will be cleaned up by the action menu/action callbacks
  TimelineItem *item = s_phone_ui_data->call_response_item;
  s_phone_ui_data->call_response_item = NULL;

  TimelineItemAction *reply_action = timeline_item_find_reply_action(item);

  if (!reply_action) {
    return;
  }

  switch (reply_action->type) {
    case TimelineItemActionTypeResponse:
      timeline_actions_push_response_menu(item, reply_action, SMS_REPLY_COLOR,
                                          prv_action_menu_did_close,
                                          modal_manager_get_window_stack(ModalPriorityNotification),
                                          TimelineItemActionSourcePhoneUi,
                                          true /* standalone_reply */);
      break;
    case TimelineItemActionTypeAncsResponse:
      // Mark this window so we know to pop it when we get a response
      s_phone_ui_data->waiting_for_action_result = true;

      // Kick off the reply action automatically - we will pop the phone ui once we get an action
      // result and can show the action menu
      timeline_actions_invoke_action(reply_action, item, prv_ancs_response_action_result_handler,
                                     item);
      break;
    default:
      break;
  }
}

//! Action bar click handlers
static void prv_answer_click_handler(ClickRecognizerRef recognizer, void *unused) {
  prv_stop_ringing();
  phone_call_answer();

  // This must be called before prv_set_status_text, otherwise the text will not be centered
  prv_action_bar_setup(PhoneCallActions_None);
  prv_set_window_color(ACCEPT_COLOR, false);
  prv_set_icon_resource(TIMELINE_RESOURCE_DURING_PHONE_CALL);

  if (s_phone_ui_data->show_ongoing_call_ui) {
    prv_start_call_duration_timer();
  } else {
    prv_set_status_text(ACCEPTED);
    prv_window_pop_with_delay(ACCEPT_DELAY_MS);
  }

  prv_set_answer_window();
}

static void prv_decline_call(void) {
  prv_stop_ringing();
  phone_call_decline();

  prv_stop_call_duration_timer();
  prv_set_icon_resource(TIMELINE_RESOURCE_DISMISSED_PHONE_CALL);
  prv_set_window_color(DECLINE_COLOR, true);

  // This must be called before prv_set_status_text, otherwise the text will not be centered
  prv_action_bar_setup(PhoneCallActions_None);
  prv_set_status_text(DECLINED);
}

static void prv_decline_click_handler(ClickRecognizerRef recognizer, void *unused) {
  prv_decline_call();
  prv_window_pop_with_delay(DECLINE_DELAY_MS);
}

static void prv_sms_reply_click_handler(ClickRecognizerRef recognizer, void *unused) {
  prv_decline_call();
  s_phone_ui_data->open_reply_menu_on_pop = true;

  const TimelineItemAction *reply_action =
      timeline_item_find_reply_action(s_phone_ui_data->call_response_item);

  switch (reply_action->type) {
    case TimelineItemActionTypeResponse:
      // On Android, we just open the action menu at the same time we pop the window
      prv_window_pop_with_delay(SMS_REPLY_DELAY_MS);
      break;
    case TimelineItemActionTypeAncsResponse:
      // On iOS, show the "Call Declined" animation and send the AncsResponse message shortly after
      // We hold the phone UI up until timeline_actions responds or another call comes in
      s_phone_ui_data->window_pop_timer = evented_timer_register(SMS_REPLY_IOS_DELAY_MS,
          false /* repeating */, prv_open_reply_action_menu, NULL);
      break;
    default:
      break;
  }
}

static void prv_pop_click_handler(ClickRecognizerRef recognizer, void *unused) {
  analytics_inc(ANALYTICS_DEVICE_METRIC_PHONE_CALL_POP_COUNT, AnalyticsClient_System);
  prv_stop_ringing();
  prv_window_pop();
}

//! Action bar animation
static void prv_hide_action_bar(void) {
  if (s_phone_ui_data->hid_action_bar) {
    return;
  }
  s_phone_ui_data->hid_action_bar = true;

#if !PLATFORM_TINTIN
  const GRect window_bounds = s_phone_ui_data->window.layer.bounds;
  GRect offscreen = GRect(window_bounds.size.w, 0, PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH, 0),
                          window_bounds.size.h);
  Animation *action_bar_animation = property_animation_get_animation(
      property_animation_create_layer_frame(&s_phone_ui_data->action_bar.layer, NULL, &offscreen));
  animation_set_duration(action_bar_animation, ACTION_BAR_DISAPPEAR_ANIMATION_FRAMES
                                               * ANIMATION_FRAME_MS);
  animation_set_curve(action_bar_animation, AnimationCurveEaseIn);
  GPoint overshoot = GPoint(PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH / 2, 0) + BOUNCEBACK_DISTANCE, 0);
  Animation *ui_movement = property_animation_get_animation(
      property_animation_create_bounds_origin(&s_phone_ui_data->core_ui_container, NULL,
                                              &overshoot));
  animation_set_curve(ui_movement, AnimationCurveEaseIn);
  animation_set_duration(ui_movement, 3 * ANIMATION_FRAME_MS);
  Animation *ui_bounceback = property_animation_get_animation(
      property_animation_create_bounds_origin(
        &s_phone_ui_data->core_ui_container, &overshoot,
        &GPoint(PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH / 2, 0), 0)));
  animation_set_curve(ui_bounceback, AnimationCurveEaseOut);
  animation_set_duration(ui_bounceback, 2 * ANIMATION_FRAME_MS);
  Animation *ui_animation = animation_sequence_create(ui_movement, ui_bounceback, NULL);
  Animation *combined = animation_spawn_create(action_bar_animation, ui_animation, NULL);
  s_phone_ui_data->action_bar_animation = combined;
  animation_schedule(combined);
#if PBL_ROUND
  // Extend the bounds to center the call text when the action bar is removed
  s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w += TEXT_RIGHTSIDE_PADDING;
  text_layer_set_text_alignment(&s_phone_ui_data->caller_id_text_layer, GTextAlignmentCenter);
  s_phone_ui_data->call_status_text_layer.layer.bounds.size.w += TEXT_RIGHTSIDE_PADDING;
  text_layer_set_text_alignment(&s_phone_ui_data->call_status_text_layer, GTextAlignmentCenter);
  // Center the kino icon
  s_phone_ui_data->icon_layer.layer.frame.origin.x = ICON_POSITION_CENTERED_X;
#endif

#else
  const GRect container_bounds = s_phone_ui_data->core_ui_container.bounds;
  const GRect onscreen = GRect(ACTION_BAR_WIDTH / 2, 0,
                               container_bounds.size.w, container_bounds.size.h);
  layer_set_hidden(&s_phone_ui_data->action_bar.layer, true /* hide */);
  layer_set_bounds(&s_phone_ui_data->core_ui_container, &onscreen);
#endif
}

//! Action bar setup functions
static void prv_set_action_bar_icon(ButtonId button, ResourceId resource, GBitmap *storage) {
  if (resource == RESOURCE_ID_INVALID) {
    action_bar_layer_clear_icon(&s_phone_ui_data->action_bar, button);
    return;
  }

  gbitmap_deinit(storage);
  gbitmap_init_with_resource_system(storage, SYSTEM_APP, resource);
  action_bar_layer_set_icon(&s_phone_ui_data->action_bar, button, storage);
}

// Returns the appropriate app id for the given phone number and call source
static const char *prv_get_app_id(const char *number, PhoneCallSource source) {
  if (!number) {
    return NULL;
  }

  // Select appropriate app id
  switch (source) {
    case PhoneCallSource_PP:
      // We require the this to be a valid number when coming from PP
      if (prv_is_string_a_phone_number(number)) {
        return ANDROID_PHONE_KEY;
      }
      break;
    case PhoneCallSource_ANCS:
    case PhoneCallSource_ANCS_Legacy:
      return IOS_PHONE_KEY;
      break;
  }

  return NULL;
}

// Checks for the existance of a call reply action in the notif pref db and loads it into
// a timeline item
static bool prv_load_sms_reply_action(const char *number, PhoneCallSource source) {
  const char *app_id = prv_get_app_id(number, source);

  if (!app_id) {
    return false;
  }

  // Load actions from prefs db and determine if we have an SMS reply option
  iOSNotifPrefs *notif_prefs = ios_notif_pref_db_get_prefs((uint8_t *)app_id, strlen(app_id));
  if (!notif_prefs) {
    return false;
  }

  // Add attributes to the timeline item for contact lookup
  AttributeList attributes = {};

  attribute_list_add_cstring(&attributes, AttributeIdSender, number);
  attribute_list_add_cstring(&attributes, AttributeIdiOSAppIdentifier, app_id);

  TimelineItem *item = timeline_item_create_with_attributes(0, 0, TimelineItemTypeNotification,
                                                            LayoutIdUnknown,
                                                            &attributes,
                                                            &notif_prefs->action_group);
  bool rv = false;

  // Make sure we have a reply action (this properly handles NULL items)
  const TimelineItemAction *reply_action = timeline_item_find_reply_action(item);
  if (reply_action) {
    s_phone_ui_data->call_response_item = item;
    rv = true;

    if (reply_action->type == TimelineItemActionTypeResponse) {
      item->header.id = (Uuid)UUID_SEND_SMS;
    }
  } else {
    timeline_item_destroy(item);
  }

  attribute_list_destroy_list(&attributes);
  ios_notif_pref_db_free_prefs(notif_prefs);

  return rv;
}

//! Action bar click configurations
static void prv_click_config_provider(void *context) {
  if (s_phone_ui_data->up_action) {
    window_single_click_subscribe(BUTTON_ID_UP, s_phone_ui_data->up_action);
  }

  if (s_phone_ui_data->select_action) {
    window_single_click_subscribe(BUTTON_ID_SELECT, s_phone_ui_data->select_action);
  }

  if (s_phone_ui_data->down_action) {
    window_single_click_subscribe(BUTTON_ID_DOWN, s_phone_ui_data->down_action);
  }

  window_single_click_subscribe(BUTTON_ID_BACK, prv_pop_click_handler);
}

static void prv_action_bar_setup(PhoneCallActions actions) {
  s_phone_ui_data->up_action = NULL;
  s_phone_ui_data->select_action = NULL;
  s_phone_ui_data->down_action = NULL;

  ResourceId up_icon = RESOURCE_ID_INVALID;
  ResourceId select_icon = RESOURCE_ID_INVALID;
  ResourceId down_icon = RESOURCE_ID_INVALID;

  if (actions) {
    if (actions & PhoneCallActions_Answer) {
      s_phone_ui_data->up_action = prv_answer_click_handler;
      up_icon = RESOURCE_ID_ACTION_BAR_ICON_CHECK;
    }

    if (actions & PhoneCallActions_Reply) {
      // Move to top if that place isn't taken
      if (!s_phone_ui_data->up_action) {
        s_phone_ui_data->up_action = prv_sms_reply_click_handler;
        up_icon = RESOURCE_ID_ACTION_BAR_ICON_SMS;
      } else {
        s_phone_ui_data->select_action = prv_sms_reply_click_handler;
        select_icon = RESOURCE_ID_ACTION_BAR_ICON_SMS;
      }
    }

    if (actions & PhoneCallActions_Decline) {
      s_phone_ui_data->down_action = prv_decline_click_handler;
      down_icon = RESOURCE_ID_ACTION_BAR_ICON_X;
    }

    prv_set_action_bar_icon(BUTTON_ID_UP, up_icon, &s_phone_ui_data->up_bitmap);
    prv_set_action_bar_icon(BUTTON_ID_SELECT, select_icon, &s_phone_ui_data->select_bitmap);
    prv_set_action_bar_icon(BUTTON_ID_DOWN, down_icon, &s_phone_ui_data->down_bitmap);
  } else {
    prv_hide_action_bar();
  }

  action_bar_layer_set_click_config_provider(&s_phone_ui_data->action_bar,
                                             prv_click_config_provider);
}

//! Put the correct data in the 3 text fields
static void prv_display_caller_info(PebblePhoneCaller *caller) {
  prv_set_caller_id_text(caller);
}

static void prv_phone_ui_deinit(void) {
  if (s_phone_ui_data == NULL) {
    return;
  }

  kino_layer_pause(&s_phone_ui_data->icon_layer);
  kino_layer_deinit(&s_phone_ui_data->icon_layer);
#if !PLATFORM_TINTIN
  // The reels will destroy intermediate images, but not the one currently on screen
  // clean it up here.  Note that we don't have to do this on Tintin/Bianca as we
  // do not create an intermediary reel for animating.
  kino_reel_destroy(s_phone_ui_data->current_icon);

  animation_unschedule(s_phone_ui_data->bg_color_animation);
  animation_unschedule(s_phone_ui_data->action_bar_animation);
  animation_unschedule(s_phone_ui_data->call_status_animation);
#endif
  s_phone_ui_data->current_icon = NULL;
  s_phone_ui_data->current_icon_id = 0;

  status_bar_layer_deinit(&s_phone_ui_data->status_bar);
  gbitmap_deinit(&s_phone_ui_data->up_bitmap);
  gbitmap_deinit(&s_phone_ui_data->select_bitmap);
  gbitmap_deinit(&s_phone_ui_data->down_bitmap);

  text_layer_deinit(&s_phone_ui_data->call_status_text_layer);
  text_layer_deinit(&s_phone_ui_data->caller_id_text_layer);

  evented_timer_cancel(s_phone_ui_data->call_duration_timer);
  evented_timer_cancel(s_phone_ui_data->window_pop_timer);

  action_bar_layer_deinit(&s_phone_ui_data->action_bar);

  i18n_free_all(s_phone_ui_data);

  prv_stop_ringing();

  window_deinit(&s_phone_ui_data->window);

  timeline_item_destroy(s_phone_ui_data->call_response_item);

  kernel_free(s_phone_ui_data);

  s_phone_ui_data = NULL;
}

static void prv_handle_window_unload(Window *window) {
  prv_phone_ui_deinit();
}

//! Window destroy functions
//! Currently only 1 call window can exist at a time
static void prv_window_pop(void) {
  if (s_phone_ui_data == NULL) {
    // Check to make sure we didn't get popped already.
    // There could possibly be 2 of these callback in the queue at time if this is called right after
    // a prv_pop_with_delay
    return;
  }

  if (s_phone_ui_data->open_reply_menu_on_pop) {
    prv_open_reply_action_menu(NULL);
  }

  window_stack_remove(&s_phone_ui_data->window, true /* animated */);

  // The window_stack_remove() call should run the unload handler (which deinits the ui),
  // but in the rare case that the window never loaded (i.e. a higher priority modal was up)
  // then we could leak the phone_ui data and assert on the next phone call.
  // Deinit again to cover this case (will be a no-op) if the window was alredy deinited.
  prv_phone_ui_deinit();
}

static void prv_window_pop_cb(void *unused) {
  s_phone_ui_data->window_pop_timer = EVENTED_TIMER_INVALID_ID;
  prv_window_pop();
}

static void prv_window_pop_with_delay(uint32_t delay_ms) {
  s_phone_ui_data->window_pop_timer = evented_timer_register(
      delay_ms, false /* repeating */, prv_window_pop_cb, NULL);
}

//! Window setup
//! Currently only 1 call window can exist at a time
static void prv_phone_ui_init(void) {
  PBL_ASSERTN(s_phone_ui_data == NULL);

  s_phone_ui_data = kernel_zalloc_check(sizeof(PhoneUIData));
  s_phone_ui_data->hid_action_bar = false;

  s_phone_ui_data->style = s_phone_styles[system_theme_get_content_size()];

  const PhoneStyle *style = s_phone_ui_data->style;
  s_phone_ui_data->name_font = system_theme_get_font(TextStyleFont_Title);
  s_phone_ui_data->long_name_font =
      system_theme_get_font(PBL_IF_RECT_ELSE(TextStyleFont_Header, TextStyleFont_Title));
  s_phone_ui_data->status_font =
      PBL_IF_RECT_ELSE(system_theme_get_font(TextStyleFont_Header),
                       fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));

  Window *window = &s_phone_ui_data->window;

  window_init(window, WINDOW_NAME("Phone"));
  window_set_status_bar_icon(window, (GBitmap*)&s_status_icon_phone_bitmap);
  layer_set_update_proc(&window->layer, prv_window_update_proc);
  window_set_window_handlers(&s_phone_ui_data->window, &(WindowHandlers) {
    .unload = prv_handle_window_unload,
  });
  window_set_overrides_back_button(window, true);
#if !PLATFORM_TINTIN
  s_phone_ui_data->bg_color.left = DEFAULT_COLOR;
  s_phone_ui_data->bg_color.right = DEFAULT_COLOR;
  s_phone_ui_data->bg_color.boundary = 0;
#endif

  const int16_t width = window->layer.bounds.size.w - (TEXT_MARGIN_WIDTH * 2);

  // Container layer
  layer_init(&s_phone_ui_data->core_ui_container, &window->layer.bounds);
  layer_add_child(&window->layer, &s_phone_ui_data->core_ui_container);

  // Status bar
  status_bar_layer_init(&s_phone_ui_data->status_bar);
  layer_set_frame(&s_phone_ui_data->status_bar.layer,
                  &GRect(0, 0, window->layer.bounds.size.w - PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH, 0),
                         STATUS_BAR_LAYER_HEIGHT));
  status_bar_layer_set_colors(&s_phone_ui_data->status_bar,
                              PBL_IF_COLOR_ELSE(GColorClear, GColorWhite),
                              GColorBlack);
  layer_add_child(&s_phone_ui_data->core_ui_container, &s_phone_ui_data->status_bar.layer);

  // Icon
  kino_layer_init(&s_phone_ui_data->icon_layer,
                  &(GRect){ style->icon_pos, { ICON_WIDTH, ICON_WIDTH } });
  kino_layer_set_alignment(&s_phone_ui_data->icon_layer, GAlignCenter);
  layer_add_child(&s_phone_ui_data->core_ui_container, &s_phone_ui_data->icon_layer.layer);

  // Caller ID text
  const GRect caller_id_text_rect = GRect(TEXT_MARGIN_WIDTH, style->caller_id_pos_y,
                                          width, style->caller_id_height);
  text_layer_init_with_parameters(&s_phone_ui_data->caller_id_text_layer,
                                  &caller_id_text_rect, NULL, NULL,
                                  GColorBlack,
                                  PBL_IF_COLOR_ELSE(GColorClear, GColorWhite),
                                  PBL_IF_RECT_ELSE(GTextAlignmentCenter, GTextAlignmentRight),
                                  GTextOverflowModeTrailingEllipsis);
  layer_add_child(&s_phone_ui_data->core_ui_container,
                  &s_phone_ui_data->caller_id_text_layer.layer);
  // Shrink the bounds but not the frame size to allow for centering when action bar removed
  s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w = width - TEXT_RIGHTSIDE_PADDING;

  // Status text
  const GRect call_status_text_rect = GRect(TEXT_MARGIN_WIDTH, style->status_pos_y,
                                            width, style->status_height);
  text_layer_init_with_parameters(&s_phone_ui_data->call_status_text_layer,
                                  &call_status_text_rect, NULL, s_phone_ui_data->status_font,
                                  GColorBlack,
                                  PBL_IF_COLOR_ELSE(GColorClear, GColorWhite),
                                  PBL_IF_RECT_ELSE(GTextAlignmentCenter, GTextAlignmentRight),
                                  GTextOverflowModeTrailingEllipsis);
  layer_set_hidden(&s_phone_ui_data->call_status_text_layer.layer, true);
  layer_set_clips(&s_phone_ui_data->call_status_text_layer.layer, false);
  layer_add_child(&s_phone_ui_data->core_ui_container,
                  &s_phone_ui_data->call_status_text_layer.layer);
  // Shrink the bounds but not the frame size to allow for centering when action bar removed
  s_phone_ui_data->call_status_text_layer.layer.bounds.size.w = width - TEXT_RIGHTSIDE_PADDING;

  // Action bar
  action_bar_layer_init(&s_phone_ui_data->action_bar);
  action_bar_layer_add_to_window(&s_phone_ui_data->action_bar, window);

  modal_window_push(window, ModalPriorityCritical, true /* animated */);
}

static bool prv_check_popups_are_blocked(void) {
  if (launcher_popups_are_blocked()) {
    PBL_LOG(LOG_LEVEL_INFO, "Ignoring call event. Popups are blocked");
    return true;
  }
  return false;
}

//!
//! API for updating / creating the phone UI
//!
void phone_ui_handle_incoming_call(PebblePhoneCaller *caller, bool can_answer,
                                   bool show_ongoing_call_ui, PhoneCallSource source) {
  if (prv_check_popups_are_blocked()) {
    return;
  }

  if (s_phone_ui_data) {
    // In this case we are waiting to pop the window and a new event has come in.
    // Pop it immediately and then set up for the new event
    prv_window_pop();
  }

  prv_phone_ui_init();
  s_phone_ui_data->show_ongoing_call_ui = show_ongoing_call_ui;

  prv_unfold_icon_resource(TIMELINE_RESOURCE_INCOMING_PHONE_CALL);

  bool can_reply = false;
  if (caller) {
    prv_display_caller_info(caller);

    // Check if we support sms reply
    can_reply = prv_load_sms_reply_action(caller->number, source);
  }

  uint8_t actions = PhoneCallActions_Decline;
  if (can_reply) {
    actions |= PhoneCallActions_Reply;
  }
  if (can_answer) {
    actions |= PhoneCallActions_Answer;
  }
  prv_action_bar_setup(actions);

  prv_start_ringing();
}

void phone_ui_handle_outgoing_call(PebblePhoneCaller *caller) {
  if (s_phone_ui_data) {
    // In this case we are waiting to pop the window and a new event has come in.
    // Pop it immediately and then set up for the new event
    prv_window_pop();
  }

  prv_phone_ui_init();

  // FIXME: PBL-21570 Outgoing call small is missing
  prv_unfold_icon_resource(TIMELINE_RESOURCE_INCOMING_PHONE_CALL);

  if (caller) {
    prv_display_caller_info(caller);
  }

  prv_action_bar_setup(PhoneCallActions_None);

  prv_window_pop_with_delay(OUTGOING_CALL_DELAY_MS);
}

void phone_ui_handle_missed_call(void) {
  if (!s_phone_ui_data) {
    return;
  }

  prv_stop_ringing();

  prv_set_icon_resource(TIMELINE_RESOURCE_DISMISSED_PHONE_CALL);

  prv_action_bar_setup(PhoneCallActions_None);

  prv_window_pop_with_delay(MISSED_CALL_DELAY_MS);
}

void phone_ui_handle_call_start(bool can_decline) {
  if (!s_phone_ui_data) {
    PBL_LOG(LOG_LEVEL_ERROR, "Can't handle call start, UI isn't setup");
    return;
  }

  prv_stop_ringing();

#if PBL_RECT
  prv_set_icon_resource(TIMELINE_RESOURCE_DURING_PHONE_CALL);
#else
  // action bar requires right-aligned icon, otherwise centered icon
  prv_set_icon_resource((can_decline) ? TIMELINE_RESOURCE_DURING_PHONE_CALL :
                        TIMELINE_RESOURCE_DURING_PHONE_CALL_CENTERED);
#endif

  prv_set_window_color(ACCEPT_COLOR, false);

  prv_action_bar_setup(can_decline ? PhoneCallActions_Decline : PhoneCallActions_None);

  prv_start_call_duration_timer();
  prv_set_answer_window();
}

void phone_ui_handle_call_end(bool call_accepted, bool disconnected) {
  if (!s_phone_ui_data) {
    PBL_LOG(LOG_LEVEL_ERROR, "Can't handle call end, UI isn't setup");
    return;
  }

  prv_stop_ringing();

  prv_stop_call_duration_timer();

  // This must be called before prv_set_status_text, otherwise the text will not be centered
  prv_action_bar_setup(PhoneCallActions_None);

  if (call_accepted) {
    prv_set_icon_resource(TIMELINE_RESOURCE_DURING_PHONE_CALL);
    prv_set_window_color(ACCEPT_COLOR, true);
    prv_set_status_text(ACCEPTED);
  } else {
    prv_set_icon_resource(TIMELINE_RESOURCE_DISMISSED_PHONE_CALL);
    prv_set_window_color(DECLINE_COLOR, true);
    if (disconnected) {
      prv_set_status_text(DISCONNECTED);
    } else {
      prv_set_status_text(DECLINED);
    }
  }

  prv_window_pop_with_delay(CALL_END_DELAY_MS);
}

void phone_ui_handle_call_hide(void) {
  // Just pop the window - it'll handle all the cleanup
  prv_window_pop();
}

void phone_ui_handle_caller_id(PebblePhoneCaller *caller) {
  if (!s_phone_ui_data) {
    PBL_LOG(LOG_LEVEL_ERROR, "Can't update caller id, UI isn't setup");
    return;
  }

  if (caller) {
    prv_display_caller_info(caller);
  }
}
