/*
 * 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 "unobstructed_area_service.h"
#include "unobstructed_area_service_private.h"

#include "applib/app.h"
#include "applib/graphics/framebuffer.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall_internal.h"
#include "system/passert.h"

static void prv_handle_unobstructed_area_event(PebbleEvent *event, void *context);
static void prv_origin_y_to_area(int16_t origin_y, GRect *area_out);

void unobstructed_area_service_init(UnobstructedAreaState *state, int16_t current_y) {
  PBL_ASSERTN(state);
  *state = (UnobstructedAreaState) {};
  prv_origin_y_to_area(current_y, &state->area);
  state->event_info = (EventServiceInfo) {
    .type = PEBBLE_UNOBSTRUCTED_AREA_EVENT,
    .handler = prv_handle_unobstructed_area_event,
    .context = state,
  };
  event_service_client_subscribe(&state->event_info);
}

void unobstructed_area_service_deinit(UnobstructedAreaState *state) {
  PBL_ASSERTN(state);
  event_service_client_unsubscribe(&state->event_info);
}

static void prv_put_area_event(UnobstructedAreaEventType type, int16_t current_y,
                               int16_t final_y, AnimationProgress progress) {
  PebbleEvent event = {
    .type = PEBBLE_UNOBSTRUCTED_AREA_EVENT,
    .unobstructed_area = {
      .type = type,
      .current_y = current_y,
      .final_y = final_y,
      .progress = progress,
    },
  };
  event_put(&event);
}

static void prv_clip_area(GRect *area_out) {
  PBL_ASSERTN(area_out);
  const GRect display_frame = { .size = framebuffer_get_size(app_state_get_framebuffer()) };
  grect_clip(area_out, &display_frame);
}

//! Currently, the unobstructed area is derived from the origin of the obstruction.
//! This is equivalent to the height of the unobstructed area.
static void prv_origin_y_to_area(int16_t origin_y, GRect *area_out) {
  PBL_ASSERTN(area_out);
  *area_out = (GRect) {
    .size = { DISP_COLS, origin_y },
  };
  prv_clip_area(area_out);
}

void unobstructed_area_service_will_change(int16_t current_y, int16_t final_y) {
  prv_put_area_event(UnobstructedAreaEventType_WillChange, current_y, final_y,
                     ANIMATION_NORMALIZED_MIN);
}

void unobstructed_area_service_change(int16_t current_y, int16_t final_y,
                                      AnimationProgress progress) {
  prv_put_area_event(UnobstructedAreaEventType_Change, current_y, final_y, progress);
}

void unobstructed_area_service_did_change(int16_t final_y) {
  prv_put_area_event(UnobstructedAreaEventType_DidChange, final_y, final_y,
                     ANIMATION_NORMALIZED_MAX);
}

static void prv_save_event_area(UnobstructedAreaState *state, PebbleEvent *event) {
  GRect area;
  prv_origin_y_to_area(event->unobstructed_area.current_y, &area);
  state->area = area;
  prv_clip_area(&state->area);
}

static void prv_call_will_change(UnobstructedAreaState *state, PebbleEvent *event) {
  if (state->is_changing) {
    return;
  }
  // Always call the will handler even if the app restarts or starts out of sync with
  // the animation and has had its app state reinitialized. Set `is_changing` to keep track.
  state->is_changing = true;
  if (state->handlers.will_change) {
    GRect final_area;
    prv_origin_y_to_area(event->unobstructed_area.final_y, &final_area);
    state->handlers.will_change(final_area, state->context);
  }
}

static void prv_handle_will_change_event(PebbleEvent *event, void *context) {
  UnobstructedAreaState *state = context;
  // It is the producer's responsibility not to overlap unobstructed area changes.
  PBL_ASSERTN(!state->is_changing);
  prv_call_will_change(state, event);
}

static void prv_handle_change_event(PebbleEvent *event, void *context) {
  UnobstructedAreaState *state = context;
  prv_call_will_change(state, event);
  if (state->handlers.change) {
    state->handlers.change(event->unobstructed_area.progress, state->context);
  }
}

static void prv_handle_did_change_event(PebbleEvent *event, void *context) {
  UnobstructedAreaState *state = context;
  prv_call_will_change(state, event);
  state->is_changing = false;
  if (state->handlers.did_change) {
    state->handlers.did_change(state->context);
  }
}

static void prv_handle_unobstructed_area_event(PebbleEvent *event, void *context) {
  UnobstructedAreaState *state = context;
  GRect previous_area = state->area;
  prv_save_event_area(state, event);
  if (!grect_equal(&previous_area, &state->area)) {
    app_request_render();
  }
  switch (event->unobstructed_area.type) {
    case UnobstructedAreaEventType_WillChange:
      prv_handle_will_change_event(event, context);
      break;
    case UnobstructedAreaEventType_Change:
      prv_handle_change_event(event, context);
      break;
    case UnobstructedAreaEventType_DidChange:
      prv_handle_did_change_event(event, context);
      break;
  }
}

void unobstructed_area_service_subscribe(UnobstructedAreaState *state,
                                         const UnobstructedAreaHandlers *handlers,
                                         void *context) {
  PBL_ASSERTN(state && handlers);
  state->handlers = *handlers;
  state->context = context;
}

void unobstructed_area_service_unsubscribe(UnobstructedAreaState *state) {
  PBL_ASSERTN(state);
  state->handlers = (UnobstructedAreaHandlers) {};
}

void unobstructed_area_service_get_area(UnobstructedAreaState *state, GRect *area_out) {
  if (state && area_out) {
    *area_out = state->area;
  }
}

void app_unobstructed_area_service_subscribe(UnobstructedAreaHandlers handlers, void *context) {
  unobstructed_area_service_subscribe(app_state_get_unobstructed_area_state(), &handlers, context);
}

void app_unobstructed_area_service_unsubscribe(void) {
  unobstructed_area_service_unsubscribe(app_state_get_unobstructed_area_state());
}
