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

#include "applib/graphics/gdraw_command_private.h"
#include "applib/graphics/gdraw_command_transforms.h"
#include "applib/ui/animation.h"
#include "applib/ui/animation_interpolate.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/kino/kino_reel_pdci.h"
#include "applib/ui/kino/kino_reel_custom.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
#include "util/math.h"
#include "util/net.h"
#include "util/struct.h"

typedef struct {
  GRect layer_frame;
  GRect from;
  GRect to;

  const TransformImpl *impl;
  void *context;

  int32_t normalized;
  uint32_t elapsed;
  uint32_t duration;

  KinoReel *from_reel;
  KinoReel *to_reel;

  GDrawCommandList *list_copy;
  GSize list_copy_size;

  bool owns_from_reel;
  bool owns_to_reel;
  bool global;
} KinoReelTransformData;

static bool prv_is_currently_from(KinoReelTransformData *data) {
  return (!data->to_reel || (data->normalized < ANIMATION_NORMALIZED_MAX / 2));
}

static KinoReel *prv_get_current_reel(KinoReelTransformData *data) {
  return prv_is_currently_from(data) ? data->from_reel : data->to_reel;
}

static GSize prv_get_current_size(KinoReelTransformData *data) {
  return prv_is_currently_from(data) ? data->from.size : data->to.size;
}

static GPoint prv_get_interpolated_origin(KinoReelTransformData *data) {
  const GSize size = prv_get_current_size(data);
  const GPoint offset = interpolate_gpoint(data->normalized, grect_center_point(&data->from),
                                           grect_center_point(&data->to));
  return gpoint_sub(offset, GPoint(size.w / 2, size.h / 2));
}

static bool prv_image_size_eq_rect_size(KinoReelTransformData *data, GRect rect) {
  GSize size = kino_reel_get_size(prv_get_current_reel(data));
  return gsize_equal(&size, &rect.size);
}

static void prv_free_list_copy(KinoReelTransformData *data) {
  applib_free(data->list_copy);
  data->list_copy = NULL;
}

static GDrawCommandList *prv_get_or_create_list_copy(KinoReelTransformData *data,
                                                     GDrawCommandList *list) {
  if (data->list_copy && (gdraw_command_list_get_data_size(data->list_copy) >=
                          gdraw_command_list_get_data_size(list))) {
    return data->list_copy;
  }
  prv_free_list_copy(data);
  data->list_copy = gdraw_command_list_clone(list);
  return data->list_copy;
}

static void prv_destructor(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data->impl->destructor) {
    data->impl->destructor(data->context);
  }
  if (data->owns_from_reel) {
    kino_reel_destroy(data->from_reel);
  }
  if (data->owns_to_reel) {
    kino_reel_destroy(data->to_reel);
  }
  gdraw_command_list_destroy(data->list_copy);
  applib_free(data);
}

static uint32_t prv_elapsed_getter(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  return scale_int32(data->normalized, ANIMATION_NORMALIZED_MAX, data->duration);
}

static uint32_t prv_get_duration(KinoReelTransformData *data) {
  uint32_t duration = data->duration;
  if (data->from_reel) {
    const uint32_t from_duration = kino_reel_get_duration(data->from_reel);
    // If we don't have a 'to_reel' then we are looping back to the 'from_reel' so it's okay for
    // the 'from_reel' to have an infinite duration
    //
    // If we have a 'to_reel' then ignore infinite duration requests because we will never get to
    // it and burn a lot of power along the way!
    if ((data->to_reel == NULL) || (from_duration != PLAY_DURATION_INFINITE)) {
      duration = MAX(duration, from_duration);
    }
  }

  if (data->to_reel) {
    // We want to make sure the transform duration is at least as long as the to_reel duration
    // so that the resource transition runs to completion if it's animated
    const uint32_t to_duration = kino_reel_get_duration(data->to_reel);
    duration = MAX(duration, to_duration);
  }
  return duration;
}

static bool prv_elapsed_setter(KinoReel *reel, uint32_t elapsed) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data->elapsed == elapsed) {
    return false;
  }

  bool changed = false;

  data->elapsed = elapsed;
  if (data->from_reel && kino_reel_set_elapsed(data->from_reel, elapsed)) {
    changed = true;
  }
  if (data->to_reel && kino_reel_set_elapsed(data->to_reel, elapsed)) {
    changed = true;
  }

  const int32_t normalized = animation_timing_clip(
      scale_int32(elapsed, data->duration, ANIMATION_NORMALIZED_MAX));
  if (data->normalized == normalized) {
    return changed;
  }

  data->normalized = normalized;

  // No position setter is shorthand for always triggering a transform on any position setting
  bool transform_changed = true;
  if (data->impl->position_setter &&
      !data->impl->position_setter(normalized, data->context)) {
    transform_changed = false;
  }

  return (changed || transform_changed);
}

static uint32_t prv_duration_getter(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  return prv_get_duration(data);
}

static GSize prv_size_getter(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  return interpolate_gsize(data->normalized, data->from.size, data->to.size);
}

static void prv_transform_list(KinoReelTransformData *data) {
  KinoReel *reel = prv_get_current_reel(data);
  GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
  GDrawCommandList *list = prv_get_or_create_list_copy(data, source_list);
  if (!list) {
    return;
  }
  if (!gdraw_command_list_copy(list, gdraw_command_list_get_data_size(source_list),
                                source_list)) {
    return;
  }
  if (data->impl->apply) {
    const GSize size = kino_reel_get_size(reel);
    data->impl->apply(list, size, &data->from, &data->to, data->normalized, data->context);
  }
}

static void prv_draw_command_list_processed(GContext *ctx, GDrawCommandList *list, GPoint offset,
                                            KinoReelProcessor *processor) {
  GPoint draw_box_origin = ctx->draw_state.drawing_box.origin;
  graphics_context_move_draw_box(ctx, offset);
  gdraw_command_list_draw_processed(ctx, list, NULL_SAFE_FIELD_ACCESS(processor,
                                                                      draw_command_processor,
                                                                      NULL));
  ctx->draw_state.drawing_box.origin = draw_box_origin;
}

static void prv_draw_reel_or_command_list_processed(
    GContext *ctx, KinoReel *reel, GDrawCommandList *list, GPoint offset,
    KinoReelProcessor *processor) {
  if (list) {
    prv_draw_command_list_processed(ctx, list, offset, processor);
  } else {
    kino_reel_draw_processed(reel, ctx, offset, processor);
  }
}

static void prv_draw_processed_in_local(KinoReelTransformData *data, GContext *ctx, GPoint offset,
                                        KinoReelProcessor *processor) {
  KinoReel *reel = prv_get_current_reel(data);
  GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
  GDrawCommandList *list = prv_get_or_create_list_copy(data, source_list);
  if (data->global) {
    offset = gpoint_sub(GPointZero, data->layer_frame.origin);
  }
  prv_draw_reel_or_command_list_processed(ctx, reel, list, offset, processor);
}

static void prv_draw_processed_in_global(KinoReelTransformData *data, GContext *ctx, GPoint offset,
                                         KinoReelProcessor *processor) {
  KinoReel *reel = prv_get_current_reel(data);
  GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
  GDrawCommandList *list = prv_get_or_create_list_copy(data, source_list);
  offset = gpoint_to_local_coordinates(GPointZero, ctx);
  if (!list) {
    // There is no list with global coordinates embedded. Instead, interpolate the offset.
    gpoint_add_eq(&offset, prv_get_interpolated_origin(data));
  }
  prv_draw_reel_or_command_list_processed(ctx, reel, list, offset, processor);
}

static void prv_draw_processed_at_rect(KinoReelTransformData *data, GContext *ctx, GPoint offset,
                                       GRect rect, KinoReelProcessor *processor) {
  if (!prv_image_size_eq_rect_size(data, rect)) {
    prv_transform_list(data);
    prv_draw_processed_in_local(data, ctx, offset, processor);
    return;
  }
  prv_free_list_copy(data);
  if (data->global) {
    offset = gpoint_sub(GPointZero, data->layer_frame.origin);
  }
  offset = gpoint_add(offset, rect.origin);
  KinoReel *reel = prv_get_current_reel(data);
  GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
  prv_draw_reel_or_command_list_processed(ctx, reel, source_list, offset, processor);
}

static void prv_draw_processed_func(KinoReel *reel, GContext *ctx, GPoint offset,
                                    KinoReelProcessor *processor) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);

  if (data->normalized == 0) {
    prv_draw_processed_at_rect(data, ctx, offset, data->from, processor);
    return;
  }

  if (data->normalized == ANIMATION_NORMALIZED_MAX) {
    prv_draw_processed_at_rect(data, ctx, offset, data->to, processor);
    return;
  }

  prv_transform_list(data);

  if (data->global) {
    prv_draw_processed_in_global(data, ctx, offset, processor);
  } else {
    prv_draw_processed_in_local(data, ctx, offset, processor);
  }
}

static GDrawCommandList *prv_get_gdraw_command_list(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    KinoReel *reel = prv_get_current_reel(data);
    return kino_reel_get_gdraw_command_list(reel);
  }
  return NULL;
}

static const KinoReelImpl s_kino_reel_impl_transform = {
  .destructor = prv_destructor,
  .get_elapsed = prv_elapsed_getter,
  .set_elapsed = prv_elapsed_setter,
  .get_duration = prv_duration_getter,
  .get_size = prv_size_getter,
  .draw_processed = prv_draw_processed_func,
  .get_gdraw_command_list = prv_get_gdraw_command_list,
};

KinoReel *kino_reel_transform_create(const TransformImpl *impl, void *context) {
  KinoReelTransformData *data = applib_malloc(sizeof(KinoReelTransformData));
  if (!data) {
    return NULL;
  }

  *data = (KinoReelTransformData) {
    .impl = impl,
    .context = context,
    .duration = ANIMATION_DEFAULT_DURATION_MS,
  };

  KinoReel *reel = kino_reel_custom_create(&s_kino_reel_impl_transform, data);
  if (!reel) {
    applib_free(data);
  }
  return reel;
}

void *kino_reel_transform_get_context(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    return data->context;
  }
  return NULL;
}

void kino_reel_transform_set_from_reel(KinoReel *reel, KinoReel *from_reel,
                                            bool take_ownership) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (!data) {
    return;
  }
  if (data->owns_from_reel) {
    kino_reel_destroy(data->from_reel);
  }
  data->from_reel = from_reel;
  data->owns_from_reel = take_ownership;
  prv_free_list_copy(data);
}

KinoReel *kino_reel_transform_get_from_reel(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    return data->from_reel;
  }
  return NULL;
}

void kino_reel_transform_set_to_reel(KinoReel *reel, KinoReel *to_reel,
                                          bool take_ownership) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (!data) {
    return;
  }
  if (data->owns_to_reel) {
    kino_reel_destroy(data->to_reel);
  }
  data->to_reel = to_reel;
  data->owns_to_reel = take_ownership;
  prv_free_list_copy(data);
}

KinoReel *kino_reel_transform_get_to_reel(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    return data->to_reel;
  }
  return NULL;
}

void kino_reel_transform_set_layer_frame(KinoReel *reel, GRect layer_frame) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    data->layer_frame = layer_frame;
  }
}

void kino_reel_transform_set_from_frame(KinoReel *reel, GRect from) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    data->from = from;
  }
}

void kino_reel_transform_set_to_frame(KinoReel *reel, GRect to) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    data->to = to;
  }
}

GRect kino_reel_transform_get_from_frame(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    return data->from;
  }
  return GRectZero;
}

GRect kino_reel_transform_get_to_frame(KinoReel *reel) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    return data->to;
  }
  return GRectZero;
}

void kino_reel_transform_set_global(KinoReel *reel, bool global) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    data->global = global;
  }
}

void kino_reel_transform_set_transform_duration(KinoReel *reel, uint32_t duration) {
  KinoReelTransformData *data = kino_reel_custom_get_data(reel);
  if (data) {
    data->duration = duration;
  }
}
