// 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 "components/visitedlink/browser/visitedlink_event_listener.h"

#include "base/bind.h"
#include "components/visitedlink/browser/visitedlink_delegate.h"
#include "components/visitedlink/common/visitedlink.mojom.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host.h"
#include "services/service_manager/public/cpp/interface_provider.h"

using base::Time;
using base::TimeDelta;
using content::RenderWidgetHost;

namespace {

// The amount of time we wait to accumulate visited link additions.
constexpr int kCommitIntervalMs = 100;

// Size of the buffer after which individual link updates deemed not warranted
// and the overall update should be used instead.
const unsigned kVisitedLinkBufferThreshold = 50;

}  // namespace

namespace visitedlink {

// This class manages buffering and sending visited link hashes (fingerprints)
// to renderer based on widget visibility.
// As opposed to the VisitedLinkEventListener, which coalesces to
// reduce the rate of messages being sent to render processes, this class
// ensures that the updates occur only when explicitly requested. This is
// used for RenderProcessHostImpl to only send Add/Reset link events to the
// renderers when their tabs are visible and the corresponding RenderViews are
// created.
class VisitedLinkUpdater {
 public:
  explicit VisitedLinkUpdater(int render_process_id)
      : reset_needed_(false),
        invalidate_hashes_(false),
        render_process_id_(render_process_id) {
    BindInterface(content::RenderProcessHost::FromID(render_process_id),
                  &sink_);
  }

  // Informs the renderer about a new visited link table.
  void SendVisitedLinkTable(base::ReadOnlySharedMemoryRegion* region) {
    if (region->IsValid())
      sink_->UpdateVisitedLinks(region->Duplicate());
  }

  // Buffers |links| to update, but doesn't actually relay them.
  void AddLinks(const VisitedLinkCommon::Fingerprints& links) {
    if (reset_needed_)
      return;

    if (pending_.size() + links.size() > kVisitedLinkBufferThreshold) {
      // Once the threshold is reached, there's no need to store pending visited
      // link updates -- we opt for resetting the state for all links.
      AddReset(false);
      return;
    }

    pending_.insert(pending_.end(), links.begin(), links.end());
  }

  // Tells the updater that sending individual link updates is no longer
  // necessary and the visited state for all links should be reset. If
  // |invalidateHashes| is true all cached visited links hashes should be
  // dropped.
  void AddReset(bool invalidate_hashes) {
    reset_needed_ = true;
    // Do not set to false. If tab is invisible the reset message will not be
    // sent until tab became visible.
    if (invalidate_hashes)
      invalidate_hashes_ = true;
    pending_.clear();
  }

  // Sends visited link update messages: a list of links whose visited state
  // changed or reset of visited state for all links.
  void Update() {
    content::RenderProcessHost* process =
        content::RenderProcessHost::FromID(render_process_id_);
    if (!process)
      return;  // Happens in tests

    if (!process->VisibleClientCount())
      return;

    if (reset_needed_) {
      sink_->ResetVisitedLinks(invalidate_hashes_);
      reset_needed_ = false;
      invalidate_hashes_ = false;
      return;
    }

    if (pending_.empty())
      return;

    sink_->AddVisitedLinks(pending_);

    pending_.clear();
  }

 private:
  bool reset_needed_;
  bool invalidate_hashes_;
  int render_process_id_;
  mojom::VisitedLinkNotificationSinkPtr sink_;
  VisitedLinkCommon::Fingerprints pending_;
};

VisitedLinkEventListener::VisitedLinkEventListener(
    content::BrowserContext* browser_context)
    : coalesce_timer_(&default_coalesce_timer_),
      browser_context_(browser_context) {
  registrar_.Add(this, content::NOTIFICATION_RENDERER_PROCESS_CREATED,
                 content::NotificationService::AllBrowserContextsAndSources());
  registrar_.Add(this, content::NOTIFICATION_RENDERER_PROCESS_TERMINATED,
                 content::NotificationService::AllBrowserContextsAndSources());
  registrar_.Add(this, content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED,
                 content::NotificationService::AllBrowserContextsAndSources());
}

VisitedLinkEventListener::~VisitedLinkEventListener() {
  if (!pending_visited_links_.empty())
    pending_visited_links_.clear();
}

void VisitedLinkEventListener::NewTable(
    base::ReadOnlySharedMemoryRegion* table_region) {
  DCHECK(table_region && table_region->IsValid());
  table_region_ = table_region->Duplicate();
  if (!table_region_.IsValid())
    return;

  // Send to all RenderProcessHosts.
  for (Updaters::iterator i = updaters_.begin(); i != updaters_.end(); ++i) {
    // Make sure to not send to incognito renderers.
    content::RenderProcessHost* process =
        content::RenderProcessHost::FromID(i->first);
    if (!process)
      continue;

    i->second->SendVisitedLinkTable(&table_region_);
  }
}

void VisitedLinkEventListener::Add(VisitedLinkMaster::Fingerprint fingerprint) {
  pending_visited_links_.push_back(fingerprint);

  if (!coalesce_timer_->IsRunning()) {
    coalesce_timer_->Start(
        FROM_HERE, TimeDelta::FromMilliseconds(kCommitIntervalMs),
        base::Bind(&VisitedLinkEventListener::CommitVisitedLinks,
                   base::Unretained(this)));
  }
}

void VisitedLinkEventListener::Reset(bool invalidate_hashes) {
  pending_visited_links_.clear();
  coalesce_timer_->Stop();

  for (Updaters::iterator i = updaters_.begin(); i != updaters_.end(); ++i) {
    i->second->AddReset(invalidate_hashes);
    i->second->Update();
  }
}

void VisitedLinkEventListener::SetCoalesceTimerForTest(
    base::OneShotTimer* coalesce_timer_override) {
  coalesce_timer_ = coalesce_timer_override;
}

void VisitedLinkEventListener::CommitVisitedLinks() {
  // Send to all RenderProcessHosts.
  for (Updaters::iterator i = updaters_.begin(); i != updaters_.end(); ++i) {
    i->second->AddLinks(pending_visited_links_);
    i->second->Update();
  }

  pending_visited_links_.clear();
}

void VisitedLinkEventListener::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  switch (type) {
    case content::NOTIFICATION_RENDERER_PROCESS_CREATED: {
      content::RenderProcessHost* process =
          content::Source<content::RenderProcessHost>(source).ptr();
      if (browser_context_ != process->GetBrowserContext())
        return;

      // Happens on browser start up.
      if (!table_region_.IsValid())
        return;

      updaters_[process->GetID()].reset(
          new VisitedLinkUpdater(process->GetID()));
      updaters_[process->GetID()]->SendVisitedLinkTable(&table_region_);
      break;
    }
    case content::NOTIFICATION_RENDERER_PROCESS_TERMINATED: {
      content::RenderProcessHost* process =
          content::Source<content::RenderProcessHost>(source).ptr();
      if (updaters_.count(process->GetID())) {
        updaters_.erase(process->GetID());
      }
      break;
    }
    case content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED: {
      RenderWidgetHost* widget =
          content::Source<RenderWidgetHost>(source).ptr();
      int child_id = widget->GetProcess()->GetID();
      if (updaters_.count(child_id))
        updaters_[child_id]->Update();
      break;
    }
    default:
      NOTREACHED();
      break;
  }
}

}  // namespace visitedlink
