// Copyright (c) 2012 The Chromium Embedded Framework 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_info.h"

#include "libcef/browser/browser_host_base.h"
#include "libcef/browser/thread_util.h"
#include "libcef/common/frame_util.h"
#include "libcef/common/values_impl.h"

#include "base/logging.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/render_process_host.h"
#include "ipc/ipc_message.h"

CefBrowserInfo::FrameInfo::~FrameInfo() {
#if DCHECK_IS_ON()
  if (frame_ && !IsCurrentMainFrame()) {
    // Should already be Detached.
    DCHECK(!frame_->GetRenderFrameHost());
  }
#endif
}

CefBrowserInfo::CefBrowserInfo(int browser_id,
                               bool is_popup,
                               bool is_windowless,
                               CefRefPtr<CefDictionaryValue> extra_info)
    : browser_id_(browser_id),
      is_popup_(is_popup),
      is_windowless_(is_windowless),
      extra_info_(extra_info) {
  DCHECK_GT(browser_id, 0);
}

CefBrowserInfo::~CefBrowserInfo() {
  DCHECK(frame_info_set_.empty());
}

CefRefPtr<CefBrowserHostBase> CefBrowserInfo::browser() const {
  base::AutoLock lock_scope(lock_);
  if (!is_closing_)
    return browser_;
  return nullptr;
}

void CefBrowserInfo::SetBrowser(CefRefPtr<CefBrowserHostBase> browser) {
  NotificationStateLock lock_scope(this);

  if (browser) {
    DCHECK(!browser_);

    // Cache the associated frame handler.
    if (auto client = browser->GetClient()) {
      frame_handler_ = client->GetFrameHandler();
    }
  } else {
    DCHECK(browser_);
  }

  auto old_browser = browser_;
  browser_ = browser;

  if (!browser_) {
    RemoveAllFrames(old_browser);

    // Any future calls to MaybeExecuteFrameNotification will now fail.
    // NotificationStateLock already took a reference for the delivery of any
    // notifications that are currently queued due to RemoveAllFrames.
    frame_handler_ = nullptr;
  }
}

void CefBrowserInfo::SetClosing() {
  base::AutoLock lock_scope(lock_);
  DCHECK(!is_closing_);
  is_closing_ = true;
}

void CefBrowserInfo::MaybeCreateFrame(content::RenderFrameHost* host,
                                      bool is_guest_view) {
  CEF_REQUIRE_UIT();

  const auto global_id = host->GetGlobalId();
  const bool is_main_frame = (host->GetParent() == nullptr);

  // A speculative RFH will be created in response to a browser-initiated
  // cross-origin navigation (e.g. via LoadURL) and eventually either discarded
  // or swapped in based on whether the navigation is committed. We'll create a
  // frame object for the speculative RFH so that it can be found by
  // frame/routing ID. However, we won't replace the main frame with a
  // speculative RFH until after it's swapped in, and we'll generally prefer to
  // return a non-speculative RFH for the same node ID if one exists.
  const bool is_speculative = (static_cast<content::RenderFrameHostImpl*>(host)
                                   ->frame_tree_node()
                                   ->render_manager()
                                   ->current_frame_host() != host);

  NotificationStateLock lock_scope(this);
  DCHECK(browser_);

  const auto it = frame_id_map_.find(global_id);
  if (it != frame_id_map_.end()) {
    auto info = it->second;

#if DCHECK_IS_ON()
    // Check that the frame info hasn't changed unexpectedly.
    DCHECK_EQ(info->global_id_, global_id);
    DCHECK_EQ(info->is_guest_view_, is_guest_view);
    DCHECK_EQ(info->is_main_frame_, is_main_frame);
#endif

    if (!info->is_guest_view_ && info->is_speculative_ && !is_speculative) {
      // Upgrade the frame info from speculative to non-speculative.
      if (info->is_main_frame_) {
        // Set the main frame object.
        SetMainFrame(browser_, info->frame_);
      }
      info->is_speculative_ = false;
    }
    return;
  }

  auto frame_info = new FrameInfo;
  frame_info->host_ = host;
  frame_info->global_id_ = global_id;
  frame_info->is_guest_view_ = is_guest_view;
  frame_info->is_main_frame_ = is_main_frame;
  frame_info->is_speculative_ = is_speculative;

  // Guest views don't get their own CefBrowser or CefFrame objects.
  if (!is_guest_view) {
    // Create a new frame object.
    frame_info->frame_ = new CefFrameHostImpl(this, host);
    MaybeNotifyFrameCreated(frame_info->frame_);
    if (is_main_frame && !is_speculative) {
      SetMainFrame(browser_, frame_info->frame_);
    }

#if DCHECK_IS_ON()
    // Check that the frame info hasn't changed unexpectedly.
    DCHECK_EQ(frame_util::MakeFrameId(global_id),
              frame_info->frame_->GetIdentifier());
    DCHECK_EQ(frame_info->is_main_frame_, frame_info->frame_->IsMain());
#endif
  }

  browser_->request_context()->OnRenderFrameCreated(global_id, is_main_frame,
                                                    is_guest_view);

  // Populate the lookup maps.
  frame_id_map_.insert(std::make_pair(global_id, frame_info));

  // And finally set the ownership.
  frame_info_set_.insert(base::WrapUnique(frame_info));
}

void CefBrowserInfo::FrameHostStateChanged(
    content::RenderFrameHost* host,
    content::RenderFrameHost::LifecycleState old_state,
    content::RenderFrameHost::LifecycleState new_state) {
  CEF_REQUIRE_UIT();

  if ((old_state == content::RenderFrameHost::LifecycleState::kPrerendering ||
       old_state ==
           content::RenderFrameHost::LifecycleState::kInBackForwardCache) &&
      new_state == content::RenderFrameHost::LifecycleState::kActive) {
    if (auto frame = GetFrameForHost(host)) {
      // Update the associated RFH, which may have changed.
      frame->MaybeReAttach(this, host);

      if (frame->IsMain()) {
        // Update the main frame object.
        NotificationStateLock lock_scope(this);
        SetMainFrame(browser_, frame);
      }

      // Update draggable regions.
      frame->MaybeSendDidStopLoading();
    }
  }

  // Update BackForwardCache state.
  bool added_to_bfcache =
      new_state ==
      content::RenderFrameHost::LifecycleState::kInBackForwardCache;
  bool removed_from_bfcache =
      old_state ==
      content::RenderFrameHost::LifecycleState::kInBackForwardCache;
  if (!added_to_bfcache && !removed_from_bfcache)
    return;

  base::AutoLock lock_scope(lock_);

  auto it = frame_id_map_.find(host->GetGlobalId());
  DCHECK(it != frame_id_map_.end());
  DCHECK((!it->second->is_in_bfcache_ && added_to_bfcache) ||
         (it->second->is_in_bfcache_ && removed_from_bfcache));
  it->second->is_in_bfcache_ = added_to_bfcache;
}

void CefBrowserInfo::RemoveFrame(content::RenderFrameHost* host) {
  CEF_REQUIRE_UIT();

  NotificationStateLock lock_scope(this);

  const auto global_id = host->GetGlobalId();
  auto it = frame_id_map_.find(global_id);
  DCHECK(it != frame_id_map_.end());

  auto frame_info = it->second;

  browser_->request_context()->OnRenderFrameDeleted(
      global_id, frame_info->is_main_frame_, frame_info->is_guest_view_);

  // Remove from the lookup maps.
  frame_id_map_.erase(it);

  // And finally delete the frame info.
  {
    auto it2 = frame_info_set_.find(frame_info);

    // Explicitly Detach everything but the current main frame.
    const auto& frame_info = *it2;
    if (frame_info->frame_ && !frame_info->IsCurrentMainFrame()) {
      if (frame_info->frame_->Detach())
        MaybeNotifyFrameDetached(browser_, frame_info->frame_);
    }

    frame_info_set_.erase(it2);
  }
}

CefRefPtr<CefFrameHostImpl> CefBrowserInfo::GetMainFrame() {
  base::AutoLock lock_scope(lock_);
  // Early exit if called post-destruction.
  if (!browser_ || is_closing_)
    return nullptr;

  CHECK(main_frame_);
  return main_frame_;
}

CefRefPtr<CefFrameHostImpl> CefBrowserInfo::CreateTempSubFrame(
    const content::GlobalRenderFrameHostId& parent_global_id) {
  CefRefPtr<CefFrameHostImpl> parent = GetFrameForGlobalId(parent_global_id);
  if (!parent)
    parent = GetMainFrame();
  // Intentionally not notifying for temporary frames.
  return new CefFrameHostImpl(this, parent->GetIdentifier());
}

CefRefPtr<CefFrameHostImpl> CefBrowserInfo::GetFrameForHost(
    const content::RenderFrameHost* host,
    bool* is_guest_view,
    bool prefer_speculative) const {
  if (is_guest_view)
    *is_guest_view = false;

  if (!host)
    return nullptr;

  return GetFrameForGlobalId(
      const_cast<content::RenderFrameHost*>(host)->GetGlobalId(), is_guest_view,
      prefer_speculative);
}

CefRefPtr<CefFrameHostImpl> CefBrowserInfo::GetFrameForGlobalId(
    const content::GlobalRenderFrameHostId& global_id,
    bool* is_guest_view,
    bool prefer_speculative) const {
  if (is_guest_view)
    *is_guest_view = false;

  if (!frame_util::IsValidGlobalId(global_id))
    return nullptr;

  base::AutoLock lock_scope(lock_);

  const auto it = frame_id_map_.find(global_id);
  if (it != frame_id_map_.end()) {
    const auto info = it->second;

    if (info->is_guest_view_) {
      if (is_guest_view)
        *is_guest_view = true;
      return nullptr;
    }

    if (info->is_speculative_ && !prefer_speculative) {
      if (info->is_main_frame_ && main_frame_) {
        // Always prefer the non-speculative main frame.
        return main_frame_;
      }

      LOG(WARNING) << "Returning a speculative frame for "
                   << frame_util::GetFrameDebugString(global_id);
    }

    DCHECK(info->frame_);
    return info->frame_;
  }

  return nullptr;
}

CefBrowserInfo::FrameHostList CefBrowserInfo::GetAllFrames() const {
  base::AutoLock lock_scope(lock_);
  FrameHostList frames;
  for (const auto& info : frame_info_set_) {
    if (info->frame_ && !info->is_speculative_ && !info->is_in_bfcache_) {
      frames.insert(info->frame_);
    }
  }
  return frames;
}

CefBrowserInfo::NavigationLock::NavigationLock() : weak_ptr_factory_(this) {}

CefBrowserInfo::NavigationLock::~NavigationLock() {
  CEF_REQUIRE_UIT();
  if (pending_action_) {
    CEF_POST_TASK(CEF_UIT, std::move(pending_action_));
  }
}

scoped_refptr<CefBrowserInfo::NavigationLock>
CefBrowserInfo::CreateNavigationLock() {
  CEF_REQUIRE_UIT();
  scoped_refptr<NavigationLock> lock;
  if (!navigation_lock_) {
    lock = new NavigationLock();
    navigation_lock_ = lock->weak_ptr_factory_.GetWeakPtr();
  } else {
    lock = navigation_lock_.get();
  }
  return lock;
}

bool CefBrowserInfo::IsNavigationLocked(base::OnceClosure pending_action) {
  CEF_REQUIRE_UIT();
  if (navigation_lock_) {
    navigation_lock_->pending_action_ = std::move(pending_action);
    return true;
  }
  return false;
}

void CefBrowserInfo::MaybeExecuteFrameNotification(
    FrameNotifyOnceAction pending_action) {
  CefRefPtr<CefFrameHandler> frame_handler;

  {
    base::AutoLock lock_scope_(notification_lock_);
    if (!frame_handler_) {
      // No notifications will be executed.
      return;
    }

    if (notification_state_lock_) {
      // Queue the notification until the lock is released.
      notification_state_lock_->queue_.push(std::move(pending_action));
      return;
    }

    frame_handler = frame_handler_;
  }

  // Execute immediately if not locked.
  std::move(pending_action).Run(frame_handler);
}

void CefBrowserInfo::MaybeNotifyDraggableRegionsChanged(
    CefRefPtr<CefBrowserHostBase> browser,
    CefRefPtr<CefFrameHostImpl> frame,
    std::vector<CefDraggableRegion> draggable_regions) {
  CEF_REQUIRE_UIT();
  DCHECK(frame->IsMain());

  if (draggable_regions == draggable_regions_)
    return;

  draggable_regions_ = std::move(draggable_regions);

  if (auto client = browser->GetClient()) {
    if (auto handler = client->GetDragHandler()) {
      handler->OnDraggableRegionsChanged(browser.get(), frame,
                                         draggable_regions_);
    }
  }
}

// Passing in |browser| here because |browser_| may already be cleared.
void CefBrowserInfo::SetMainFrame(CefRefPtr<CefBrowserHostBase> browser,
                                  CefRefPtr<CefFrameHostImpl> frame) {
  lock_.AssertAcquired();
  DCHECK(browser);
  DCHECK(!frame || frame->IsMain());

  if (frame && main_frame_ &&
      frame->GetIdentifier() == main_frame_->GetIdentifier()) {
    // Nothing to do.
    return;
  }

  CefRefPtr<CefFrameHostImpl> old_frame;
  if (main_frame_) {
    old_frame = main_frame_;
    if (old_frame->Detach())
      MaybeNotifyFrameDetached(browser, old_frame);
  }

  main_frame_ = frame;

  MaybeNotifyMainFrameChanged(browser, old_frame, main_frame_);
}

void CefBrowserInfo::MaybeNotifyFrameCreated(
    CefRefPtr<CefFrameHostImpl> frame) {
  CEF_REQUIRE_UIT();

  // Never notify for temporary objects.
  DCHECK(!frame->is_temporary());

  MaybeExecuteFrameNotification(base::BindOnce(
      [](scoped_refptr<CefBrowserInfo> self, CefRefPtr<CefFrameHostImpl> frame,
         CefRefPtr<CefFrameHandler> handler) {
        if (auto browser = self->browser()) {
          handler->OnFrameCreated(browser, frame);
        }
      },
      scoped_refptr<CefBrowserInfo>(this), frame));
}

// Passing in |browser| here because |browser_| may already be cleared.
void CefBrowserInfo::MaybeNotifyFrameDetached(
    CefRefPtr<CefBrowserHostBase> browser,
    CefRefPtr<CefFrameHostImpl> frame) {
  CEF_REQUIRE_UIT();

  // Never notify for temporary objects.
  DCHECK(!frame->is_temporary());

  MaybeExecuteFrameNotification(base::BindOnce(
      [](CefRefPtr<CefBrowserHostBase> browser,
         CefRefPtr<CefFrameHostImpl> frame,
         CefRefPtr<CefFrameHandler> handler) {
        handler->OnFrameDetached(browser, frame);
      },
      browser, frame));
}

// Passing in |browser| here because |browser_| may already be cleared.
void CefBrowserInfo::MaybeNotifyMainFrameChanged(
    CefRefPtr<CefBrowserHostBase> browser,
    CefRefPtr<CefFrameHostImpl> old_frame,
    CefRefPtr<CefFrameHostImpl> new_frame) {
  CEF_REQUIRE_UIT();

  // Never notify for temporary objects.
  DCHECK(!old_frame || !old_frame->is_temporary());
  DCHECK(!new_frame || !new_frame->is_temporary());

  MaybeExecuteFrameNotification(base::BindOnce(
      [](CefRefPtr<CefBrowserHostBase> browser,
         CefRefPtr<CefFrameHostImpl> old_frame,
         CefRefPtr<CefFrameHostImpl> new_frame,
         CefRefPtr<CefFrameHandler> handler) {
        handler->OnMainFrameChanged(browser, old_frame, new_frame);
      },
      browser, old_frame, new_frame));
}

void CefBrowserInfo::RemoveAllFrames(
    CefRefPtr<CefBrowserHostBase> old_browser) {
  lock_.AssertAcquired();

  // Make sure any callbacks will see the correct state (e.g. like
  // CefBrowser::GetMainFrame returning nullptr and CefBrowser::IsValid
  // returning false).
  DCHECK(!browser_);
  DCHECK(old_browser);

  // Clear the lookup maps.
  frame_id_map_.clear();

  // Explicitly Detach everything but the current main frame.
  for (auto& info : frame_info_set_) {
    if (info->frame_ && !info->IsCurrentMainFrame()) {
      if (info->frame_->Detach())
        MaybeNotifyFrameDetached(old_browser, info->frame_);
    }
  }

  if (main_frame_)
    SetMainFrame(old_browser, nullptr);

  // And finally delete the frame info.
  frame_info_set_.clear();
}

CefBrowserInfo::NotificationStateLock::NotificationStateLock(
    CefBrowserInfo* browser_info)
    : browser_info_(browser_info) {
  CEF_REQUIRE_UIT();

  // Take the navigation state lock.
  {
    base::AutoLock lock_scope_(browser_info_->notification_lock_);
    CHECK(!browser_info_->notification_state_lock_);
    browser_info_->notification_state_lock_ = this;
    // We may need this on destruction, and the original might be cleared.
    frame_handler_ = browser_info_->frame_handler_;
  }

  // Take the browser info state lock.
  browser_info_lock_scope_.reset(new base::AutoLock(browser_info_->lock_));
}

CefBrowserInfo::NotificationStateLock::~NotificationStateLock() {
  CEF_REQUIRE_UIT();

  // Unlock in reverse order.
  browser_info_lock_scope_.reset();

  {
    base::AutoLock lock_scope_(browser_info_->notification_lock_);
    CHECK_EQ(this, browser_info_->notification_state_lock_);
    browser_info_->notification_state_lock_ = nullptr;
  }

  if (!queue_.empty()) {
    DCHECK(frame_handler_);

    // Don't navigate while inside callbacks.
    auto nav_lock = browser_info_->CreateNavigationLock();

    // Empty the queue of pending actions. Any of these actions might result in
    // the acquisition of a new NotificationStateLock.
    while (!queue_.empty()) {
      std::move(queue_.front()).Run(frame_handler_);
      queue_.pop();
    }
  }
}