// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "libcef/browser/browser_context.h"

#include <map>
#include <utility>

#include "libcef/browser/context.h"
#include "libcef/browser/media_router/media_router_manager.h"
#include "libcef/browser/request_context_impl.h"
#include "libcef/browser/thread_util.h"
#include "libcef/common/cef_switches.h"
#include "libcef/common/frame_util.h"
#include "libcef/features/runtime.h"

#include "base/files/file_util.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"

using content::BrowserThread;

namespace {

// Manages the global list of Impl instances.
class ImplManager {
 public:
  using Vector = std::vector<CefBrowserContext*>;

  ImplManager() {}

  ImplManager(const ImplManager&) = delete;
  ImplManager& operator=(const ImplManager&) = delete;

  ~ImplManager() {
    DCHECK(all_.empty());
    DCHECK(map_.empty());
  }

  void AddImpl(CefBrowserContext* impl) {
    CEF_REQUIRE_UIT();
    DCHECK(!IsValidImpl(impl));
    all_.push_back(impl);
  }

  void RemoveImpl(CefBrowserContext* impl, const base::FilePath& path) {
    CEF_REQUIRE_UIT();

    {
      Vector::iterator it = GetImplPos(impl);
      DCHECK(it != all_.end());
      all_.erase(it);
    }

    if (!path.empty()) {
      PathMap::iterator it = map_.find(path);
      DCHECK(it != map_.end());
      if (it != map_.end()) {
        map_.erase(it);
      }
    }
  }

  bool IsValidImpl(const CefBrowserContext* impl) {
    CEF_REQUIRE_UIT();
    return GetImplPos(impl) != all_.end();
  }

  CefBrowserContext* GetImplFromGlobalId(
      const content::GlobalRenderFrameHostId& global_id,
      bool require_frame_match) {
    CEF_REQUIRE_UIT();
    for (const auto& context : all_) {
      if (context->IsAssociatedContext(global_id, require_frame_match)) {
        return context;
      }
    }
    return nullptr;
  }

  CefBrowserContext* GetImplFromBrowserContext(
      const content::BrowserContext* context) {
    CEF_REQUIRE_UIT();
    if (!context) {
      return nullptr;
    }

    for (const auto& bc : all_) {
      if (bc->AsBrowserContext() == context) {
        return bc;
      }
    }
    return nullptr;
  }

  void SetImplPath(CefBrowserContext* impl, const base::FilePath& path) {
    CEF_REQUIRE_UIT();
    DCHECK(!path.empty());
    DCHECK(IsValidImpl(impl));
    DCHECK(GetImplFromPath(path) == nullptr);
    map_.insert(std::make_pair(path, impl));
  }

  CefBrowserContext* GetImplFromPath(const base::FilePath& path) {
    CEF_REQUIRE_UIT();
    DCHECK(!path.empty());
    PathMap::const_iterator it = map_.find(path);
    if (it != map_.end()) {
      return it->second;
    }
    return nullptr;
  }

  const Vector GetAllImpl() const { return all_; }

 private:
  Vector::iterator GetImplPos(const CefBrowserContext* impl) {
    Vector::iterator it = all_.begin();
    for (; it != all_.end(); ++it) {
      if (*it == impl) {
        return it;
      }
    }
    return all_.end();
  }

  using PathMap = std::map<base::FilePath, CefBrowserContext*>;
  PathMap map_;

  Vector all_;
};

#if DCHECK_IS_ON()
// Because of DCHECK()s in the object destructor.
base::LazyInstance<ImplManager>::DestructorAtExit g_manager =
    LAZY_INSTANCE_INITIALIZER;
#else
base::LazyInstance<ImplManager>::Leaky g_manager = LAZY_INSTANCE_INITIALIZER;
#endif

CefBrowserContext* GetSelf(base::WeakPtr<CefBrowserContext> self) {
  CEF_REQUIRE_UIT();
  return self.get();
}

CefBrowserContext::CookieableSchemes MakeSupportedSchemes(
    const CefString& schemes_list,
    bool include_defaults) {
  if (schemes_list.empty() && include_defaults) {
    // No explicit registration of schemes.
    return absl::nullopt;
  }

  std::vector<std::string> all_schemes;
  if (!schemes_list.empty()) {
    all_schemes =
        base::SplitString(schemes_list.ToString(), std::string(","),
                          base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  }

  if (include_defaults) {
    // Add default schemes that should always support cookies.
    // This list should match CookieMonster::kDefaultCookieableSchemes.
    all_schemes.push_back("http");
    all_schemes.push_back("https");
    all_schemes.push_back("ws");
    all_schemes.push_back("wss");
  }

  return absl::make_optional(all_schemes);
}

template <typename T>
CefBrowserContext::CookieableSchemes MakeSupportedSchemes(const T& settings) {
  return MakeSupportedSchemes(CefString(&settings.cookieable_schemes_list),
                              !settings.cookieable_schemes_exclude_defaults);
}

}  // namespace

CefBrowserContext::CefBrowserContext(const CefRequestContextSettings& settings)
    : settings_(settings), weak_ptr_factory_(this) {
  g_manager.Get().AddImpl(this);
  getter_ = base::BindRepeating(GetSelf, weak_ptr_factory_.GetWeakPtr());
}

CefBrowserContext::~CefBrowserContext() {
  CEF_REQUIRE_UIT();
#if DCHECK_IS_ON()
  DCHECK(is_shutdown_);
#endif
}

void CefBrowserContext::Initialize() {
  cache_path_ = base::FilePath(CefString(&settings_.cache_path));

  if (!cache_path_.empty()) {
    g_manager.Get().SetImplPath(this, cache_path_);
  }

  iothread_state_ = base::MakeRefCounted<CefIOThreadState>();
  cookieable_schemes_ = MakeSupportedSchemes(settings_);
}

void CefBrowserContext::Shutdown() {
  CEF_REQUIRE_UIT();

#if DCHECK_IS_ON()
  is_shutdown_ = true;
#endif

  // No CefRequestContext should be referencing this object any longer.
  DCHECK(request_context_set_.empty());

  // Unregister the context first to avoid re-entrancy during shutdown.
  g_manager.Get().RemoveImpl(this, cache_path_);

  // Destroy objects that may hold references to the MediaRouter.
  media_router_manager_.reset();

  // Invalidate any Getter references to this object.
  weak_ptr_factory_.InvalidateWeakPtrs();
}

void CefBrowserContext::AddCefRequestContext(CefRequestContextImpl* context) {
  CEF_REQUIRE_UIT();
  request_context_set_.insert(context);
}

void CefBrowserContext::RemoveCefRequestContext(
    CefRequestContextImpl* context) {
  CEF_REQUIRE_UIT();

  request_context_set_.erase(context);

  // Delete ourselves when the reference count reaches zero.
  if (request_context_set_.empty()) {
    Shutdown();

    // Allow the current call stack to unwind before deleting |this|.
    content::BrowserThread::DeleteSoon(CEF_UIT, FROM_HERE, this);
  }
}

// static
CefBrowserContext* CefBrowserContext::FromCachePath(
    const base::FilePath& cache_path) {
  return g_manager.Get().GetImplFromPath(cache_path);
}

// static
CefBrowserContext* CefBrowserContext::FromGlobalId(
    const content::GlobalRenderFrameHostId& global_id,
    bool require_frame_match) {
  return g_manager.Get().GetImplFromGlobalId(global_id, require_frame_match);
}

// static
CefBrowserContext* CefBrowserContext::FromBrowserContext(
    const content::BrowserContext* context) {
  return g_manager.Get().GetImplFromBrowserContext(context);
}

// static
CefBrowserContext* CefBrowserContext::FromProfile(const Profile* profile) {
  auto* cef_context = FromBrowserContext(profile);
  if (cef_context) {
    return cef_context;
  }

  if (cef::IsChromeRuntimeEnabled()) {
    auto* original_profile = profile->GetOriginalProfile();
    if (original_profile != profile) {
      // With the Chrome runtime if the user launches an incognito window via
      // the UI we might be associated with the original Profile instead of the
      // (current) incognito profile.
      return FromBrowserContext(original_profile);
    }
  }

  return nullptr;
}

// static
std::vector<CefBrowserContext*> CefBrowserContext::GetAll() {
  return g_manager.Get().GetAllImpl();
}

void CefBrowserContext::OnRenderFrameCreated(
    CefRequestContextImpl* request_context,
    const content::GlobalRenderFrameHostId& global_id,
    bool is_main_frame,
    bool is_guest_view) {
  CEF_REQUIRE_UIT();
  DCHECK(frame_util::IsValidGlobalId(global_id));

  render_id_set_.insert(global_id);

  CefRefPtr<CefRequestContextHandler> handler = request_context->GetHandler();
  if (handler) {
    handler_map_.AddHandler(global_id, handler);

    CEF_POST_TASK(CEF_IOT, base::BindOnce(&CefIOThreadState::AddHandler,
                                          iothread_state_, global_id, handler));
  }
}

void CefBrowserContext::OnRenderFrameDeleted(
    CefRequestContextImpl* request_context,
    const content::GlobalRenderFrameHostId& global_id,
    bool is_main_frame,
    bool is_guest_view) {
  CEF_REQUIRE_UIT();
  DCHECK(frame_util::IsValidGlobalId(global_id));

  auto it1 = render_id_set_.find(global_id);
  if (it1 != render_id_set_.end()) {
    render_id_set_.erase(it1);
  }

  CefRefPtr<CefRequestContextHandler> handler = request_context->GetHandler();
  if (handler) {
    handler_map_.RemoveHandler(global_id);

    CEF_POST_TASK(CEF_IOT, base::BindOnce(&CefIOThreadState::RemoveHandler,
                                          iothread_state_, global_id));
  }
}

CefRefPtr<CefRequestContextHandler> CefBrowserContext::GetHandler(
    const content::GlobalRenderFrameHostId& global_id,
    bool require_frame_match) const {
  CEF_REQUIRE_UIT();
  return handler_map_.GetHandler(global_id, require_frame_match);
}

bool CefBrowserContext::IsAssociatedContext(
    const content::GlobalRenderFrameHostId& global_id,
    bool require_frame_match) const {
  CEF_REQUIRE_UIT();

  if (frame_util::IsValidGlobalId(global_id)) {
    const auto it1 = render_id_set_.find(global_id);
    if (it1 != render_id_set_.end()) {
      return true;
    }
  }

  if (frame_util::IsValidChildId(global_id.child_id) && !require_frame_match) {
    // Choose an arbitrary handler for the same process.
    for (const auto& render_ids : render_id_set_) {
      if (render_ids.child_id == global_id.child_id) {
        return true;
      }
    }
  }

  return false;
}

void CefBrowserContext::RegisterSchemeHandlerFactory(
    const CefString& scheme_name,
    const CefString& domain_name,
    CefRefPtr<CefSchemeHandlerFactory> factory) {
  CEF_POST_TASK(
      CEF_IOT,
      base::BindOnce(&CefIOThreadState::RegisterSchemeHandlerFactory,
                     iothread_state_, scheme_name, domain_name, factory));
}

void CefBrowserContext::ClearSchemeHandlerFactories() {
  CEF_POST_TASK(CEF_IOT,
                base::BindOnce(&CefIOThreadState::ClearSchemeHandlerFactories,
                               iothread_state_));
}

void CefBrowserContext::LoadExtension(
    const CefString& root_directory,
    CefRefPtr<CefDictionaryValue> manifest,
    CefRefPtr<CefExtensionHandler> handler,
    CefRefPtr<CefRequestContext> loader_context) {
  NOTIMPLEMENTED();
  if (handler) {
    handler->OnExtensionLoadFailed(ERR_ABORTED);
  }
}

bool CefBrowserContext::GetExtensions(std::vector<CefString>& extension_ids) {
  NOTIMPLEMENTED();
  return false;
}

CefRefPtr<CefExtension> CefBrowserContext::GetExtension(
    const CefString& extension_id) {
  NOTIMPLEMENTED();
  return nullptr;
}

bool CefBrowserContext::UnloadExtension(const CefString& extension_id) {
  NOTIMPLEMENTED();
  return false;
}

bool CefBrowserContext::IsPrintPreviewSupported() const {
  return true;
}

network::mojom::NetworkContext* CefBrowserContext::GetNetworkContext() {
  CEF_REQUIRE_UIT();
  auto browser_context = AsBrowserContext();
  return browser_context->GetDefaultStoragePartition()->GetNetworkContext();
}

CefMediaRouterManager* CefBrowserContext::GetMediaRouterManager() {
  CEF_REQUIRE_UIT();
  if (!media_router_manager_) {
    media_router_manager_.reset(new CefMediaRouterManager(AsBrowserContext()));
  }
  return media_router_manager_.get();
}

CefBrowserContext::CookieableSchemes CefBrowserContext::GetCookieableSchemes()
    const {
  CEF_REQUIRE_UIT();
  return cookieable_schemes_;
}

// static
CefBrowserContext::CookieableSchemes
CefBrowserContext::GetGlobalCookieableSchemes() {
  CEF_REQUIRE_UIT();

  static base::NoDestructor<CookieableSchemes> schemes(
      []() { return MakeSupportedSchemes(CefContext::Get()->settings()); }());
  return *schemes;
}