/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "app.h"

#include "applib/app_heap_analytics.h"
#include "applib/graphics/graphics_private.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/window_stack.h"
#include "applib/ui/window_private.h"
#include "mcu/fpu.h"
#include "process_management/app_manager.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/profiler.h"

static void prv_render_app(void) {
  WindowStack *stack = app_state_get_window_stack();
  GContext *ctx = app_state_get_graphics_context();
  if (!window_stack_is_animating(stack)) {
    SYS_PROFILER_NODE_START(render_app);
    window_render(app_window_stack_get_top_window(), ctx);
    SYS_PROFILER_NODE_STOP(render_app);
  } else {
    // TODO: PBL-17645 render container layer instead of the two windows
    WindowTransitioningContext *transition_context = &stack->transition_context;

    if (transition_context->implementation->render) {
      transition_context->implementation->render(transition_context, ctx);
    }
  }

  *app_state_get_framebuffer_render_pending() = true;

  PebbleEvent event = {
    .type = PEBBLE_RENDER_READY_EVENT,
  };
  sys_send_pebble_event_to_kernel(&event);
}

static bool prv_window_is_render_scheduled(Window *window) {
  return window && window->is_render_scheduled;
}

static bool prv_app_is_render_scheduled() {
  Window *top_window = app_window_stack_get_top_window();
  if (prv_window_is_render_scheduled(top_window)) {
    return true;
  }

  WindowStack *stack = app_state_get_window_stack();
  if (!window_stack_is_animating(stack)) {
    return false;
  }

  WindowTransitioningContext *transition_ctx = &stack->transition_context;
  return prv_window_is_render_scheduled(transition_ctx->window_from) ||
         prv_window_is_render_scheduled(transition_ctx->window_to);
}

void app_request_render(void) {
  Window *window = app_window_stack_get_top_window();
  if (window) {
    window_schedule_render(window);
  }
}

//! Tasks that have to be done in between each event.
static NOINLINE void event_loop_upkeep(void) {

  // Check to see if the most recent event caused us to pop our final window. If that's the case, we need to
  // kill ourselves.
  if (app_window_stack_count() == 0) {
    PBL_LOG(LOG_LEVEL_DEBUG, "No more windows, killing current app");

    PebbleEvent event = { .type = PEBBLE_PROCESS_KILL_EVENT, .kill = { .gracefully = true, .task=PebbleTask_App } };
    sys_send_pebble_event_to_kernel(&event);
    return;
  }

  // Check to see if handling the previous event requires us to rerender ourselves.
  if (prv_app_is_render_scheduled() &&
      !*app_state_get_framebuffer_render_pending()) {
    prv_render_app();
  }
}

static void prv_app_will_focus_handler(PebbleEvent *e, void *context) {
  Window *window = app_window_stack_get_top_window();
  if (e->app_focus.in_focus) {
    if (window) {
      // Do not call 'appear' handler on window displacing modal window
      window_set_on_screen(window, true, false);
      window_render(window, app_state_get_graphics_context());
    }
    click_manager_reset(app_state_get_click_manager());
  } else if (window) {
    // Do not call 'disappear' handler on window displaced by modal window
    window_set_on_screen(window, false, false);
  }
}

static void prv_app_button_down_handler(PebbleEvent *e, void *context) {
  WindowStack *app_window_stack = app_state_get_window_stack();
  if (window_stack_is_animating(app_window_stack)) {
    return;
  }

  sys_analytics_inc(ANALYTICS_APP_METRIC_BUTTONS_PRESSED_COUNT, AnalyticsClient_App);

  if (e->button.button_id == BUTTON_ID_BACK &&
      !app_window_stack_get_top_window()->overrides_back_button) {
    // a transition of NULL means we will use the stored pop transition for this stack item
    window_stack_pop_with_transition(app_window_stack, NULL /* transition */);
    return;
  }

  click_recognizer_handle_button_down(
      &app_state_get_click_manager()->recognizers[e->button.button_id]);
}

static void prv_app_button_up_handler(PebbleEvent *e, void *context) {
  if (window_stack_is_animating(app_state_get_window_stack())) {
    return;
  }

  click_recognizer_handle_button_up(
      &app_state_get_click_manager()->recognizers[e->button.button_id]);
}

// this handler is called via the legacy2_status_bar_change_event and
// will update the status bar once a minute for non-fullscreen legacy2 apps
static void prv_legacy2_status_bar_handler(PebbleEvent *e, void *context) {
  Window *window = app_window_stack_get_top_window();
  // only force render if we're not fullscreen
  if (!window->is_fullscreen) {
    // a little logic to only force update when the minute changes
    ApplibInternalEventsInfo *events_info =
        app_state_get_applib_internal_events_info();
    struct tm currtime;
    sys_localtime_r(&e->clock_tick.tick_time, &currtime);
    const int minute_of_day = (currtime.tm_hour * 60) + currtime.tm_min;
    if (events_info->minute_of_last_legacy2_statusbar_change != minute_of_day) {
      events_info->minute_of_last_legacy2_statusbar_change = minute_of_day;
      window_schedule_render(window);
    }
  }
}

static void prv_legacy2_status_bar_timer_subscribe(void) {
  // we only need this tick event if we are a legacy2 app
  if (process_manager_compiled_with_legacy2_sdk()) {
    ApplibInternalEventsInfo *events_info =
        app_state_get_applib_internal_events_info();
    // Initialize the state for the status bar handler.
    events_info->minute_of_last_legacy2_statusbar_change = -1;
    events_info->legacy2_status_bar_change_event = (EventServiceInfo) {
      .type = PEBBLE_TICK_EVENT,
      .handler = prv_legacy2_status_bar_handler,
    };
    event_service_client_subscribe(
        &events_info->legacy2_status_bar_change_event);
  }
  // NOTE: We could be super fancy and register and unregister when the fullscreen
  // status changes, but it's probably not worth it as we'll be waking up once a
  // minute anyway to update the face itself and it will happen as part of the same interval
}

static void prv_legacy2_status_bar_timer_unsubscribe(void) {
  // we should only unsubscribe if we subscribed in the first place
  if (process_manager_compiled_with_legacy2_sdk()) {
    ApplibInternalEventsInfo *events_info =
        app_state_get_applib_internal_events_info();
    event_service_client_unsubscribe(
        &events_info->legacy2_status_bar_change_event);
  }
}

static void prv_app_callback_handler(PebbleEvent *e) {
  e->callback.callback(e->callback.data);
}

static NOINLINE void prv_handle_deinit_event(void) {
  ApplibInternalEventsInfo *events_info =
      app_state_get_applib_internal_events_info();
  event_service_client_unsubscribe(&events_info->will_focus_event);
  event_service_client_unsubscribe(&events_info->button_down_event);
  event_service_client_unsubscribe(&events_info->button_up_event);
  prv_legacy2_status_bar_timer_unsubscribe(); // a no-op on sdk3+ applications
  WindowStack *app_window_stack = app_state_get_window_stack();
  window_stack_lock_push(app_window_stack);
  window_stack_pop_all(app_window_stack, false);
  window_stack_unlock_push(app_window_stack);
}

// Get the app_id for the current app or worker
// @return INSTALL_ID_INVALID if unsuccessful
AppInstallId app_get_app_id(void) {
  // Only support from app or workers
  PebbleTask task = pebble_task_get_current();
  if ((task != PebbleTask_App) && (task != PebbleTask_Worker)) {
    APP_LOG(APP_LOG_LEVEL_ERROR, "Only supported from app or worker tasks");
    return INSTALL_ID_INVALID;
  }

  // Get the app id
  if (task == PebbleTask_App) {
    return sys_app_manager_get_current_app_id();
  } else {
    return sys_worker_manager_get_current_worker_id();
  }
}

void app_event_loop_common(void) {
  // Register our event handlers before we do anything else. Registering for an event requires
  // an event being sent to the kernel and therefore should be done before any other events are
  // generated by us to ensure we don't miss out on anything.
  ApplibInternalEventsInfo *events_info =
      app_state_get_applib_internal_events_info();
  events_info->will_focus_event = (EventServiceInfo) {
    .type = PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT,
    .handler = prv_app_will_focus_handler,
  };
  events_info->button_down_event = (EventServiceInfo) {
    .type = PEBBLE_BUTTON_DOWN_EVENT,
    .handler = prv_app_button_down_handler,
  };
  events_info->button_up_event = (EventServiceInfo) {
    .type = PEBBLE_BUTTON_UP_EVENT,
    .handler = prv_app_button_up_handler,
  };
  event_service_client_subscribe(&events_info->will_focus_event);
  event_service_client_subscribe(&events_info->button_down_event);
  event_service_client_subscribe(&events_info->button_up_event);
  prv_legacy2_status_bar_timer_subscribe(); // a no-op on sdk3+ applications

  event_loop_upkeep();

  // Event loop:
  while (1) {
    PebbleEvent event;

    sys_get_pebble_event(&event);

    if (event.type == PEBBLE_PROCESS_DEINIT_EVENT) {
      prv_handle_deinit_event();
      // We're done here. Return the app's main function.
      event_cleanup(&event);
      return;
    } else if (event.type == PEBBLE_CALLBACK_EVENT) {
      prv_app_callback_handler(&event);
    } else if (event.type == PEBBLE_RENDER_REQUEST_EVENT) {
      app_request_render();
    } else if (event.type == PEBBLE_RENDER_FINISHED_EVENT) {
      *app_state_get_framebuffer_render_pending() = false;
    } else {
      event_service_client_handle_event(&event);
    }

    mcu_fpu_cleanup();
    event_cleanup(&event);

    event_loop_upkeep();
  }
}

void app_event_loop(void) {
  app_event_loop_common();
  app_heap_analytics_log_stats_to_app_heartbeat(false /* is_rocky_app */);
}
