#include "mpv_player.h"

#include <epoxy/egl.h>
#include <epoxy/gl.h>
#include <flutter_linux/flutter_linux.h>
#include <gdk/gdk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/gdkwayland.h>
#endif
#include <locale.h>

#include <chrono>
#include <cstring>

#include "sanitize_utf8.h"

namespace {

bool EnsureProcessNumericLocale() {
  // libmpv parses numeric options on worker threads, so a thread-local locale
  // is insufficient. This process-wide setting intentionally remains in force
  // for the rest of the process after the first player starts.
  static const bool configured = setlocale(LC_NUMERIC, "C") != nullptr;
  return configured;
}

}  // namespace

// Flutter on Linux uses EGL (OpenGL ES) for both X11 and Wayland.
static void* get_opengl_proc_address(void* ctx, const char* name) {
  (void)ctx;
  return reinterpret_cast<void*>(eglGetProcAddress(name));
}

namespace mpv {
namespace {

NativeRenderTeardownOperations ProductionTeardownOperations() {
  return {
      [](EGLDisplay display, EGLContext context) {
        if (!eglBindAPI(EGL_OPENGL_ES_API) || !eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context)) {
          g_warning("MPV: Failed to activate EGL context for teardown: 0x%x", eglGetError());
          return false;
        }
        return true;
      },
      [](EGLDisplay display) {
        if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
          g_warning("MPV: Failed to release EGL context during teardown: 0x%x", eglGetError());
          return false;
        }
        return true;
      },
      [](EGLDisplay display, EGLContext context) {
        if (!eglDestroyContext(display, context)) {
          g_warning("MPV: Failed to destroy EGL context during teardown: 0x%x", eglGetError());
          return false;
        }
        return true;
      },
      [](mpv_render_context* render) { mpv_render_context_free(render); },
      [](mpv_handle* handle) { mpv_terminate_destroy(handle); },
  };
}

#ifdef PLEZY_MPV_PLAYER_LIFECYCLE_TEST
NativeRenderTeardownOperations*& TestTeardownOperationsOverride() {
  static NativeRenderTeardownOperations* operations = nullptr;
  return operations;
}
#endif

class NativeRenderTeardownQueue {
 public:
  static NativeRenderTeardownQueue& Instance() {
    // Native driver/libmpv teardown can block indefinitely. Keep both the
    // queue and its worker state alive until the OS ends the process so static
    // destruction never joins the worker or invalidates state it may access.
    static NativeRenderTeardownQueue* const queue = new NativeRenderTeardownQueue();
    return *queue;
  }

  void Enqueue(NativeRenderTeardownBatch batch) {
    if (batch.resources.empty() && !batch.handle) return;
    {
      std::lock_guard<std::mutex> lock(mutex_);
      batches_.push_back(std::move(batch));
      ++generation_;
    }
    condition_.notify_one();
  }

  void Retry() {
    {
      std::lock_guard<std::mutex> lock(mutex_);
      ++generation_;
    }
    condition_.notify_one();
  }

 private:
  NativeRenderTeardownQueue() : worker_([this]() { Run(); }) {}

  void Run() {
#ifdef PLEZY_MPV_PLAYER_LIFECYCLE_TEST
    const NativeRenderTeardownOperations operations =
        TestTeardownOperationsOverride() ? *TestTeardownOperationsOverride() : ProductionTeardownOperations();
#else
    const NativeRenderTeardownOperations operations = ProductionTeardownOperations();
#endif
    std::unique_lock<std::mutex> lock(mutex_);
    for (;;) {
      condition_.wait(lock, [this]() { return !batches_.empty(); });
      const uint64_t observed_generation = generation_;
      std::vector<NativeRenderTeardownBatch> work = std::move(batches_);
      batches_.clear();

      // EGL activation and mpv shutdown can block in a driver. Keep queue
      // admission independent so replacement initialization and disposal only
      // pay the short ownership-transfer critical section.
      lock.unlock();
      std::vector<NativeRenderTeardownBatch> retry;
      for (auto& batch : work) {
        if (!TryReleaseNativeRenderTeardown(batch, operations)) retry.push_back(std::move(batch));
      }
      lock.lock();
      for (auto& batch : retry) batches_.push_back(std::move(batch));

      if (batches_.empty()) continue;

      condition_.wait_for(lock, std::chrono::milliseconds(100), [this, observed_generation]() {
        return generation_ != observed_generation;
      });
    }
  }

  std::mutex mutex_;
  std::condition_variable condition_;
  std::vector<NativeRenderTeardownBatch> batches_;
  uint64_t generation_ = 0;
  std::thread worker_;
};

}  // namespace

#ifdef PLEZY_MPV_PLAYER_LIFECYCLE_TEST
void ConfigureNativeRenderTeardownQueueForTesting(NativeRenderTeardownOperations operations) {
  auto*& configured = TestTeardownOperationsOverride();
  if (configured) {
    *configured = std::move(operations);
  } else {
    configured = new NativeRenderTeardownOperations(std::move(operations));
  }
}

void EnqueueNativeRenderTeardownForTesting(NativeRenderTeardownBatch batch) {
  NativeRenderTeardownQueue::Instance().Enqueue(std::move(batch));
}
#endif

bool TryReleaseNativeRenderTeardown(
    NativeRenderTeardownBatch& batch, const NativeRenderTeardownOperations& operations) {
  for (auto it = batch.resources.begin(); it != batch.resources.end();) {
    if (it->context == EGL_NO_CONTEXT || !operations.make_current(it->display, it->context)) {
      ++it;
      continue;
    }

    if (it->render) {
      operations.free_render(it->render);
      it->render = nullptr;
    }
    if (!operations.release_current(it->display)) {
      ++it;
      continue;
    }
    if (!operations.destroy_context(it->display, it->context)) {
      ++it;
      continue;
    }
    it = batch.resources.erase(it);
  }

  if (!batch.resources.empty()) return false;
  if (batch.handle) {
    operations.terminate_handle(batch.handle);
    batch.handle = nullptr;
  }
  batch.callback_keep_alive.reset();
  return true;
}

bool TryReleaseRetainedNativeRenderContexts(
    std::vector<NativeRenderTeardownResource>& resources, const NativeRenderTeardownOperations& operations) {
  NativeRenderTeardownBatch batch;
  batch.resources = std::move(resources);
  const bool complete = TryReleaseNativeRenderTeardown(batch, operations);
  resources = std::move(batch.resources);
  return complete;
}

MpvPlayer::CallbackContext::Lease::Lease(CallbackContext* context, MpvPlayer* player)
    : context_(context), player_(player) {}

MpvPlayer::CallbackContext::Lease::Lease(Lease&& other) noexcept : context_(other.context_), player_(other.player_) {
  other.context_ = nullptr;
  other.player_ = nullptr;
}

MpvPlayer::CallbackContext::Lease& MpvPlayer::CallbackContext::Lease::operator=(Lease&& other) noexcept {
  if (this != &other) {
    Release();
    context_ = other.context_;
    player_ = other.player_;
    other.context_ = nullptr;
    other.player_ = nullptr;
  }
  return *this;
}

MpvPlayer::CallbackContext::Lease::~Lease() { Release(); }

void MpvPlayer::CallbackContext::Lease::Release() {
  if (!context_) return;
  context_->ReleaseLease();
  context_ = nullptr;
  player_ = nullptr;
}

MpvPlayer::CallbackContext::CallbackContext(MpvPlayer* player)
    : player_(player), main_context_(g_main_context_ref_thread_default()) {}

MpvPlayer::CallbackContext::~CallbackContext() { g_main_context_unref(main_context_); }

MpvPlayer::CallbackContext::Lease MpvPlayer::CallbackContext::Acquire() {
  std::lock_guard<std::mutex> lock(mutex_);
  if (!player_) return Lease();
  ++in_flight_;
  return Lease(this, player_);
}

void MpvPlayer::CallbackContext::WaitUntilDetached() {
  std::unique_lock<std::mutex> lock(mutex_);
  quiescent_.wait(lock, [this]() { return player_ == nullptr; });
}
void MpvPlayer::CallbackContext::DetachAndWait() {
  std::unique_lock<std::mutex> lock(mutex_);
  player_ = nullptr;
  quiescent_.notify_all();
  quiescent_.wait(lock, [this]() { return in_flight_ == 0; });
}

void MpvPlayer::CallbackContext::ReleaseLease() {
  std::lock_guard<std::mutex> lock(mutex_);
  --in_flight_;
  if (in_flight_ == 0) quiescent_.notify_all();
}

struct MpvPlayer::SourceCallbackData {
  explicit SourceCallbackData(std::shared_ptr<CallbackContext> callback_context)
      : context(std::move(callback_context)) {}

  std::shared_ptr<CallbackContext> context;
  guint source_id = 0;
};

MpvPlayer::MpvPlayer(bool audio_only)
    : audio_only_(audio_only), callback_context_(std::make_shared<CallbackContext>(this)) {}

MpvPlayer::~MpvPlayer() { Dispose(); }

bool MpvPlayer::HasRenderContext() const {
  std::lock_guard<std::mutex> lock(native_mutex_);
  return mpv_gl_ != nullptr;
}

EGLDisplay MpvPlayer::GetEglDisplay() const {
  std::lock_guard<std::mutex> lock(native_mutex_);
  return egl_display_;
}

EGLContext MpvPlayer::GetEglContext() const {
  std::lock_guard<std::mutex> lock(native_mutex_);
  return egl_context_;
}

bool MpvPlayer::IsInitialized() const {
  std::lock_guard<std::mutex> lock(native_mutex_);
  return mpv_ != nullptr && (audio_only_ || mpv_gl_ != nullptr);
}

bool MpvPlayer::HasMpvHandle() const {
  std::lock_guard<std::mutex> lock(native_mutex_);
  return mpv_ != nullptr;
}

bool MpvPlayer::Initialize() {
  std::lock_guard<std::mutex> lock(native_mutex_);
  if (disposed_) {
    g_warning("MPV: initialization requested after disposal");
    return false;
  }
  if (mpv_) {
    return true;  // Already initialized.
  }

  if (!EnsureProcessNumericLocale()) {
    g_warning("MPV: Failed to establish the process-wide C numeric locale");
    return false;
  }

  // Create mpv instance.
  mpv_ = mpv_create();
  if (!mpv_) {
    g_warning("MPV: mpv_create() failed");
    return false;
  }

  if (audio_only_) {
    // Music core: no VO, no video decode. vid=no keeps embedded cover art
    // from ever becoming a video track, and force-window/audio-display make
    // sure mpv never opens a video output for it either.
    mpv_set_option_string(mpv_, "vid", "no");
    mpv_set_option_string(mpv_, "force-window", "no");
    mpv_set_option_string(mpv_, "audio-display", "no");
    mpv_set_option_string(mpv_, "gapless-audio", "weak");
  } else {
    // Configure mpv for embedded playback.
    mpv_set_option_string(mpv_, "vo", "libmpv");
    mpv_set_option_string(mpv_, "hwdec", "auto");
  }
  mpv_set_option_string(mpv_, "keep-open", "yes");
  mpv_set_option_string(mpv_, "audio-fallback-to-null", "yes");

  if (!audio_only_) {
    // HDR tone mapping
    mpv_set_option_string(mpv_, "tone-mapping", "auto");
    mpv_set_option_string(mpv_, "target-colorspace-hint", plezy::mpv_common::TargetColorspaceHint(hdr_enabled_));
    mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
  }
  mpv_set_option_string(mpv_, "idle", "yes");
  mpv_set_option_string(mpv_, "input-default-bindings", "no");
  mpv_set_option_string(mpv_, "input-vo-keyboard", "no");
  mpv_set_option_string(mpv_, "osc", "no");
  mpv_set_option_string(mpv_, "terminal", "no");

  // Default to warn-level logging
  mpv_request_log_messages(mpv_, "warn");

  // Initialize mpv.
  int err = mpv_initialize(mpv_);
  if (err < 0) {
    g_warning("MPV: mpv_initialize() failed: %s", mpv_error_string(err));
    mpv_destroy(mpv_);
    mpv_ = nullptr;
    return false;
  }

  // Set up event wakeup callback.
  mpv_set_wakeup_callback(mpv_, OnMpvWakeup, callback_context_.get());
  mpv_observe_property(mpv_, 0, "current-ao", MPV_FORMAT_STRING);
  mpv_observe_property(mpv_, 0, "audio-device-list", MPV_FORMAT_NONE);

  g_message("MPV: Initialization successful (%s)", audio_only_ ? "audio-only" : "render context deferred");
  return true;
}

void MpvPlayer::RetryPendingNativeTeardown() { NativeRenderTeardownQueue::Instance().Retry(); }

bool MpvPlayer::InitRenderContext() {
  RetryPendingNativeTeardown();

  std::lock_guard<std::mutex> lock(native_mutex_);
  if (audio_only_ || disposed_) {
    g_warning("MPV: Render context requested for an unavailable player");
    return false;
  }
  if (mpv_gl_) return true;
  if (!mpv_) {
    g_warning("MPV: Cannot create render context - mpv not initialized");
    return false;
  }

  const EGLDisplay flutter_display = eglGetCurrentDisplay();
  const EGLContext flutter_context = eglGetCurrentContext();
  const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
  const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
  const EGLenum previous_api = eglQueryAPI();
  if (flutter_display == EGL_NO_DISPLAY || flutter_context == EGL_NO_CONTEXT || previous_api == EGL_NONE) {
    g_warning("MPV: No EGL context available");
    return false;
  }

  auto restore_flutter = [&]() {
    const EGLBoolean api_restored = previous_api == EGL_NONE ? EGL_TRUE : eglBindAPI(previous_api);
    const EGLBoolean restored = api_restored == EGL_TRUE
                                    ? eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context)
                                    : EGL_FALSE;
    return restored == EGL_TRUE && api_restored == EGL_TRUE;
  };
  if (!retained_render_contexts_.empty()) {
    const bool released =
        TryReleaseRetainedNativeRenderContexts(retained_render_contexts_, ProductionTeardownOperations());
    const bool flutter_restored = restore_flutter();
    if (!released) {
      g_warning("MPV: Retained render context still requires a later EGL teardown retry");
    }
    if (!flutter_restored) {
      g_warning("MPV: Failed to restore Flutter EGL state after retained teardown: 0x%x", eglGetError());
    }
    if (!released || !flutter_restored) return false;
  }

  EGLint config_id = 0;
  if (!eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id)) {
    g_warning("MPV: Failed to query Flutter EGL config: 0x%x", eglGetError());
    return false;
  }
  EGLConfig config = nullptr;
  EGLint num_configs = 0;
  const EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE};
  if (!eglChooseConfig(flutter_display, config_attribs, &config, 1, &num_configs) || num_configs != 1) {
    g_warning("MPV: Failed to select Flutter EGL config: 0x%x", eglGetError());
    return false;
  }
  if (!eglBindAPI(EGL_OPENGL_ES_API)) {
    g_warning("MPV: Failed to bind OpenGL ES API: 0x%x", eglGetError());
    return false;
  }

  const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
  EGLContext candidate_context = eglCreateContext(flutter_display, config, EGL_NO_CONTEXT, context_attribs);
  if (candidate_context == EGL_NO_CONTEXT) {
    g_warning("MPV: Failed to create isolated EGL context: 0x%x", eglGetError());
    if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
      g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError());
    }
    return false;
  }

  auto destroy_candidate_context = [&]() {
    const EGLenum api_before_cleanup = eglQueryAPI();
    if (eglGetCurrentContext() == candidate_context) {
      if (!eglBindAPI(EGL_OPENGL_ES_API) ||
          !eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
        g_warning("MPV: Failed to release rejected EGL context: 0x%x", eglGetError());
        return;
      }
    }
    if (!eglDestroyContext(flutter_display, candidate_context)) {
      g_warning("MPV: Failed to destroy rejected EGL context: 0x%x", eglGetError());
    }
    if (api_before_cleanup != EGL_NONE && !eglBindAPI(api_before_cleanup)) {
      g_warning("MPV: Failed to restore EGL API after context cleanup: 0x%x", eglGetError());
    }
  };

  if (!eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, candidate_context)) {
    g_warning("MPV: Failed to activate isolated EGL context: 0x%x", eglGetError());
    destroy_candidate_context();
    if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
      g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError());
    }
    return false;
  }

  mpv_opengl_init_params gl_init_params{};
  gl_init_params.get_proc_address = get_opengl_proc_address;
  gl_init_params.get_proc_address_ctx = nullptr;
  mpv_render_param params[] = {
      {MPV_RENDER_PARAM_API_TYPE, const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
      {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
      {MPV_RENDER_PARAM_INVALID, nullptr},
      {MPV_RENDER_PARAM_INVALID, nullptr},
  };

  GdkDisplay* gdk_display = gdk_display_get_default();
#ifdef GDK_WINDOWING_WAYLAND
  if (GDK_IS_WAYLAND_DISPLAY(gdk_display)) {
    params[2].type = MPV_RENDER_PARAM_WL_DISPLAY;
    params[2].data = gdk_wayland_display_get_wl_display(gdk_display);
  }
#endif
#ifdef GDK_WINDOWING_X11
  if (GDK_IS_X11_DISPLAY(gdk_display)) {
    params[2].type = MPV_RENDER_PARAM_X11_DISPLAY;
    params[2].data = gdk_x11_display_get_xdisplay(gdk_display);
  }
#endif

  mpv_render_context* candidate_gl = nullptr;
  const int error = mpv_render_context_create(&candidate_gl, mpv_, params);
  const bool restored = restore_flutter();
  if (error < 0 || candidate_gl == nullptr || !restored) {
    if (error < 0) {
      g_warning("MPV: mpv_render_context_create() failed: %s", mpv_error_string(error));
    } else if (!restored) {
      g_warning("MPV: Failed to restore Flutter EGL state: 0x%x", eglGetError());
    } else {
      g_warning("MPV: mpv returned a null render context");
    }
    bool retained_candidate = false;
    if (candidate_gl) {
      if (eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, candidate_context)) {
        mpv_render_context_free(candidate_gl);
      } else {
        g_warning("MPV: Failed to reactivate rejected EGL context: 0x%x; retaining it for teardown", eglGetError());
        retained_render_contexts_.push_back({candidate_gl, flutter_display, candidate_context});
        retained_candidate = true;
      }
      if (!restore_flutter()) {
        g_warning("MPV: Failed final Flutter EGL restoration: 0x%x", eglGetError());
      }
    }
    if (!retained_candidate) destroy_candidate_context();
    return false;
  }

  egl_display_ = flutter_display;
  egl_context_ = candidate_context;
  mpv_gl_ = candidate_gl;
  mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, callback_context_.get());
  g_message("MPV: Render context created with isolated EGL context");
  return true;
}

void MpvPlayer::Dispose() {
  if (disposed_.exchange(true)) {
    return;
  }

  // Stop native producers before revoking access to the player. A callback
  // already entered on an mpv thread owns a lease and is allowed to finish.
  {
    std::lock_guard<std::mutex> lock(native_mutex_);
    if (mpv_) {
      const char* stop_command[] = {"stop", nullptr};
      const int stop_result = mpv_command_async(mpv_, 0, stop_command);
      if (stop_result < 0) {
        g_warning("MPV: Failed to enqueue stop during disposal: %s", mpv_error_string(stop_result));
      }
    }
    if (mpv_gl_) {
      mpv_render_context_set_update_callback(mpv_gl_, nullptr, nullptr);
    }
    if (mpv_) {
      mpv_set_wakeup_callback(mpv_, nullptr, nullptr);
    }
  }
  callback_context_->DetachAndWait();

  {
    std::lock_guard<std::mutex> lock(callback_mutex_);
    redraw_callback_ = nullptr;
    event_callback_ = nullptr;
  }

  auto cancelled = pending_requests_.CancelAll();
  for (auto& callback : cancelled.status) {
    callback(MPV_ERROR_UNINITIALIZED);
  }
  for (auto& callback : cancelled.properties) {
    callback(-1, "");
  }

  RemoveTrackedSources();

  // Transfer every render/context pair and the shared mpv handle to the
  // managed teardown thread. A failed EGL bind leaves the complete pair in
  // the queue; the handle cannot be terminated until every pair is gone.
  NativeRenderTeardownBatch teardown;
  {
    std::lock_guard<std::mutex> lock(native_mutex_);
    teardown.resources = std::move(retained_render_contexts_);
    if (mpv_gl_ || egl_context_ != EGL_NO_CONTEXT) {
      teardown.resources.push_back({mpv_gl_, egl_display_, egl_context_});
    }
    teardown.handle = mpv_;
    teardown.callback_keep_alive = callback_context_;
    mpv_gl_ = nullptr;
    mpv_ = nullptr;
    egl_display_ = EGL_NO_DISPLAY;
    egl_context_ = EGL_NO_CONTEXT;
  }
  NativeRenderTeardownQueue::Instance().Enqueue(std::move(teardown));

  observed_properties_.Clear();
}

void MpvPlayer::Render(int width, int height, int fbo) {
  std::lock_guard<std::mutex> lock(native_mutex_);
  if (disposed_ || !mpv_gl_) return;

  mpv_opengl_fbo mpv_fbo{};
  mpv_fbo.fbo = fbo;
  mpv_fbo.w = width;
  mpv_fbo.h = height;
  mpv_fbo.internal_format = 0;

  int flip_y = 0;
  mpv_render_param params[] = {
      {MPV_RENDER_PARAM_OPENGL_FBO, &mpv_fbo},
      {MPV_RENDER_PARAM_FLIP_Y, &flip_y},
      {MPV_RENDER_PARAM_INVALID, nullptr},
  };
  mpv_render_context_render(mpv_gl_, params);
}

void MpvPlayer::Command(const std::vector<std::string>& args) { CommandAsync(args, nullptr); }

void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
  if (disposed_ || !mpv_) {
    if (callback) callback(MPV_ERROR_UNINITIALIZED);
    return;
  }

  plezy::mpv_common::SubmitCommandAsync(mpv_, pending_requests_, args, std::move(callback));
}

void MpvPlayer::SetProperty(const std::string& name, const std::string& value) {
  SetPropertyAsync(name, value, nullptr);
}

void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback) {
  if (disposed_ || !mpv_) {
    if (callback) callback(MPV_ERROR_UNINITIALIZED);
    return;
  }

  if (name == "hdr-enabled") {
    SetHDREnabled(plezy::mpv_common::ParseEnabledFlag(value), std::move(callback));
    return;
  }

  if (name == "pause" && !plezy::mpv_common::ParseEnabledFlag(value)) {
    auto completion = std::move(callback);
    callback = [this, completion = std::move(completion)](int error) {
      if (error >= 0 && !disposed_) {
        audio_recovery_.RequestResume();
        EnsureAudioRecoveryTimer();
      }
      if (completion) completion(error);
    };
  }
  plezy::mpv_common::SubmitSetPropertyAsync(mpv_, pending_requests_, name, value, std::move(callback));
}

void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback callback) {
  if (disposed_ || !mpv_) {
    if (callback) callback(MPV_ERROR_UNINITIALIZED, "");
    return;
  }

  plezy::mpv_common::SubmitGetPropertyAsync(mpv_, pending_requests_, name, std::move(callback));
}

void MpvPlayer::ObserveProperty(const std::string& name, const std::string& format, int id) {
  if (disposed_ || !mpv_) return;

  const auto request = observed_properties_.Register(name, format, id);
  if (!request.added) return;
  mpv_observe_property(mpv_, request.userdata, name.c_str(), request.format);
}

void MpvPlayer::ReportMouseMove(int x, int y) {
  if (disposed_ || !mpv_) return;
  std::string x_str = std::to_string(x);
  std::string y_str = std::to_string(y);
  const char* args[] = {"mouse", x_str.c_str(), y_str.c_str(), nullptr};
  mpv_command_async(mpv_, 0, args);
}

void MpvPlayer::SetEventCallback(EventCallback callback) {
  std::lock_guard<std::mutex> lock(callback_mutex_);
  event_callback_ = std::move(callback);
}

void MpvPlayer::SetRedrawCallback(RedrawCallback callback) {
  std::lock_guard<std::mutex> lock(callback_mutex_);
  redraw_callback_ = std::move(callback);
}

void MpvPlayer::SetLogLevel(const std::string& level) {
  if (disposed_ || !mpv_) return;
  mpv_request_log_messages(mpv_, level.c_str());
}

void MpvPlayer::OnMpvWakeup(void* ctx) {
  auto* context = static_cast<CallbackContext*>(ctx);
  auto lease = context->Acquire();
  if (!lease) return;

  MpvPlayer* player = lease.player();
  if (!player->disposed_) {
    player->ScheduleWakeupSource();
  }
}

void MpvPlayer::OnMpvRenderUpdate(void* ctx) {
  auto* context = static_cast<CallbackContext*>(ctx);
  auto lease = context->Acquire();
  if (!lease) return;

  MpvPlayer* player = lease.player();
  if (player->disposed_) return;

  bool expected = false;
  if (!player->needs_redraw_.compare_exchange_strong(expected, true)) {
    return;
  }

  // Flutter texture notification must run on the player's owning GLib
  // context, never on mpv's render/VO thread.
  player->ScheduleRedrawSource();
}

void MpvPlayer::DestroySourceCallbackData(gpointer data) { delete static_cast<SourceCallbackData*>(data); }

void MpvPlayer::ScheduleWakeupSource() {
  std::lock_guard<std::mutex> lock(source_mutex_);
  if (disposed_ || wakeup_source_id_ != 0) return;

  GSource* source = g_idle_source_new();
  g_source_set_priority(source, G_PRIORITY_HIGH_IDLE);
  auto* data = new SourceCallbackData(callback_context_);
  g_source_set_callback(source, DispatchWakeupSource, data, DestroySourceCallbackData);
  data->source_id = g_source_attach(source, callback_context_->main_context());
  wakeup_source_id_ = data->source_id;
  g_source_unref(source);
}

void MpvPlayer::ScheduleRedrawSource() {
  std::lock_guard<std::mutex> lock(source_mutex_);
  if (disposed_ || redraw_source_id_ != 0) return;

  GSource* source = g_idle_source_new();
  auto* data = new SourceCallbackData(callback_context_);
  g_source_set_callback(source, DispatchRedrawSource, data, DestroySourceCallbackData);
  data->source_id = g_source_attach(source, callback_context_->main_context());
  redraw_source_id_ = data->source_id;
  g_source_unref(source);

  if (redraw_source_id_ == 0) {
    needs_redraw_ = false;
  }
}

void MpvPlayer::ScheduleRecoverySource() {
  std::lock_guard<std::mutex> lock(source_mutex_);
  if (disposed_ || recovery_source_id_ != 0) return;

  GSource* source = g_timeout_source_new(100);
  auto* data = new SourceCallbackData(callback_context_);
  g_source_set_callback(source, DispatchRecoverySource, data, DestroySourceCallbackData);
  data->source_id = g_source_attach(source, callback_context_->main_context());
  recovery_source_id_ = data->source_id;
  g_source_unref(source);
}

gboolean MpvPlayer::DispatchWakeupSource(gpointer data) {
  auto* source_data = static_cast<SourceCallbackData*>(data);
  auto lease = source_data->context->Acquire();
  if (!lease) return G_SOURCE_REMOVE;

  MpvPlayer* player = lease.player();
  {
    std::lock_guard<std::mutex> lock(player->source_mutex_);
    if (player->wakeup_source_id_ == source_data->source_id) {
      player->wakeup_source_id_ = 0;
    }
  }
  if (!player->disposed_ && player->mpv_) {
    player->ProcessEvents();
  }
  return G_SOURCE_REMOVE;
}

gboolean MpvPlayer::DispatchRedrawSource(gpointer data) {
  auto* source_data = static_cast<SourceCallbackData*>(data);
  auto lease = source_data->context->Acquire();
  if (!lease) return G_SOURCE_REMOVE;

  MpvPlayer* player = lease.player();
  {
    std::lock_guard<std::mutex> lock(player->source_mutex_);
    if (player->redraw_source_id_ == source_data->source_id) {
      player->redraw_source_id_ = 0;
    }
  }
  if (player->disposed_) return G_SOURCE_REMOVE;

  RedrawCallback callback;
  {
    std::lock_guard<std::mutex> lock(player->callback_mutex_);
    callback = player->redraw_callback_;
  }
  if (callback) callback();
  return G_SOURCE_REMOVE;
}

gboolean MpvPlayer::DispatchRecoverySource(gpointer data) {
  auto* source_data = static_cast<SourceCallbackData*>(data);
  auto lease = source_data->context->Acquire();
  if (!lease) return G_SOURCE_REMOVE;

  MpvPlayer* player = lease.player();
  if (player->disposed_) return G_SOURCE_REMOVE;

  player->MaybeRunAudioRecovery();
  if (player->audio_recovery_.HasPendingWork()) {
    return G_SOURCE_CONTINUE;
  }

  std::lock_guard<std::mutex> lock(player->source_mutex_);
  if (player->recovery_source_id_ == source_data->source_id) {
    player->recovery_source_id_ = 0;
  }
  return G_SOURCE_REMOVE;
}

void MpvPlayer::RemoveTrackedSources() {
  std::lock_guard<std::mutex> lock(source_mutex_);
  GMainContext* context = callback_context_->main_context();
  auto remove = [context](guint& source_id) {
    if (source_id == 0) return;
    GSource* source = g_main_context_find_source_by_id(context, source_id);
    if (source) g_source_destroy(source);
    source_id = 0;
  };
  remove(wakeup_source_id_);
  remove(redraw_source_id_);
  remove(recovery_source_id_);
}

bool MpvPlayer::ProcessEvents() {
  if (disposed_ || !mpv_) return false;

  while (true) {
    mpv_event* event = mpv_wait_event(mpv_, 0);
    if (event->event_id == MPV_EVENT_NONE) {
      break;
    }
    if (event->event_id == MPV_EVENT_SHUTDOWN) {
      return false;
    }
    HandleMpvEvent(event);
  }
  return true;
}

void MpvPlayer::LogRecovery(const std::string& text) {
  g_warning("MPV audio-recovery: %s", text.c_str());
  FlValue* data = fl_value_new_map();
  fl_value_set_string_take(data, "prefix", fl_value_new_string("audio-recovery"));
  fl_value_set_string_take(data, "level", fl_value_new_string("warn"));
  fl_value_set_string_take(data, "text", fl_value_new_string(text.c_str()));
  SendEvent("log-message", data);
  fl_value_unref(data);
}

void MpvPlayer::TryAudioReload(const char* reason, int attempt, uint64_t request_generation) {
  LogRecovery("issuing ao-reload (reason=" + std::string(reason) + ", attempt " + std::to_string(attempt) + ")");
  const std::string reason_copy = reason;
  auto callback_context = callback_context_;
  CommandAsync({"ao-reload"}, [callback_context, reason_copy, attempt, request_generation](int error) {
    auto lease = callback_context->Acquire();
    if (!lease) return;
    MpvPlayer* player = lease.player();
    player->audio_recovery_.CompleteReload(request_generation);
    player->LogRecovery(
        "ao-reload completed (reason=" + reason_copy + ", attempt " + std::to_string(attempt) +
        ", error=" + std::to_string(error) + ")");
  });
}

void MpvPlayer::MaybeRunAudioRecovery() {
  const auto action = audio_recovery_.NextReload(plezy::mpv_common::AudioRecoveryState::Clock::now());
  if (action.reason == plezy::mpv_common::AudioReloadReason::kNone) {
    return;
  }
  const char* reason = action.reason == plezy::mpv_common::AudioReloadReason::kResume ? "resume" : "null-fallback";
  TryAudioReload(reason, action.attempt, action.request_generation);
  if (action.exhausted) {
    LogRecovery("audio recovery budget exhausted; waiting for device list change");
  }
}

void MpvPlayer::EnsureAudioRecoveryTimer() {
  if (!audio_recovery_.HasPendingWork()) return;
  ScheduleRecoverySource();
}

void MpvPlayer::HandleMpvEvent(mpv_event* event) {
  if (plezy::mpv_common::DispatchReplyEvent(
          pending_requests_, event, [](const char* value) { return SanitizeUtf8(value); })) {
    return;
  }

  switch (event->event_id) {
    case MPV_EVENT_LOG_MESSAGE: {
      auto* msg = static_cast<mpv_event_log_message*>(event->data);
      if (!msg) break;
      g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);

      FlValue* data = fl_value_new_map();
      fl_value_set_string_take(data, "prefix", fl_value_new_string(SanitizeUtf8(msg->prefix).c_str()));
      fl_value_set_string_take(data, "level", fl_value_new_string(SanitizeUtf8(msg->level).c_str()));
      fl_value_set_string_take(data, "text", fl_value_new_string(SanitizeUtf8(msg->text).c_str()));
      SendEvent("log-message", data);
      fl_value_unref(data);
      break;
    }
    case MPV_EVENT_PROPERTY_CHANGE: {
      auto* prop = static_cast<mpv_event_property*>(event->data);
      if (!prop || !prop->name) break;
      mpv_node node = plezy::mpv_common::ExtractPropertyNode(prop);

      const auto notice = plezy::mpv_common::ObserveAudioRecoveryProperty(audio_recovery_, event, prop);
      if (notice.message) LogRecovery(notice.message);
      // Recovery runs off a GLib timer here, so newly queued work has to arm it.
      if (notice.scheduled_work) EnsureAudioRecoveryTimer();

      SendPropertyChange(prop->name, &node);
      break;
    }
    case MPV_EVENT_END_FILE: {
      audio_recovery_.SetFileLoaded(false);
      auto* end = static_cast<mpv_event_end_file*>(event->data);
      if (!end) break;
      FlValue* data = fl_value_new_map();
      fl_value_set_string_take(data, "reason", fl_value_new_int(static_cast<int>(end->reason)));
      if (end->reason == MPV_END_FILE_REASON_ERROR) {
        fl_value_set_string_take(data, "error", fl_value_new_int(static_cast<int>(end->error)));
        fl_value_set_string_take(
            data, "message", fl_value_new_string(SanitizeUtf8(mpv_error_string(end->error)).c_str()));
      }
      SendEvent("end-file", data);
      fl_value_unref(data);
      break;
    }
    case MPV_EVENT_START_FILE: {
      SendEvent("start-file");
      break;
    }
    case MPV_EVENT_FILE_LOADED: {
      audio_recovery_.SetFileLoaded(true);
      EnsureAudioRecoveryTimer();
      SendEvent("file-loaded");
      break;
    }
    case MPV_EVENT_PLAYBACK_RESTART: {
      SendEvent("playback-restart");
      break;
    }
    default:
      break;
  }
}

namespace {

// Adapts the shared, bounded mpv_node walk onto GLib-owned FlValues.
struct FlValueNodeBuilder {
  using Value = FlValue*;
  using ListBuilder = FlValue*;
  using MapBuilder = FlValue*;

  static Value Null() { return fl_value_new_null(); }
  static Value Boolean(bool value) { return fl_value_new_bool(value); }
  static Value Int(int64_t value) { return fl_value_new_int(value); }
  static Value Double(double value) { return fl_value_new_float(value); }
  static Value String(const char* value, size_t length) {
    return fl_value_new_string(SanitizeUtf8(value, length).c_str());
  }

  static ListBuilder NewList() { return fl_value_new_list(); }
  static void Append(ListBuilder& list, Value value) { fl_value_append_take(list, value); }
  static Value FinishList(ListBuilder list) { return list; }

  static MapBuilder NewMap() { return fl_value_new_map(); }
  static void Insert(MapBuilder& map, const char* key, size_t key_length, Value value) {
    fl_value_set_string_take(map, SanitizeUtf8(key, key_length).c_str(), value);
  }
  static Value FinishMap(MapBuilder map) { return map; }
  static void AbandonMap(MapBuilder& map) { fl_value_unref(map); }
};

}  // namespace

FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) { return plezy::mpv_common::ConvertNode<FlValueNodeBuilder>(node); }

FlValue* MpvPlayer::NodeToFlValue(mpv_node* node, plezy::mpv_common::NodeConversionBudget* budget) {
  return plezy::mpv_common::ConvertNode<FlValueNodeBuilder>(node, 0, budget);
}

void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
  if (!name) return;

  int id = 0;
  if (!observed_properties_.LookupId(name, &id)) return;

  FlValue* list = fl_value_new_list();
  fl_value_append_take(list, fl_value_new_int(id));
  if (data) {
    fl_value_append_take(list, NodeToFlValue(data));
  } else {
    fl_value_append_take(list, fl_value_new_null());
  }

  EventCallback callback;
  {
    std::lock_guard<std::mutex> lock(callback_mutex_);
    callback = event_callback_;
  }
  if (callback) callback(list);
  fl_value_unref(list);
}

void MpvPlayer::SendEvent(const std::string& name, FlValue* data) {
  FlValue* event_map = fl_value_new_map();
  fl_value_set_string_take(event_map, "type", fl_value_new_string("event"));
  fl_value_set_string_take(event_map, "name", fl_value_new_string(name.c_str()));
  if (data) {
    fl_value_set_string_take(event_map, "data", fl_value_ref(data));
  }

  EventCallback callback;
  {
    std::lock_guard<std::mutex> lock(callback_mutex_);
    callback = event_callback_;
  }
  if (callback) callback(event_map);
  fl_value_unref(event_map);
}

void MpvPlayer::SetHDREnabled(bool enabled, StatusCallback callback) {
  SetPropertyAsync(
      "target-colorspace-hint", plezy::mpv_common::TargetColorspaceHint(enabled),
      [this, enabled, callback = std::move(callback)](int error) mutable {
        if (plezy::mpv_common::SetPropertyStatusSucceeded(error) && !disposed_) {
          hdr_enabled_ = enabled;
        }
        if (callback) callback(error);
      });
}
}  // namespace mpv
