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

#include "chrome/browser/permissions/one_time_permissions_tracker.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/permissions/one_time_permissions_tracker_observer.h"
#include "chrome/browser/profiles/profile.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/permissions/content_setting_permission_context_base.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_util.h"
#include "content/public/browser/visibility.h"
#include "url/gurl.h"

OneTimePermissionsTracker::OneTimePermissionsTracker() = default;
OneTimePermissionsTracker::~OneTimePermissionsTracker() = default;

OneTimePermissionsTracker::OriginTrackEntry::OriginTrackEntry() = default;

OneTimePermissionsTracker::OriginTrackEntry::~OriginTrackEntry() = default;

void OneTimePermissionsTracker::Shutdown() {
  for (auto& observer : observer_list_) {
    observer.OnShutdown();
  }
  observer_list_.Clear();
}

void OneTimePermissionsTracker::AddObserver(
    OneTimePermissionsTrackerObserver* observer) {
  observer_list_.AddObserver(observer);
}

void OneTimePermissionsTracker::RemoveObserver(
    OneTimePermissionsTrackerObserver* observer) {
  observer_list_.RemoveObserver(observer);
}

void OneTimePermissionsTracker::WebContentsBackgrounded(
    const url::Origin& origin) {
  if (!ShouldIgnoreOrigin(origin)) {
    // For some reason using `origin_tracker_[origin].background_tab_counter++;`
    // on some builds leaves the value of
    // `origin_tracker_[origin].background_tab_counter` at 0 in case of
    // insertion. My best efforts to understand it have failed. Hence, `+=` is
    // necessary here for the feature to work at all there.
    origin_tracker_[origin].background_tab_counter += 1;

    if (AreAllTabsToOriginBackgroundedOrDiscarded(origin)) {
      // When all undiscarded tabs which point to the origin are in the
      // background, the timers should be reset.
      origin_tracker_[origin].background_expiration_timer->Start(
          FROM_HERE, permissions::kOneTimePermissionTimeout,
          base::BindOnce(
              &OneTimePermissionsTracker::NotifyBackgroundTimerExpired,
              weak_factory_.GetWeakPtr(), origin,
              OneTimePermissionsTrackerObserver::BackgroundExpiryType::
                  kTimeout));

      origin_tracker_[origin].background_expiration_long_timer->Start(
          FROM_HERE, permissions::kOneTimePermissionMaximumLifetime,
          base::BindOnce(
              &OneTimePermissionsTracker::NotifyBackgroundTimerExpired,
              weak_factory_.GetWeakPtr(), origin,
              OneTimePermissionsTrackerObserver::BackgroundExpiryType::
                  kLongTimeout));

      HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_CAMERA);
      HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_MIC);
    } else {
      origin_tracker_[origin].background_expiration_timer->Stop();
      origin_tracker_[origin].background_expiration_long_timer->Stop();
    }
  }
}

void OneTimePermissionsTracker::WebContentsUnbackgrounded(
    const url::Origin& origin) {
  if (!ShouldIgnoreOrigin(origin)) {
    origin_tracker_[origin].background_tab_counter--;
    // Since the tab has been unbackgrounded, the timers should be reset
    origin_tracker_[origin].background_expiration_timer->Stop();
    origin_tracker_[origin].background_expiration_long_timer->Stop();
  }
}

void OneTimePermissionsTracker::WebContentsLoadedOrigin(
    const url::Origin& origin) {
  if (!ShouldIgnoreOrigin(origin)) {
    origin_tracker_[origin].undiscarded_tab_counter++;
    origin_tracker_[origin].background_expiration_timer->Stop();
    origin_tracker_[origin].background_expiration_long_timer->Stop();
  }
}

void OneTimePermissionsTracker::WebContentsUnloadedOrigin(
    const url::Origin& origin) {
  if (!ShouldIgnoreOrigin(origin)) {
    origin_tracker_[origin].undiscarded_tab_counter--;
    DCHECK(!(origin_tracker_[origin].undiscarded_tab_counter < 0));
    if (origin_tracker_[origin].undiscarded_tab_counter == 0) {
      NotifyLastPageFromOriginClosed(origin);
    }
  }
}

void OneTimePermissionsTracker::StartContentSpecificExpirationTimer(
    const url::Origin& origin,
    ContentSettingsType content_setting,
    NotifyFunction notify_callback) {
  origin_tracker_[origin].used_content_settings_set.insert(content_setting);
  origin_tracker_[origin]
      .content_setting_specific_expiration_timer_map[content_setting]
      ->Start(
          FROM_HERE, permissions::kOneTimePermissionTimeout,
          base::BindOnce(notify_callback, weak_factory_.GetWeakPtr(), origin));
}

void OneTimePermissionsTracker::HandleUserMediaState(
    const url::Origin& origin,
    ContentSettingsType content_setting) {
  NotifyFunction notify_callback;
  switch (content_setting) {
    case ContentSettingsType::MEDIASTREAM_CAMERA:
      notify_callback = &OneTimePermissionsTracker::NotifyCapturingVideoExpired;
      break;
    case ContentSettingsType::MEDIASTREAM_MIC:
      notify_callback = &OneTimePermissionsTracker::NotifyCapturingAudioExpired;
      break;
    default:
      NOTREACHED();
  }

  if (origin_tracker_[origin].used_content_settings_set.find(content_setting) !=
      origin_tracker_[origin].used_content_settings_set.end()) {
    if (origin_tracker_[origin]
            .content_setting_specific_expiration_timer_map.find(
                content_setting) ==
        origin_tracker_[origin]
            .content_setting_specific_expiration_timer_map.end()) {
      origin_tracker_[origin]
          .content_setting_specific_expiration_timer_map[content_setting] =
          std::make_unique<base::OneShotTimer>();
    }

    if (origin_tracker_[origin]
                .content_setting_specific_counter_map[content_setting] == 0 &&
        AreAllTabsToOriginBackgroundedOrDiscarded(origin)) {
      StartContentSpecificExpirationTimer(origin, content_setting,
                                          notify_callback);
    } else {
      origin_tracker_[origin]
          .content_setting_specific_expiration_timer_map[content_setting]
          ->Stop();
    }
  }
}

void OneTimePermissionsTracker::CapturingVideoChanged(const url::Origin& origin,
                                                      bool is_capturing_video) {
  if (!ShouldIgnoreOrigin(origin)) {
    if (is_capturing_video &&
        origin_tracker_[origin].used_content_settings_set.find(
            ContentSettingsType::MEDIASTREAM_CAMERA) ==
            origin_tracker_[origin].used_content_settings_set.end()) {
      origin_tracker_[origin].used_content_settings_set.insert(
          ContentSettingsType::MEDIASTREAM_CAMERA);
    }

    origin_tracker_[origin].content_setting_specific_counter_map
        [ContentSettingsType::MEDIASTREAM_CAMERA] +=
        is_capturing_video ? 1 : -1;
    HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_CAMERA);
  }
}

void OneTimePermissionsTracker::CapturingAudioChanged(const url::Origin& origin,
                                                      bool is_capturing_audio) {
  if (!ShouldIgnoreOrigin(origin)) {
    if (is_capturing_audio &&
        origin_tracker_[origin].used_content_settings_set.find(
            ContentSettingsType::MEDIASTREAM_MIC) ==
            origin_tracker_[origin].used_content_settings_set.end()) {
      origin_tracker_[origin].used_content_settings_set.insert(
          ContentSettingsType::MEDIASTREAM_MIC);
    }

    origin_tracker_[origin].content_setting_specific_counter_map
        [ContentSettingsType::MEDIASTREAM_MIC] += is_capturing_audio ? 1 : -1;
    HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_MIC);
  }
}

void OneTimePermissionsTracker::CleanupStateForExpiredContentSetting(
    ContentSettingsType type,
    ContentSettingsPattern primary_pattern,
    ContentSettingsPattern secondary_pattern) {
  std::vector<url::Origin> affected_origins;
  for (const auto& entry : origin_tracker_) {
    const GURL top_level_origin_as_gurl = entry.first.GetURL();
    if (primary_pattern.Matches(top_level_origin_as_gurl) &&
        secondary_pattern.Matches(top_level_origin_as_gurl)) {
      affected_origins.push_back(entry.first);
    }
  }

  for (const auto& origin : affected_origins) {
    if (type == permissions::PermissionUtil::GetGeolocationType()) {
      origin_tracker_[origin].background_expiration_timer->Stop();
    } else {
      origin_tracker_[origin]
          .content_setting_specific_expiration_timer_map.erase(type);
    }

    origin_tracker_[origin].content_setting_specific_counter_map.erase(type);
    origin_tracker_[origin].used_content_settings_set.erase(type);
  }
}

void OneTimePermissionsTracker::FireRunningTimersForTesting() {
  // The loops in this method require manual 'forward-looking' iteration because
  // the methods executing upon timer expiration might erase elements from the
  // map that is being iterated over.
  for (auto i = origin_tracker_.begin(), e = origin_tracker_.end(); i != e;) {
    auto origin_entry = i++;
    if (origin_entry->second.background_expiration_timer->IsRunning()) {
      origin_entry->second.background_expiration_timer->FireNow();
    }
  }

  for (auto i_outer = origin_tracker_.begin(), e_outer = origin_tracker_.end();
       i_outer != e_outer;) {
    auto origin_entry = i_outer++;
    for (auto
             i_inner =
                 origin_entry->second
                     .content_setting_specific_expiration_timer_map.begin(),
             e_inner = origin_entry->second
                           .content_setting_specific_expiration_timer_map.end();
         i_inner != e_inner;) {
      auto timer_entry = i_inner++;
      if (timer_entry->second->IsRunning()) {
        timer_entry->second->FireNow();
      }
    }
  }
}

void OneTimePermissionsTracker::NotifyLastPageFromOriginClosed(
    const url::Origin& origin) {
  for (auto& observer : observer_list_) {
    observer.OnLastPageFromOriginClosed(origin);
  }
}

bool OneTimePermissionsTracker::ShouldIgnoreOrigin(const url::Origin& origin) {
  // There are cases where chrome://newtab/ and chrome://new-tab-page/ are
  // used synonymously causing inconsistencies in the map. So we just ignore
  // them.
  return origin.opaque() ||
         origin == url::Origin::Create(GURL("chrome://newtab/")) ||
         origin == url::Origin::Create(GURL("chrome://new-tab-page/"));
}

void OneTimePermissionsTracker::NotifyBackgroundTimerExpired(
    const url::Origin& origin,
    const OneTimePermissionsTrackerObserver::BackgroundExpiryType&
        expiry_type) {
  for (auto& observer : observer_list_) {
    observer.OnAllTabsInBackgroundTimerExpired(origin, expiry_type);
  }
  switch (expiry_type) {
    case OneTimePermissionsTrackerObserver::BackgroundExpiryType::kTimeout:
      origin_tracker_[origin].background_expiration_timer->Stop();
      return;
    case OneTimePermissionsTrackerObserver::BackgroundExpiryType::kLongTimeout:
      origin_tracker_[origin].background_expiration_long_timer->Stop();
      return;
  }
}

void OneTimePermissionsTracker::NotifyCapturingVideoExpired(
    const url::Origin& origin) {
  for (auto& observer : observer_list_) {
    observer.OnCapturingVideoExpired(origin);
  }
}

void OneTimePermissionsTracker::NotifyCapturingAudioExpired(
    const url::Origin& origin) {
  for (auto& observer : observer_list_) {
    observer.OnCapturingAudioExpired(origin);
  }
}

bool OneTimePermissionsTracker::AreAllTabsToOriginBackgroundedOrDiscarded(
    const url::Origin& origin) {
  return origin_tracker_[origin].background_tab_counter ==
         origin_tracker_[origin].undiscarded_tab_counter;
}
