/*
 *  Copyright (C) 2026 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "CaptureService.h"

#include "rendering/capture/CaptureHandle.h"
#include "utils/log.h"

#include <mutex>

namespace KODI
{
namespace RENDERING
{
namespace CAPTURE
{

CCaptureService::~CCaptureService()
{
  FailAll();
}

std::unique_ptr<CCaptureHandle> CCaptureService::Submit(const CaptureSpec& spec,
                                                        CaptureCallback callback)
{
  auto request = std::make_shared<CaptureRequest>();
  request->spec = spec;
  request->callback = std::move(callback);

  // BOTH delivers a video-only and a composite capture, told apart only by
  // result.content in the callback; Wait/CopyResult could hand back only one
  if (spec.content == CaptureContent::BOTH && !request->callback)
  {
    CLog::LogF(LOGERROR, "BOTH capture requires a callback; Wait/CopyResult "
                         "cannot tell the video-only and composite captures apart");
    request->state = CaptureState::FAILED;
    request->event.Set();
    return std::make_unique<CCaptureHandle>(*this, std::move(request));
  }

  {
    std::unique_lock lock(m_lock);
    m_pending.push_back(request);
    m_hasPending.store(true, std::memory_order_release);
  }

  return std::make_unique<CCaptureHandle>(*this, std::move(request));
}

bool CCaptureService::LatchFrame()
{
  if (!m_hasPending.load(std::memory_order_acquire) && m_stack.empty())
    return false;

  bool needsFrame = false;

  {
    std::unique_lock lock(m_lock);
    m_hasPending.store(false, std::memory_order_relaxed);

    std::erase_if(m_stack,
                  [](const std::shared_ptr<CaptureRequest>& request) {
                    return request->state == CaptureState::CANCELLED ||
                           request->state == CaptureState::FAILED;
                  });

    // arriving requests push onto the stack top in submission order; each
    // forces one frame so an idle GUI still renders something to tap
    for (auto& request : m_pending)
    {
      if (request->state != CaptureState::PENDING)
        continue;
      m_stack.push_back(request);
      needsFrame = true;
    }
    m_pending.clear();

    // one consumer per render loop: the most recent request wins whatever its
    // cadence, so a one-shot submitted just before a continuous is buried
    // until the continuous is gone (a rare late or lost screenshot, accepted)
    for (auto& request : m_stack)
    {
      if (request->state == CaptureState::LATCHED)
        request->state = CaptureState::PENDING;
    }
    std::shared_ptr<CaptureRequest> latchedRequest;
    if (!m_stack.empty())
      latchedRequest = m_stack.back();

    if (latchedRequest)
    {
      latchedRequest->served = false; // FrameComplete judges this at the end of the frame
      latchedRequest->state = CaptureState::LATCHED;
    }

    // the latched one-shot forces frames until it serves or FrameComplete fails
    // it; a buried one-shot waits quietly and a lone CONTINUOUS never forces,
    // so unchanged frames are still skipped during a steady continuous capture
    if (latchedRequest && latchedRequest->spec.cadence == CaptureCadence::ONESHOT)
      needsFrame = true;
  }

  return needsFrame;
}

std::vector<std::shared_ptr<CaptureRequest>> CCaptureService::TakeActive(CaptureContent content)
{
  std::vector<std::shared_ptr<CaptureRequest>> matches;
  std::unique_lock lock(m_lock);
  // exactly one request is LATCHED per frame; return it wherever it sits. A
  // BOTH request is served by the video tap and the composite tap alike.
  for (const auto& request : m_stack)
  {
    if (request->state != CaptureState::LATCHED)
      continue;
    if (request->spec.content == content || request->spec.content == CaptureContent::BOTH)
      matches.push_back(request);
    break;
  }
  return matches;
}

void CCaptureService::Complete(const std::shared_ptr<CaptureRequest>& request, CaptureResult result)
{
  bool dispatch = false;
  bool finished = false;

  {
    std::unique_lock lock(m_lock);
    if (request->state == CaptureState::LATCHED)
    {
      request->result = result;
      request->deliveries++;
      request->served = true;
      dispatch = true;
      if (request->spec.cadence == CaptureCadence::ONESHOT)
      {
        // a BOTH request keeps running after its video-only delivery so the
        // composite tap can deliver in the same frame; the composite tap
        // always runs, after video, so it finishes there
        if (!(request->spec.content == CaptureContent::BOTH &&
              result.content == CaptureContent::VIDEO))
        {
          request->state = CaptureState::DONE;
          finished = true;
        }
      }
    }
    else
    {
      // cancelled while a tap was servicing it
      finished = true;
    }
  }

  request->event.Set();

  if (finished)
    RemoveActive(request);
  // dispatch a copy so a second delivery cannot overwrite the first callback's
  // result before the worker reads it (a BOTH request delivers twice)
  if (dispatch && request->callback)
    Dispatch(request, result);
}

void CCaptureService::Fail(const std::shared_ptr<CaptureRequest>& request)
{
  bool failed = false;

  {
    std::unique_lock lock(m_lock);
    if (request->state == CaptureState::LATCHED || request->state == CaptureState::PENDING)
    {
      request->state = CaptureState::FAILED;
      failed = true;
    }
  }

  if (failed)
    CLog::LogF(LOGERROR, "capture request failed (content {}, {}x{})",
               static_cast<int>(request->spec.content), request->spec.width, request->spec.height);

  request->event.Set();
  if (failed)
  {
    RemoveActive(request);
    // a callback consumer learns of the failure from the empty result
    if (request->callback)
      Dispatch(request, {});
  }
}

void CCaptureService::FrameComplete()
{
  std::shared_ptr<CaptureRequest> unserved;

  {
    std::unique_lock lock(m_lock);
    for (const auto& request : m_stack)
    {
      if (request->state != CaptureState::LATCHED)
        continue;
      if (!request->served)
        unserved = request;
      break; // at most one request is latched per frame
    }
  }

  if (unserved)
    Fail(unserved);
}

void CCaptureService::Cancel(const std::shared_ptr<CaptureRequest>& request)
{
  {
    std::unique_lock lock(m_lock);
    if (request->state == CaptureState::DONE || request->state == CaptureState::FAILED)
      return;
    request->state = CaptureState::CANCELLED;
  }

  request->event.Set();
}

CaptureState CCaptureService::GetState(const CaptureRequest& request) const
{
  std::unique_lock lock(m_lock);
  return request.state;
}

uint64_t CCaptureService::GetDeliveries(const CaptureRequest& request) const
{
  std::unique_lock lock(m_lock);
  return request.deliveries;
}

CaptureResult CCaptureService::CopyResult(const CaptureRequest& request) const
{
  std::unique_lock lock(m_lock);
  return request.result;
}

void CCaptureService::FailAll()
{
  std::vector<std::shared_ptr<CaptureRequest>> all;

  {
    std::unique_lock lock(m_lock);
    for (auto& request : m_pending)
    {
      if (request->state == CaptureState::PENDING)
        request->state = CaptureState::FAILED;
    }
    all = std::move(m_pending);
    m_pending.clear();

    for (auto& request : m_stack)
    {
      if (request->state == CaptureState::LATCHED || request->state == CaptureState::PENDING)
        request->state = CaptureState::FAILED;
    }
    all.insert(all.end(), m_stack.begin(), m_stack.end());
    m_stack.clear();
  }

  for (auto& request : all)
    request->event.Set();

  m_callbackQueue.CancelJobs();
}

void CCaptureService::Dispatch(const std::shared_ptr<CaptureRequest>& request, CaptureResult result)
{
  m_callbackQueue.Submit([request, result = std::move(result)] { request->callback(result); });
}

void CCaptureService::RemoveActive(const std::shared_ptr<CaptureRequest>& request)
{
  std::unique_lock lock(m_lock);
  std::erase(m_stack, request);
}

} // namespace CAPTURE
} // namespace RENDERING
} // namespace KODI
