// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/no_destructor.h"
#include "base/task/thread_pool.h"
#include "base/threading/sequence_local_storage_slot.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/browser/service_process_host_passkeys.h"
#include "content/public/browser/video_capture_service.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/video_capture/video_capture_service_impl.h"

#if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
#include "base/functional/bind.h"
#include "base/task/bind_post_task.h"
#include "content/browser/gpu/browser_gpu_channel_host_factory.h"
#include "gpu/command_buffer/client/shared_image_interface.h"
#include "media/base/media_switches.h"
#include "media/capture/video/video_capture_gpu_channel_host.h"
#include "media/media_buildflags.h"
#endif

#if BUILDFLAG(IS_WIN)
#define CREATE_IN_PROCESS_TASK_RUNNER base::ThreadPool::CreateCOMSTATaskRunner
#else
#define CREATE_IN_PROCESS_TASK_RUNNER \
  base::ThreadPool::CreateSingleThreadTaskRunner
#endif

namespace content {

// Helper class to allow access to class-based passkeys.
class VideoCaptureServiceLauncher {
 public:
  static void Launch(
      mojo::PendingReceiver<video_capture::mojom::VideoCaptureService>
          receiver) {
    ServiceProcessHost::Options options;
    options.WithDisplayName("Video Capture");
    // TODO(https://crbug.com/328099369) Remove once gpu client is provided
    // directly.
    options.WithGpuClient(ServiceProcessHostGpuClient::GetPassKey());
#if BUILDFLAG(IS_MAC)
    // On Mac, the service requires a CFRunLoop which is provided by a
    // UI message loop. See https://crbug.com/40572495.
    options.WithExtraCommandLineSwitches({switches::kMessageLoopTypeUi});
#endif
#if defined(WEBRTC_USE_PIPEWIRE)
    // The PipeWire camera implementation in webrtc uses gdbus for portal
    // handling, so the glib message loop must be used.
    options.WithExtraCommandLineSwitches({switches::kMessageLoopTypeUi});
#endif

    ServiceProcessHost::Launch(std::move(receiver), options.Pass());
  }
};

namespace {

video_capture::mojom::VideoCaptureService* g_service_override = nullptr;

// TODO: The lifetime of the in-process video capture service is currently
// managed by two disjoint static variables: the `SequenceLocalStorageSlot` for
// the Remote and the `static base::NoDestructor` here for the service
// implementation. This is fragile and drops subsequent receivers if called
// multiple times. These should be merged into a single unified manager object.
void BindInProcessInstance(
    mojo::PendingReceiver<video_capture::mojom::VideoCaptureService> receiver,
    scoped_refptr<gpu::GpuChannelHost> gpu_channel_host) {
  static base::NoDestructor<video_capture::VideoCaptureServiceImpl> service(
      std::move(receiver), GetUIThreadTaskRunner({}),
      /*create_system_monitor=*/false);

#if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
  service->SetGpuChannelHost(
      std::move(gpu_channel_host),
      base::BindPostTask(
          GetUIThreadTaskRunner({}),
          base::BindRepeating([](gpu::GpuChannelEstablishedCallback callback) {
            auto* factory = BrowserGpuChannelHostFactory::instance();
            if (factory) {
              factory->EstablishGpuChannel(std::move(callback));
            } else {
              std::move(callback).Run(nullptr);
            }
          })));
#endif
}

mojo::Remote<video_capture::mojom::VideoCaptureService>& GetUIThreadRemote() {
  // NOTE: This use of sequence-local storage is only to ensure that the Remote
  // only lives as long as the UI-thread sequence, since the UI-thread sequence
  // may be torn down and reinitialized e.g. between unit tests.
  static base::SequenceLocalStorageSlot<
      mojo::Remote<video_capture::mojom::VideoCaptureService>>
      remote_slot;
  return remote_slot.GetOrCreateValue();
}

// This is a custom traits type we use in conjunction with mojo::ReceiverSetBase
// so that all dispatched messages can be forwarded to the currently bound UI
// thread Remote.
struct ForwardingImplRefTraits {
  using PointerType = void*;
  static bool IsNull(PointerType) { return false; }
  static video_capture::mojom::VideoCaptureService* GetRawPointer(PointerType) {
    return &GetVideoCaptureService();
  }
};

// If |GetVideoCaptureService()| is called from off the UI thread, return a
// sequence-local Remote. Its corresponding receiver will be bound in this set,
// forwarding to the current UI-thread Remote.
void BindProxyRemoteOnUIThread(
    mojo::PendingReceiver<video_capture::mojom::VideoCaptureService> receiver) {
  static base::NoDestructor<mojo::ReceiverSetBase<
      mojo::Receiver<video_capture::mojom::VideoCaptureService,
                     ForwardingImplRefTraits>,
      void>>
      receivers;
  receivers->Add(nullptr, std::move(receiver));
}

}  // namespace

video_capture::mojom::VideoCaptureService& GetVideoCaptureService() {
  if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
    static base::SequenceLocalStorageSlot<
        mojo::Remote<video_capture::mojom::VideoCaptureService>>
        storage;
    auto& remote = storage.GetOrCreateValue();
    if (!remote.is_bound()) {
      GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE, base::BindOnce(&BindProxyRemoteOnUIThread,
                                    remote.BindNewPipeAndPassReceiver()));
    }
    return *remote.get();
  }

  if (g_service_override) {
    return *g_service_override;
  }

  auto& remote = GetUIThreadRemote();
  if (!remote.is_bound()) {
    auto receiver = remote.BindNewPipeAndPassReceiver();
    if (features::IsVideoCaptureServiceEnabledForBrowserProcess()) {
      auto dedicated_task_runner = CREATE_IN_PROCESS_TASK_RUNNER(
          {base::MayBlock(), base::WithBaseSyncPrimitives(),
           base::TaskPriority::BEST_EFFORT},
          base::SingleThreadTaskRunnerThreadMode::DEDICATED);

#if BUILDFLAG(IS_ANDROID) && BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
      auto* factory = BrowserGpuChannelHostFactory::instance();
      if (base::FeatureList::IsEnabled(media::kAndroidZeroCopyVideoCapture) &&
          factory) {
        factory->EstablishGpuChannel(base::BindOnce(
            [](mojo::PendingReceiver<video_capture::mojom::VideoCaptureService>
                   receiver,
               scoped_refptr<base::SingleThreadTaskRunner> task_runner,
               scoped_refptr<gpu::GpuChannelHost> gpu_channel_host) {
              task_runner->PostTask(
                  FROM_HERE,
                  base::BindOnce(&BindInProcessInstance, std::move(receiver),
                                 std::move(gpu_channel_host)));
            },
            std::move(receiver), dedicated_task_runner));
      } else {
        dedicated_task_runner->PostTask(
            FROM_HERE, base::BindOnce(&BindInProcessInstance,
                                      std::move(receiver), nullptr));
      }
#else
      dedicated_task_runner->PostTask(
          FROM_HERE,
          base::BindOnce(&BindInProcessInstance, std::move(receiver), nullptr));
#endif
    } else {
      // Launch in a utility service.
      VideoCaptureServiceLauncher::Launch(std::move(receiver));
#if !BUILDFLAG(IS_ANDROID)
      // On Android, we do not use automatic service shutdown, because when
      // shutting down the service, we lose caching of the supported formats,
      // and re-querying these can take several seconds on certain Android
      // devices.
      remote.set_idle_handler(
          base::Seconds(5),
          base::BindRepeating(
              [](mojo::Remote<video_capture::mojom::VideoCaptureService>*
                     remote) { remote->reset(); },
              &remote));
#endif  // !BUILDFLAG(IS_ANDROID)

      // Make sure the Remote is also reset in case of e.g. service crash so we
      // can restart it as needed.
      remote.reset_on_disconnect();
    }
  }

  return *remote.get();
}

void OverrideVideoCaptureServiceForTesting(  // IN-TEST
    video_capture::mojom::VideoCaptureService* service) {
  g_service_override = service;
}

}  // namespace content
