#include "common/method_channel_utils.h"
#include "plugin/coreframe_web_view_host_plugin_private.h"
#include "webview/webview_internal.h"

#include <cairo.h>
#include <webkit2/webkit2.h>

namespace {

constexpr guint kPaintPollIntervalMs = 16;
constexpr guint kPaintPollAttempts = 30;

struct PresentationCapture {
  WebKitWebView *web_view = nullptr;
  FlMethodCall *method_call = nullptr;
  gchar *token = nullptr;
  gchar *entry_key = nullptr;
  gchar *root_web_runtime_id = nullptr;
  gchar *application_scope_id = nullptr;
  gint64 presentation_revision = 0;
  guint paint_source_id = 0;
  guint readiness_source_id = 0;
  guint readiness_attempts_remaining = 120;
  guint paint_attempts_remaining = kPaintPollAttempts;
  gboolean responded = FALSE;
  guint64 trusted_epoch = 0;
  GByteArray *png_bytes = nullptr;
};

void read_identity(PresentationCapture *capture);
void read_identity_after_snapshot(PresentationCapture *capture);
void settle_finished_cb(GObject *source_object, GAsyncResult *result,
                        gpointer user_data);

LinuxWebView *current_webview(PresentationCapture *capture) {
  return capture == nullptr || capture->web_view == nullptr
             ? nullptr
             : static_cast<LinuxWebView *>(g_object_get_data(
                   G_OBJECT(capture->web_view), kLinuxWebViewInstanceKey));
}

gboolean capture_is_available(PresentationCapture *capture) {
  LinuxWebView *webview = current_webview(capture);
  // The WebView is a GTK overlay owned by Flutter. During a frame handoff GTK
  // can briefly report an unmapped widget or a stale allocation even though
  // Flutter has published a visible, positive geometry. The WebKit snapshot
  // operation is asynchronous and is the authoritative readiness check.
  if (webview == nullptr || webview->web_view != capture->web_view ||
      !webview->visible || webview->frame_width <= 0 ||
      webview->frame_height <= 0) {
    return FALSE;
  }
  if (webview->trusted_configured &&
      (!trusted_document_is_active(webview) ||
       webview->trusted_epoch != capture->trusted_epoch)) {
    return FALSE;
  }
  const gchar *uri = webkit_web_view_get_uri(webview->web_view);
  return uri != nullptr && *uri != '\0';
}

void destroy_capture(PresentationCapture *capture) {
  if (capture == nullptr) return;
  if (capture->paint_source_id != 0) {
    g_source_remove(capture->paint_source_id);
    capture->paint_source_id = 0;
  }
  if (capture->readiness_source_id != 0) {
    g_source_remove(capture->readiness_source_id);
    capture->readiness_source_id = 0;
  }
  if (LinuxWebView *webview = current_webview(capture); webview != nullptr) {
    webview->presentation_capture_in_progress = FALSE;
  }
  g_clear_object(&capture->web_view);
  g_clear_object(&capture->method_call);
  g_free(capture->token);
  g_free(capture->entry_key);
  g_free(capture->root_web_runtime_id);
  g_free(capture->application_scope_id);
  if (capture->png_bytes != nullptr) g_byte_array_unref(capture->png_bytes);
  g_free(capture);
}

void finish_capture(PresentationCapture *capture, FlValue *value) {
  if (capture == nullptr) return;
  if (!capture->responded) {
    capture->responded = TRUE;
    respond(capture->method_call, success_response(value));
  } else if (value != nullptr) {
    fl_value_unref(value);
  }
  destroy_capture(capture);
}

void fail_capture(PresentationCapture *capture) {
  finish_capture(capture, nullptr);
}

void begin_settle(PresentationCapture *capture);

void retry_settle(PresentationCapture *capture) {
  if (capture == nullptr || capture->responded) return;
  if (capture->readiness_attempts_remaining == 0) {
    fail_capture(capture);
    return;
  }
  --capture->readiness_attempts_remaining;
  capture->readiness_source_id = g_timeout_add(
      kPaintPollIntervalMs,
      [](gpointer user_data) -> gboolean {
        PresentationCapture *capture =
            static_cast<PresentationCapture *>(user_data);
        capture->readiness_source_id = 0;
        begin_settle(capture);
        return G_SOURCE_REMOVE;
      },
      capture);
  if (capture->readiness_source_id == 0) fail_capture(capture);
}

void begin_settle(PresentationCapture *capture) {
  if (capture == nullptr || capture->responded) return;
  if (!capture_is_available(capture)) {
    retry_settle(capture);
    return;
  }
  static constexpr gchar kSettleBody[] =
      "const element=document.activeElement;const end=element?.selectionEnd;"
      "if(typeof end==='number'&&typeof element?.setSelectionRange==='function')"
      "element.setSelectionRange(end,end);"
      "globalThis.getSelection?.()?.removeAllRanges();"
      "if(element instanceof HTMLElement)element.blur();"
      "return new Promise((resolve)=>{"
      "let settled=false;"
      "const mark=()=>{if(settled)return;settled=true;"
      "document.documentElement.dataset.coreframePaintToken=token;"
      "resolve(true);};"
      "requestAnimationFrame(()=>requestAnimationFrame(mark));"
      "setTimeout(mark,100);"
      "});";
  GVariantDict arguments;
  g_variant_dict_init(&arguments, nullptr);
  g_variant_dict_insert(&arguments, "token", "s", capture->token);
  GVariant *values = g_variant_dict_end(&arguments);
  webkit_web_view_call_async_javascript_function(
      capture->web_view, kSettleBody, -1, values, nullptr, nullptr, nullptr,
      settle_finished_cb, capture);
  g_variant_unref(values);
}

gchar *property_string(JSCValue *object, const gchar *name) {
  if (object == nullptr || !jsc_value_is_object(object)) return nullptr;
  JSCValue *property = jsc_value_object_get_property(object, name);
  if (property == nullptr || jsc_value_is_null(property) ||
      jsc_value_is_undefined(property)) {
    if (property != nullptr) g_object_unref(property);
    return nullptr;
  }
  gchar *result = jsc_value_to_string(property);
  g_object_unref(property);
  return result;
}

gboolean read_identity(JSCValue *value, PresentationCapture *capture) {
  if (value == nullptr || !jsc_value_is_object(value)) return FALSE;
  gchar *entry_key = property_string(value, "entryKey");
  gchar *root_web_runtime_id = property_string(value, "rootWebRuntimeId");
  gchar *application_scope_id = property_string(value, "applicationScopeId");
  JSCValue *revision_value =
      jsc_value_object_get_property(value, "presentationRevision");
  const gint32 revision = revision_value == nullptr
                              ? 0
                              : jsc_value_to_int32(revision_value);
  const gboolean valid = entry_key != nullptr && *entry_key != '\0' &&
                         root_web_runtime_id != nullptr &&
                         *root_web_runtime_id != '\0' &&
                         application_scope_id != nullptr &&
                         *application_scope_id != '\0' && revision > 0 &&
                         g_strcmp0(entry_key, capture->entry_key) == 0 &&
                         g_strcmp0(root_web_runtime_id,
                                   capture->root_web_runtime_id) == 0 &&
                         g_strcmp0(application_scope_id,
                                   capture->application_scope_id) == 0 &&
                         revision == capture->presentation_revision;
  if (revision_value != nullptr) g_object_unref(revision_value);
  g_free(entry_key);
  g_free(root_web_runtime_id);
  g_free(application_scope_id);
  return valid;
}

FlValue *identity_value(PresentationCapture *capture) {
  FlValue *identity = fl_value_new_map();
  fl_value_set_string_take(identity, "entryKey",
                           fl_value_new_string(capture->entry_key));
  fl_value_set_string_take(
      identity, "presentationRevision",
      fl_value_new_int(capture->presentation_revision));
  fl_value_set_string_take(
      identity, "rootWebRuntimeId",
      fl_value_new_string(capture->root_web_runtime_id));
  fl_value_set_string_take(identity, "applicationScopeId",
                           fl_value_new_string(capture->application_scope_id));
  return identity;
}

cairo_status_t append_png_bytes(void *closure, const unsigned char *data,
                                unsigned int length) {
  GByteArray *bytes = static_cast<GByteArray *>(closure);
  g_byte_array_append(bytes, data, length);
  return CAIRO_STATUS_SUCCESS;
}

void snapshot_finished_cb(GObject *source_object, GAsyncResult *result,
                          gpointer user_data) {
  PresentationCapture *capture =
      static_cast<PresentationCapture *>(user_data);
  if (!capture_is_available(capture)) {
    fail_capture(capture);
    return;
  }

  GError *error = nullptr;
  cairo_surface_t *surface = webkit_web_view_get_snapshot_finish(
      WEBKIT_WEB_VIEW(source_object), result, &error);
  if (error != nullptr || surface == nullptr ||
      cairo_surface_get_type(surface) != CAIRO_SURFACE_TYPE_IMAGE ||
      cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS ||
      cairo_image_surface_get_width(surface) <= 0 ||
      cairo_image_surface_get_height(surface) <= 0) {
    g_clear_error(&error);
    if (surface != nullptr) cairo_surface_destroy(surface);
    fail_capture(capture);
    return;
  }

  GByteArray *bytes = g_byte_array_new();
  const cairo_status_t status =
      cairo_surface_write_to_png_stream(surface, append_png_bytes, bytes);
  cairo_surface_destroy(surface);
  if (status != CAIRO_STATUS_SUCCESS || bytes->len == 0) {
    g_byte_array_unref(bytes);
    fail_capture(capture);
    return;
  }

  capture->png_bytes = bytes;
  read_identity_after_snapshot(capture);
}

void start_snapshot(PresentationCapture *capture) {
  if (!capture_is_available(capture)) {
    fail_capture(capture);
    return;
  }
  webkit_web_view_get_snapshot(
      capture->web_view, WEBKIT_SNAPSHOT_REGION_VISIBLE,
      WEBKIT_SNAPSHOT_OPTIONS_NONE, nullptr, snapshot_finished_cb, capture);
}

void identity_finished_cb(GObject *source_object, GAsyncResult *result,
                          gpointer user_data) {
  PresentationCapture *capture =
      static_cast<PresentationCapture *>(user_data);
  if (!capture_is_available(capture)) {
    fail_capture(capture);
    return;
  }
  GError *error = nullptr;
  JSCValue *value = webkit_web_view_evaluate_javascript_finish(
      WEBKIT_WEB_VIEW(source_object), result, &error);
  const gboolean valid = error == nullptr && read_identity(value, capture);
  if (value != nullptr) g_object_unref(value);
  g_clear_error(&error);
  if (!valid) {
    fail_capture(capture);
    return;
  }
  start_snapshot(capture);
}

void final_identity_finished_cb(GObject *source_object, GAsyncResult *result,
                                gpointer user_data) {
  PresentationCapture *capture =
      static_cast<PresentationCapture *>(user_data);
  if (!capture_is_available(capture)) {
    fail_capture(capture);
    return;
  }

  GError *error = nullptr;
  JSCValue *value = webkit_web_view_evaluate_javascript_finish(
      WEBKIT_WEB_VIEW(source_object), result, &error);
  const gboolean valid = error == nullptr && read_identity(value, capture);
  if (value != nullptr) g_object_unref(value);
  g_clear_error(&error);
  if (!valid || capture->png_bytes == nullptr) {
    fail_capture(capture);
    return;
  }

  GByteArray *bytes = capture->png_bytes;
  capture->png_bytes = nullptr;
  FlValue *response = fl_value_new_map();
  fl_value_set_string_take(response, "identity", identity_value(capture));
  fl_value_set_string_take(
      response, "png", fl_value_new_uint8_list(bytes->data, bytes->len));
  g_byte_array_unref(bytes);
  finish_capture(capture, response);
}

void read_identity_after_snapshot(PresentationCapture *capture) {
  static constexpr gchar kIdentityScript[] =
      "(()=>{const d=document.documentElement.dataset;return {"
      "entryKey:d.coreframeEntryKey||'',"
      "presentationRevision:Number(d.coreframePresentationRevision||0),"
      "rootWebRuntimeId:d.coreframeRootWebRuntimeId||'',"
      "applicationScopeId:d.coreframeApplicationScopeId||''};})()";
  webkit_web_view_evaluate_javascript(
      capture->web_view, kIdentityScript, -1, nullptr, nullptr, nullptr,
      final_identity_finished_cb, capture);
}

void read_identity(PresentationCapture *capture) {
  static constexpr gchar kIdentityScript[] =
      "(()=>{const d=document.documentElement.dataset;return {"
      "entryKey:d.coreframeEntryKey||'',"
      "presentationRevision:Number(d.coreframePresentationRevision||0),"
      "rootWebRuntimeId:d.coreframeRootWebRuntimeId||'',"
      "applicationScopeId:d.coreframeApplicationScopeId||''};})()";
  webkit_web_view_evaluate_javascript(
      capture->web_view, kIdentityScript, -1, nullptr, nullptr, nullptr,
      identity_finished_cb, capture);
}

void paint_probe_finished_cb(GObject *source_object, GAsyncResult *result,
                             gpointer user_data) {
  PresentationCapture *capture =
      static_cast<PresentationCapture *>(user_data);
  if (!capture_is_available(capture)) {
    fail_capture(capture);
    return;
  }
  GError *error = nullptr;
  JSCValue *value = webkit_web_view_evaluate_javascript_finish(
      WEBKIT_WEB_VIEW(source_object), result, &error);
  gchar *paint_token = error == nullptr && value != nullptr
                           ? jsc_value_to_string(value)
                           : nullptr;
  if (value != nullptr) g_object_unref(value);
  const gboolean painted =
      paint_token != nullptr && g_strcmp0(paint_token, capture->token) == 0;
  g_free(paint_token);
  g_clear_error(&error);
  if (painted) {
    read_identity(capture);
    return;
  }
  if (capture->paint_attempts_remaining == 0) {
    fail_capture(capture);
    return;
  }
  --capture->paint_attempts_remaining;
  capture->paint_source_id = g_timeout_add(
      kPaintPollIntervalMs,
      [](gpointer user_data) -> gboolean {
        PresentationCapture *capture =
            static_cast<PresentationCapture *>(user_data);
        capture->paint_source_id = 0;
        if (!capture_is_available(capture)) {
          fail_capture(capture);
          return G_SOURCE_REMOVE;
        }
        static constexpr gchar kPaintScript[] =
            "document.documentElement.dataset.coreframePaintToken||''";
        webkit_web_view_evaluate_javascript(
            capture->web_view, kPaintScript, -1, nullptr, nullptr, nullptr,
            paint_probe_finished_cb, capture);
        return G_SOURCE_REMOVE;
      },
      capture);
  if (capture->paint_source_id == 0) fail_capture(capture);
}

void settle_finished_cb(GObject *source_object, GAsyncResult *result,
                        gpointer user_data) {
  PresentationCapture *capture =
      static_cast<PresentationCapture *>(user_data);
  if (!capture_is_available(capture)) {
    retry_settle(capture);
    return;
  }
  GError *error = nullptr;
  JSCValue *value = webkit_web_view_call_async_javascript_function_finish(
      WEBKIT_WEB_VIEW(source_object), result, &error);
  if (error != nullptr || value == nullptr) {
    g_clear_error(&error);
    if (value != nullptr) g_object_unref(value);
    fail_capture(capture);
    return;
  }
  g_clear_error(&error);
  g_object_unref(value);
  capture->paint_source_id = g_timeout_add(
      kPaintPollIntervalMs,
      [](gpointer user_data) -> gboolean {
        PresentationCapture *capture =
            static_cast<PresentationCapture *>(user_data);
        capture->paint_source_id = 0;
        if (!capture_is_available(capture)) {
          fail_capture(capture);
          return G_SOURCE_REMOVE;
        }
        static constexpr gchar kPaintScript[] =
            "document.documentElement.dataset.coreframePaintToken||''";
        webkit_web_view_evaluate_javascript(
            capture->web_view, kPaintScript, -1, nullptr, nullptr, nullptr,
            paint_probe_finished_cb, capture);
        return G_SOURCE_REMOVE;
      },
      capture);
  if (capture->paint_source_id == 0) fail_capture(capture);
}

}  // namespace

gboolean trusted_document_is_active(LinuxWebView *webview) {
  if (webview == nullptr || webview->web_view == nullptr ||
      !webview->trusted_configured || !webview->trusted_active ||
      !webview->trusted_ready || webview->trusted_authorized_uri == nullptr) {
    return FALSE;
  }
  const gchar *uri = webkit_web_view_get_uri(webview->web_view);
  if (uri == nullptr || g_strcmp0(uri, webview->trusted_authorized_uri) != 0) {
    return FALSE;
  }
  GError *error = nullptr;
  GUri *parsed = g_uri_parse(uri, G_URI_FLAGS_NONE, &error);
  const gchar *scheme = parsed == nullptr ? nullptr : g_uri_get_scheme(parsed);
  const gchar *host = parsed == nullptr ? nullptr : g_uri_get_host(parsed);
  const gint port = parsed == nullptr ? -1 : g_uri_get_port(parsed);
  gchar *origin = scheme != nullptr && host != nullptr && port > 0
                      ? g_strdup_printf("%s://%s:%d", scheme, host, port)
                      : nullptr;
  const gboolean trusted = parsed != nullptr &&
                             g_strcmp0(origin, webview->trusted_origin) == 0 &&
                             g_uri_get_userinfo(parsed) == nullptr;
  g_free(origin);
  g_clear_error(&error);
  if (parsed != nullptr) g_uri_unref(parsed);
  return trusted;
}

void capture_presentation_artifact(LinuxWebView *webview,
                                   FlMethodCall *method_call) {
  if (method_call == nullptr) return;
  FlValue *args = fl_method_call_get_args(method_call);
  const gchar *entry_key = map_lookup_string(args, "entryKey");
  const gchar *root_web_runtime_id =
      map_lookup_string(args, "rootWebRuntimeId");
  const gchar *application_scope_id =
      map_lookup_string(args, "applicationScopeId");
  const gint64 presentation_revision =
      map_lookup_int(args, "presentationRevision", 0);
  if (webview == nullptr || webview->web_view == nullptr ||
      entry_key == nullptr ||
      root_web_runtime_id == nullptr || application_scope_id == nullptr ||
      *entry_key == '\0' || *root_web_runtime_id == '\0' ||
      *application_scope_id == '\0' || presentation_revision < 1 ||
      webview->presentation_capture_in_progress) {
    respond(method_call, success_response());
    return;
  }
  if (webview->trusted_configured && !trusted_document_is_active(webview)) {
    respond(method_call, success_response());
    return;
  }
  const gchar *uri = webkit_web_view_get_uri(webview->web_view);
  if (uri == nullptr || *uri == '\0') {
    respond(method_call, success_response());
    return;
  }

  PresentationCapture *capture = g_new0(PresentationCapture, 1);
  capture->web_view = WEBKIT_WEB_VIEW(g_object_ref(webview->web_view));
  capture->method_call = FL_METHOD_CALL(g_object_ref(method_call));
  capture->token = g_strdup_printf(
      "coreframe-%" G_GUINT64_FORMAT,
      static_cast<guint64>(g_get_monotonic_time()));
  capture->entry_key = g_strdup(entry_key);
  capture->root_web_runtime_id = g_strdup(root_web_runtime_id);
  capture->application_scope_id = g_strdup(application_scope_id);
  capture->presentation_revision = presentation_revision;
  capture->trusted_epoch = webview->trusted_epoch;
  webview->presentation_capture_in_progress = TRUE;

  begin_settle(capture);
}
