/*
 * 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 "clar.h"

#include "applib/graphics/gcontext.h"
#include "applib/graphics/gtypes.h"
#include "kernel/events.h"
#include "kernel/ui/modals/modal_manager.h"
#include "services/common/compositor/compositor.h"

// Stubs
///////////////////////////////////////////////////////////

#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_compositor_dma.h"
#include "stubs_framebuffer.h"
#include "stubs_gbitmap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_timeline_peek.h"

extern void prv_handle_display_update_complete(void);

static int s_count_animation_create = 0;
Animation* animation_create(void) {
  ++s_count_animation_create;

  return (Animation*) (uintptr_t) s_count_animation_create;
}

static int s_count_animation_schedule = 0;
static Animation *s_scheduled_animation;
bool animation_schedule(Animation *animation) {
  ++s_count_animation_schedule;
  s_scheduled_animation = animation;

  return true;
}

bool animation_set_auto_destroy(Animation *animation, bool auto_destroy) {
  return true;
}

bool animation_is_scheduled(Animation *animation_h) {
  return animation_h && animation_h == s_scheduled_animation;
}

static int s_count_animation_destroy = 0;
bool animation_unschedule(Animation *animation) {
  ++s_count_animation_destroy;
  s_scheduled_animation = NULL;
  return true;
}

bool animation_destroy(Animation *animation) {
  ++s_count_animation_destroy;
  s_scheduled_animation = NULL;
  return true;
}


static int s_app_window_render_count;
FrameBuffer* app_state_get_framebuffer(void) {
  // Not a great proxy for app rendering but good enough. This gets called twice per app render.
  ++s_app_window_render_count;

  return compositor_get_framebuffer();
}

void app_manager_get_framebuffer_size(GSize *size) {
  *size = (GSize) {DISP_COLS, DISP_ROWS};
}

void bitblt_bitmap_into_bitmap(GBitmap* dest_bitmap, const GBitmap* src_bitmap, GPoint dest_offset,
                               GCompOp compositing_mode, GColor tint_color) {
}

void compositor_dot_transition_app_to_app_init(Animation *animation) {
}

bool compositor_dot_transition_app_to_app_update_func(
    GContext *ctx, Animation *animation, uint32_t distance_normalized) {
  return true;
}

static bool s_modal_window_present = false;
Window* modal_manager_get_top_window(void) {
  return (Window*)(uintptr_t)s_modal_window_present;
}

static int s_modal_manager_render_count;
void modal_manager_render(GContext *ctx) {
  ++s_modal_manager_render_count;
}

ModalProperty modal_manager_get_properties(void) {
  return s_modal_window_present ? ModalProperty_Exists : ModalPropertyDefault;
}

GContext* kernel_ui_get_graphics_context(void) {
  static GContext s_context;
  return &s_context;
}

void framebuffer_clear(FrameBuffer* f) {
}

void framebuffer_dirty_all(FrameBuffer *fb) {
}

GBitmap framebuffer_get_as_bitmap(FrameBuffer *fb, const GSize *size) {
  return (GBitmap) { };
}

void framebuffer_set_line(FrameBuffer* f, uint8_t y, const uint8_t* buffer) {
}

GDrawState graphics_context_get_drawing_state(GContext* ctx) {
  return (GDrawState) { };
}

void graphics_context_set_drawing_state(GContext* ctx, GDrawState draw_state) {
}

AnimationPrivate *animation_private_animation_find(Animation *handle) {
  return NULL;
}

static int s_count_display_update = 0;
void compositor_display_update(void (*handle_update_complete_cb)(void)) {
  ++s_count_display_update;
}

static bool s_display_update_in_progress = false;
bool compositor_display_update_in_progress(void) {
  return s_display_update_in_progress;
}

static PebbleEvent s_last_event;
void event_put(PebbleEvent* event) {
  s_last_event = *event;
}

static bool s_render_pending;
bool process_manager_send_event_to_process(PebbleTask task, PebbleEvent *event) {
  if (event->type == PEBBLE_RENDER_FINISHED_EVENT) {
    s_render_pending = false;
  }
  s_last_event = *event;
  return true;
}

static const AnimationImplementation *s_animation_implementation;
bool animation_set_implementation(Animation *animation,
                                  const AnimationImplementation *implementation) {
  s_animation_implementation = implementation;
  return true;
}

static int s_count_compositor_init_func_a = 0;
static void prv_compositor_init_func_a(Animation *animation) {
  ++s_count_compositor_init_func_a;
}

static void prv_compositor_update_func_a(GContext *ctx, Animation *animation,
                                         uint32_t distance_normalized) {
}

static int s_count_compositor_init_func_b = 0;
static void prv_compositor_init_func_b(Animation *animation) {
  ++s_count_compositor_init_func_b;
}

static void prv_compositor_update_func_b(GContext *ctx, Animation *animation,
                                         uint32_t distance_normalized) {
}

static const CompositorTransition s_transition_a = {
  .init = prv_compositor_init_func_a,
  .update = prv_compositor_update_func_a
};

static const CompositorTransition s_transition_b = {
  .init = prv_compositor_init_func_b,
  .update = prv_compositor_update_func_b
};

void launcher_task_add_callback(void (*callback)(void *data), void *data) {
}


// Tests
///////////////////////////////////////////////////////////

void test_compositor__initialize(void) {
  s_animation_implementation = NULL;

  s_last_event = (PebbleEvent) { .type = 0 };

  s_modal_window_present = false;

  s_count_animation_create = 0;
  s_count_animation_schedule = 0;
  s_count_animation_destroy = 0;
  s_scheduled_animation = NULL;

  s_count_display_update = 0;
  s_count_compositor_init_func_a = 0;
  s_count_compositor_init_func_b = 0;

  s_app_window_render_count = 0;
  s_modal_manager_render_count = 0;

  s_display_update_in_progress = false;

  s_render_pending = false;

  compositor_init();
}

void test_compositor__cleanup(void) {
}

void test_compositor__simple(void) {
  compositor_transition(&s_transition_a);

  // The animation should be created but not scheduled, as we're waiting for the app to render
  cl_assert_equal_i(s_count_animation_create, 1);
  cl_assert_equal_i(s_count_compositor_init_func_a, 1);
  cl_assert_equal_i(s_count_animation_schedule, 0);

  // Make the app render, now the animation should be scheduled
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_animation_schedule, 1);

  compositor_transition(&s_transition_b);

  // We should create a second animation, calling the b's transition init func. The animation
  // should not be scheduled as we're waiting for the interrupted app to render
  cl_assert_equal_i(s_count_animation_create, 2);
  cl_assert_equal_i(s_count_animation_schedule, 1);
  cl_assert_equal_i(s_count_compositor_init_func_a, 1);
  cl_assert_equal_i(s_count_compositor_init_func_b, 1);

  // Make the app render, now the animation should be scheduled
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_animation_schedule, 2);

  // Push a modal window mid transition, the resulting animation should be scheduled immediately.
  s_modal_window_present = true;
  compositor_transition(&s_transition_b);
  cl_assert_equal_i(s_count_animation_create, 3);
  cl_assert_equal_i(s_count_animation_schedule, 3);
  cl_assert_equal_i(s_count_compositor_init_func_a, 1);
  cl_assert_equal_i(s_count_compositor_init_func_b, 2);
}

void test_compositor__app_render_busy(void) {
  // Set the display as busy and then render the app. Nothing should update.
  s_display_update_in_progress = true;
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_display_update, 0);

  // Now fake the display update completeing. The app should know draw.
  s_display_update_in_progress = false;
  prv_handle_display_update_complete();
  cl_assert_equal_i(s_count_display_update, 1);

  // Subsequent app updates should now draw straight through.
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_display_update, 2);

  // test animation updates
  s_display_update_in_progress = true;
  // start a transition
  compositor_transition(&s_transition_a);
  cl_assert_equal_i(s_count_display_update, 2);

  // app render will be handled before transition
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_display_update, 2);
  cl_assert_equal_i(s_count_animation_schedule, 0);

  // transition is started from the deferred transition event
  s_display_update_in_progress = false;
  prv_handle_display_update_complete();
  cl_assert_equal_i(s_count_display_update, 3);

  // subsequent app render starts animation
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_animation_schedule, 1);
  cl_assert_equal_i(s_count_display_update, 3);
}

void test_compositor__modal_transition_cancels_deferred_app(void) {
  // Set the display as busy and then render the app. Nothing should update.
  s_display_update_in_progress = true;
  s_render_pending = true;
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_display_update, 0);
  cl_assert_equal_i(s_render_pending, true);

  // Now transition to a modal. The app framebuffer should be released. No animation should be started.
  s_modal_window_present = true;
  compositor_transition(&s_transition_a);
  cl_assert_equal_i(s_render_pending, false);
  cl_assert_equal_i(s_count_animation_create, 0);

  // Start the animation
  s_display_update_in_progress = false;
  prv_handle_display_update_complete();
  cl_assert_equal_i(s_count_animation_create, 1);
  cl_assert_equal_i(s_count_animation_schedule, 1);
}

void test_compositor__app_no_animation(void) {
  // Start a transition. We shouldn't update the screen because the app hasn't rendered
  // yet.
  compositor_transition(NULL);
  cl_assert_equal_i(s_count_display_update, 0);
  cl_assert_equal_i(s_count_animation_create, 0);

  // Now the app has rendered something and we should actually update the display.
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_display_update, 1);
}

void test_compositor__app_not_ready_modal_push_pop(void) {
  // If a modal window is popped revealing an app that has not yet rendered itself for the first
  // time we shouldn't render the app immediately. We need to still wait for the app to render
  // itself for the first time. This is a known issue that doesn't seem to happen too frequently
  // in practice, but we should fix it up at some point.

  // Start a null window transition to an app. It should wait for the app to report ready
  compositor_transition(NULL);
  cl_assert_equal_i(s_count_display_update, 0);
  cl_assert_equal_i(s_count_animation_create, 0);

  // Push a modal window with an animation and then pop it without an animation. The app still
  // shouldn't be ready.
  s_modal_window_present = true;
  compositor_transition(&s_transition_a);
  cl_assert_equal_i(s_count_animation_create, 1);

  s_modal_window_present = false;
  compositor_transition(NULL);

  // previous animation is unscheduled
  cl_assert_equal_i(s_count_animation_destroy, 1);

  // Throughout pushing this modal the app never reported it was ready, so we still shouldn't have
  // rendered from the app frame buffer.
  cl_assert_equal_i(s_app_window_render_count, 0);

  // Now the app has rendered something and we should actually update the display.
  compositor_app_render_ready();
  cl_assert_equal_i(s_app_window_render_count, 2);
}

void test_compositor__app_not_ready_cancelled_animation_deferred(void) {
  // Show a modal window
  s_modal_window_present = true;
  compositor_transition(NULL);

  // Now pop it with a transition
  s_modal_window_present = false;
  compositor_transition(&s_transition_a);
  cl_assert_equal_i(s_count_animation_create, 1);

  // Pretend we're in the middle of copying a frame to the display hardware
  s_display_update_in_progress = true;

  // Start a null window transition to an app while the modal is popping. It should wait for the
  // app to report ready. This shouldn't cancel any animation, as the animation isn't started yet,
  // as it should be waiting for the app to render for the first time before starting.
  compositor_transition(NULL);
  cl_assert_equal_i(s_count_animation_destroy, 1);
  cl_assert_equal_i(s_app_window_render_count, 0);

  // Now complete the animation teardown by finishing the copy to the display. We shouldn't render
  // the app because it hasn't rendered anything yet.
  s_display_update_in_progress = false;
  prv_handle_display_update_complete();
  cl_assert_equal_i(s_app_window_render_count, 0);
  cl_assert_equal_i(s_count_animation_destroy, 1);

  // Now the app has rendered something and we should actually update the display.
  compositor_app_render_ready();
  cl_assert_equal_i(s_app_window_render_count, 2);
  cl_assert_equal_i(s_count_animation_destroy, 1);
}

void test_compositor__cancel_modal_to_app_with_another_modal(void) {
  // Show a modal window
  s_modal_window_present = true;
  compositor_transition(NULL);
  cl_assert_equal_i(s_count_display_update, 1);

  // Now pop it with a transition
  s_modal_window_present = false;
  compositor_transition(&s_transition_a);
  cl_assert_equal_i(s_count_animation_create, 1);
  cl_assert_equal_i(s_count_display_update, 1);
  cl_assert_equal_i(s_count_animation_schedule, 0);

  // Have the app render once so the animation can start
  s_render_pending = true;
  compositor_app_render_ready();
  cl_assert_equal_i(s_count_animation_schedule, 1);
  // Don't allow the app to render while we're animating to it
  cl_assert_equal_i(s_render_pending, true);

  // Now before the previous animation completes, transition to a different modal
  s_modal_window_present = true;
  compositor_transition(&s_transition_b);
  // Create and schedule a new modal animation, destroying the old one
  cl_assert_equal_i(s_count_animation_create, 2);
  cl_assert_equal_i(s_count_animation_schedule, 2);
  cl_assert_equal_i(s_count_animation_destroy, 1);
  // The app framebuffer should still be locked while we're animating away
  cl_assert_equal_i(s_render_pending, true);

  // Finish the animation
  s_animation_implementation->teardown((Animation*) (uintptr_t) s_count_animation_create);
  // App should be free to render again
  cl_assert_equal_i(s_render_pending, false);
}
