// 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 "chrome/browser/accessibility/accessibility_labels_service.h"

#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "build/build_config.h"
#include "chrome/browser/language/url_language_histogram_factory.h"
#include "chrome/browser/manta/manta_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/pref_names.h"
#include "components/language/core/browser/language_usage_metrics.h"
#include "components/language/core/browser/pref_names.h"
#include "components/language/core/browser/url_language_histogram.h"
#include "components/manta/manta_service.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "components/version_info/channel.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "content/public/browser/web_contents.h"
#include "google_apis/google_api_keys.h"
#include "services/image_annotation/image_annotation_service.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/platform/ax_platform.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
#else
#include "base/android/jni_android.h"
#include "chrome/browser/image_descriptions/jni_headers/ImageDescriptionsController_jni.h"
#endif

using LanguageInfo = language::UrlLanguageHistogram::LanguageInfo;

namespace {

AccessibilityLabelsService::ImageAnnotatorBinder&
GetImageAnnotatorBinderOverride() {
  static base::NoDestructor<AccessibilityLabelsService::ImageAnnotatorBinder>
      binder;
  return *binder;
}

class ImageAnnotatorClient : public image_annotation::Annotator::Client {
 public:
  explicit ImageAnnotatorClient(Profile* profile) : profile_(profile) {}

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

  ~ImageAnnotatorClient() override = default;

  std::vector<std::string> GetAcceptLanguages() override {
    std::vector<std::string> accept_languages;
    const PrefService* pref_service = profile_->GetPrefs();
    std::string accept_languages_pref =
        pref_service->GetString(language::prefs::kAcceptLanguages);
    for (const std::string& lang :
         base::SplitString(accept_languages_pref, ",", base::TRIM_WHITESPACE,
                           base::SPLIT_WANT_NONEMPTY)) {
      accept_languages.push_back(lang);
    }
    return accept_languages;
  }

  std::vector<std::string> GetTopLanguages() override {
    // The UrlLanguageHistogram includes the frequency of all languages
    // of pages the user has visited, and some of these might be rare or
    // even mistakes. Set a minimum threshold so that we're only returning
    // languages that account for a nontrivial amount of browsing time.
    // The purpose of this list is to handle the case where users might
    // not be setting their accept languages correctly, we want a way to
    // detect the primary languages a user actually reads.
    const float kMinTopLanguageFrequency = 0.1;

    std::vector<std::string> top_languages;
    language::UrlLanguageHistogram* url_language_histogram =
        UrlLanguageHistogramFactory::GetForBrowserContext(profile_);
    std::vector<LanguageInfo> language_infos =
        url_language_histogram->GetTopLanguages();
    for (const LanguageInfo& info : language_infos) {
      if (info.frequency >= kMinTopLanguageFrequency) {
        top_languages.push_back(info.language_code);
      }
    }
    return top_languages;
  }

  void RecordLanguageMetrics(const std::string& page_language,
                             const std::string& requested_language) override {
    base::UmaHistogramSparse(
        "Accessibility.ImageLabels.PageLanguage",
        language::LanguageUsageMetrics::ToLanguageCodeHash(page_language));
    base::UmaHistogramSparse(
        "Accessibility.ImageLabels.RequestLanguage",
        language::LanguageUsageMetrics::ToLanguageCodeHash(requested_language));
  }

 private:
  const raw_ptr<Profile> profile_;
};

}  // namespace

AccessibilityLabelsService::AccessibilityLabelsService(Profile* profile)
    : profile_(profile) {
#if BUILDFLAG(IS_ANDROID)
  // On Android we must add/remove a NetworkChangeObserver during construction/
  // destruction to provide the "Only on Wi-Fi" functionality.
  net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
#endif
}

AccessibilityLabelsService::~AccessibilityLabelsService() {
#if BUILDFLAG(IS_ANDROID)
  net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
#endif
}

// static
void AccessibilityLabelsService::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterBooleanPref(
      prefs::kAccessibilityImageLabelsEnabled, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      prefs::kAccessibilityImageLabelsOptInAccepted, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
#if BUILDFLAG(IS_ANDROID)
  registry->RegisterBooleanPref(
      prefs::kAccessibilityImageLabelsEnabledAndroid, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      prefs::kAccessibilityImageLabelsOnlyOnWifi, true,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
#endif
}

// static
void AccessibilityLabelsService::InitOffTheRecordPrefs(
    Profile* off_the_record_profile) {
  DCHECK(off_the_record_profile->IsOffTheRecord());
  off_the_record_profile->GetPrefs()->SetBoolean(
      prefs::kAccessibilityImageLabelsEnabled, false);
  off_the_record_profile->GetPrefs()->SetBoolean(
      prefs::kAccessibilityImageLabelsOptInAccepted, false);
}

void AccessibilityLabelsService::Init() {
  pref_change_registrar_.Init(profile_->GetPrefs());
  pref_change_registrar_.Add(
#if !BUILDFLAG(IS_ANDROID)
      prefs::kAccessibilityImageLabelsEnabled,
#else
      prefs::kAccessibilityImageLabelsEnabledAndroid,
#endif
      base::BindRepeating(
          &AccessibilityLabelsService::OnImageLabelsEnabledChanged,
          weak_factory_.GetWeakPtr()));

  // This ensures prefs refresh the label images AXMode on startup.
  OnImageLabelsEnabledChanged();
}

bool AccessibilityLabelsService::IsEnabled() {
#if !BUILDFLAG(IS_ANDROID)
  return profile_->GetPrefs()->GetBoolean(
      prefs::kAccessibilityImageLabelsEnabled);
#else
  return GetAndroidEnabledStatus();
#endif
}

void AccessibilityLabelsService::EnableLabelsServiceOnce(
    content::WebContents* web_contents) {
  if (!ui::AXPlatform::GetInstance().IsScreenReaderActive()) {
    return;
  }

  // TODO(grt): Use ScopedAccessibilityMode here targeting the WC and remove
  // the action.
  // For Android, we call through the JNI (see below) and send the web contents
  // directly, since Android does not support BrowserList::GetInstance.
#if !BUILDFLAG(IS_ANDROID)
  // Fire an AXAction on the active tab to enable this feature once only.
  // We only need to fire this event for the active page.
  ui::AXActionData action_data;
  action_data.action = ax::mojom::Action::kAnnotatePageImages;
  web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost(
      [&action_data](content::RenderFrameHost* render_frame_host) {
        if (render_frame_host->IsRenderFrameLive()) {
          render_frame_host->AccessibilityPerformAction(action_data);
        }
      });
#endif
}

void AccessibilityLabelsService::BindImageAnnotator(
    mojo::PendingReceiver<image_annotation::mojom::Annotator> receiver) {
  if (!remote_service_) {
    auto service_receiver = remote_service_.BindNewPipeAndPassReceiver();
    auto& binder = GetImageAnnotatorBinderOverride();
    if (binder) {
      binder.Run(std::move(service_receiver));
    } else {
      auto* manta_service = manta::MantaServiceFactory::GetForProfile(profile_);
      CHECK(manta_service);
      service_ = std::make_unique<image_annotation::ImageAnnotationService>(
          std::move(service_receiver),
          google_apis::GetAPIKey(chrome::GetChannel()),
          profile_->GetURLLoaderFactory(),
          manta_service->CreateAnchovyProvider(),
          std::make_unique<ImageAnnotatorClient>(profile_));
    }
  }

  remote_service_->BindAnnotator(std::move(receiver));
}

void AccessibilityLabelsService::OverrideImageAnnotatorBinderForTesting(
    ImageAnnotatorBinder binder) {
  GetImageAnnotatorBinderOverride() = std::move(binder);
}

void AccessibilityLabelsService::OnImageLabelsEnabledChanged() {
  if (!IsEnabled()) {
    scoped_accessibility_mode_.reset();
  } else if (!scoped_accessibility_mode_) {
    scoped_accessibility_mode_ =
        content::BrowserAccessibilityState::GetInstance()
            ->CreateScopedModeForBrowserContext(profile_,
                                                ui::AXMode::kLabelImages);
  }

  UpdateAccessibilityLabelsHistograms();
}

void AccessibilityLabelsService::UpdateAccessibilityLabelsHistograms() {
  base::UmaHistogramBoolean("Accessibility.ImageLabels2",
                            profile_->GetPrefs()->GetBoolean(
                                prefs::kAccessibilityImageLabelsEnabled));

#if BUILDFLAG(IS_ANDROID)
  // For Android we will track additional histograms.
  base::UmaHistogramBoolean(
      "Accessibility.ImageLabels.Android",
      profile_->GetPrefs()->GetBoolean(
          prefs::kAccessibilityImageLabelsEnabledAndroid));

  base::UmaHistogramBoolean("Accessibility.ImageLabels.Android.OnlyOnWifi",
                            profile_->GetPrefs()->GetBoolean(
                                prefs::kAccessibilityImageLabelsOnlyOnWifi));
#endif
}

#if BUILDFLAG(IS_ANDROID)
void AccessibilityLabelsService::OnNetworkChanged(
    net::NetworkChangeNotifier::ConnectionType type) {
  // When the network status changes, we want to (potentially) update the
  // AXMode of all web contents for the current profile.
  OnImageLabelsEnabledChanged();
}

bool AccessibilityLabelsService::GetAndroidEnabledStatus() {
  // On Android, user has an option to toggle "only on wifi", so also check
  // the current connection type if necessary.
  bool enabled = profile_->GetPrefs()->GetBoolean(
      prefs::kAccessibilityImageLabelsEnabledAndroid);

  bool only_on_wifi = profile_->GetPrefs()->GetBoolean(
      prefs::kAccessibilityImageLabelsOnlyOnWifi);

  if (enabled && only_on_wifi) {
    net::NetworkChangeNotifier::ConnectionType current_connection_type =
        net::NetworkChangeNotifier::GetConnectionType();
    bool is_wifi =
        (current_connection_type ==
         net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI);
    bool is_ethernet =
        (current_connection_type ==
         net::NetworkChangeNotifier::ConnectionType::CONNECTION_ETHERNET);
    enabled = (is_wifi || is_ethernet);
  }

  return enabled;
}

static void JNI_ImageDescriptionsController_GetImageDescriptionsOnce(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(j_web_contents);

  if (!web_contents) {
    return;
  }

  // We only need to fire this event for the active page.
  ui::AXActionData action_data;
  action_data.action = ax::mojom::Action::kAnnotatePageImages;
  web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost(
      [&action_data](content::RenderFrameHost* render_frame_host) {
        if (render_frame_host->IsRenderFrameLive()) {
          render_frame_host->AccessibilityPerformAction(action_data);
        }
      });
}
#endif

#if BUILDFLAG(IS_ANDROID)
DEFINE_JNI(ImageDescriptionsController)
#endif
