// Copyright 2018 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/translate/android/translate_bridge.h"

#include <ranges>

#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "base/check.h"
#include "base/i18n/language_tag.h"
#include "base/i18n/tag_converters.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/language/language_model_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/translate/chrome_translate_client.h"
#include "chrome/browser/translate/translate_service.h"
#include "components/language/core/browser/language_model.h"
#include "components/language/core/browser/language_model_manager.h"
#include "components/language/core/browser/pref_names.h"
#include "components/language/core/common/language_experiments.h"
#include "components/language/core/common/language_util.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_locale_settings.h"
#include "components/translate/core/browser/translate_manager.h"
#include "components/translate/core/browser/translate_metrics_logger.h"
#include "components/translate/core/browser/translate_pref_names.h"
#include "components/translate/core/browser/translate_prefs.h"
#include "content/public/browser/web_contents.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "third_party/icu/source/common/unicode/uloc.h"
#include "ui/base/l10n/l10n_util.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/browser/language/android/jni_headers/TranslateBridge_jni.h"
#include "chrome/browser/language/android/jni_headers/TranslationObserver_jni.h"

using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaArrayOfStrings;

namespace {

PrefService* GetPrefService(const base::android::JavaRef<jobject>& j_profile) {
  return Profile::FromJavaObject(j_profile)->GetPrefs();
}

class TranslationObserver
    : public translate::ContentTranslateDriver::TranslationObserver {
 public:
  TranslationObserver(JNIEnv* env,
                      const base::android::JavaRef<jobject>& j_observer)
      : env_(env), j_observer_(j_observer) {}

  void OnIsPageTranslatedChanged(content::WebContents* source) override {
    ScopedJavaLocalRef<jobject> jsource_contents;
    if (source) {
      jsource_contents = source->GetJavaWebContents();
    }
    Java_TranslationObserver_onIsPageTranslatedChanged(env_, j_observer_,
                                                       jsource_contents);
  }

  void OnPageTranslated(std::string_view source_lang,
                        std::string_view translated_lang,
                        translate::TranslateErrors error_type) override {
    Java_TranslationObserver_onPageTranslated(
        env_, j_observer_, std::string(source_lang),
        std::string(translated_lang), static_cast<int>(error_type));
  }

 private:
  raw_ptr<JNIEnv> env_;
  ScopedJavaGlobalRef<jobject> j_observer_;
};

}  // namespace

static ChromeTranslateClient* GetTranslateClient(
    const base::android::JavaRef<jobject>& j_web_contents) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(j_web_contents);
  ChromeTranslateClient* client =
      ChromeTranslateClient::FromWebContents(web_contents);
  CHECK(client);
  return client;
}

static void JNI_TranslateBridge_ManualTranslateWhenReady(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(j_web_contents);
  ChromeTranslateClient* client =
      ChromeTranslateClient::FromWebContents(web_contents);
  CHECK(client);
  client->ManualTranslateWhenReady();
}

static bool JNI_TranslateBridge_CanManuallyTranslate(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents,
    bool menuLogging) {
  ChromeTranslateClient* client = GetTranslateClient(j_web_contents);
  translate::TranslateManager* manager = client->GetTranslateManager();
  CHECK(manager);
  return manager->CanManuallyTranslate(menuLogging);
}

static bool JNI_TranslateBridge_ShouldShowManualTranslateIph(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents) {
  ChromeTranslateClient* client = GetTranslateClient(j_web_contents);
  translate::TranslateManager* manager = client->GetTranslateManager();
  CHECK(manager);

  const std::string page_lang = manager->GetLanguageState()->source_language();
  std::unique_ptr<translate::TranslatePrefs> translate_prefs(
      client->GetTranslatePrefs());

  return base::StartsWith(page_lang, "en",
                          base::CompareCase::INSENSITIVE_ASCII) &&
         !translate_prefs->ShouldForceTriggerTranslateOnEnglishPages() &&
         !manager->GetLanguageState()->translate_enabled();
}

static void JNI_TranslateBridge_SetPredefinedTargetLanguage(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents,
    const std::string& translate_language,
    bool j_should_auto_translate) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(j_web_contents);

  ChromeTranslateClient* client =
      ChromeTranslateClient::FromWebContents(web_contents);
  CHECK(client);
  client->SetPredefinedTargetLanguage(
      base::i18n::GetLanguageTagFromString(translate_language)
          .value_or(base::i18n::GetKnownLanguageTag("und")),
      j_should_auto_translate);
}

// Returns the preferred target language to translate into for this user.
static std::string JNI_TranslateBridge_GetTargetLanguage(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile) {
  Profile* profile = Profile::FromJavaObject(j_profile);
  language::LanguageModel* language_model =
      LanguageModelManagerFactory::GetForBrowserContext(profile)
          ->GetPrimaryModel();
  CHECK(language_model);
  PrefService* pref_service = profile->GetPrefs();
  std::string target_language =
      TranslateService::GetTargetLanguage(pref_service, language_model);
  CHECK(!target_language.empty());
  return target_language;
}

// Set the default target language to translate into for this user.
static void JNI_TranslateBridge_SetDefaultTargetLanguage(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::string& target_language) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));
  translate_prefs->SetRecentTargetLanguage(target_language);
}

// Determines whether the given language is blocked for translation.
static bool JNI_TranslateBridge_IsBlockedLanguage(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::string& language_code) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));
  CHECK(translate_prefs);
  return translate_prefs->IsBlockedLanguage(language_code);
}

// Gets all languages that should always be translated as a Java List.
static ScopedJavaLocalRef<jobjectArray>
JNI_TranslateBridge_GetAlwaysTranslateLanguages(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));
  return ToJavaArrayOfStrings(env,
                              translate_prefs->GetAlwaysTranslateLanguages());
}

// Gets all languages for which translation should not be prompted.
static ScopedJavaLocalRef<jobjectArray>
JNI_TranslateBridge_GetNeverTranslateLanguages(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));
  return ToJavaArrayOfStrings(env,
                              translate_prefs->GetNeverTranslateLanguages());
}

// Sets the always translate state for a language.
// The always translate language list is actually a dict mapping
// source_language -> target_language.  We use the current target language when
// adding |language| to the dict.
static void JNI_TranslateBridge_SetLanguageAlwaysTranslateState(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::string& language_code,
    bool alwaysTranslate) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));

  if (alwaysTranslate) {
    Profile* profile = Profile::FromJavaObject(j_profile);
    language::LanguageModel* language_model =
        LanguageModelManagerFactory::GetForBrowserContext(profile)
            ->GetPrimaryModel();
    CHECK(language_model);
    PrefService* pref_service = profile->GetPrefs();
    std::string target_language =
        TranslateService::GetTargetLanguage(pref_service, language_model);
    CHECK(!target_language.empty());
    translate_prefs->AddLanguagePairToAlwaysTranslateList(language_code,
                                                          target_language);
  } else {
    translate_prefs->RemoveLanguagePairFromAlwaysTranslateList(language_code);
  }
}

// static
// Input |locales| is a comma separated locale representation that consists of
// language tags (BCP47 compliant format). Each language tag contains a language
// code and a country code or a language code only.
void TranslateBridge::PrependToAcceptLanguagesIfNecessary(
    const std::string& locales,
    std::string* accept_languages) {
  std::vector<std::string> locale_list =
      base::SplitString(locales + "," + *accept_languages, ",",
                        base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

  absl::flat_hash_set<std::string> seen_tags;
  std::vector<std::pair<std::string, std::string>> unique_locale_list;
  for (const std::string& locale_str : locale_list) {
    char locale_ID[ULOC_FULLNAME_CAPACITY] = {};
    char language_code_buffer[ULOC_LANG_CAPACITY] = {};
    char country_code_buffer[ULOC_COUNTRY_CAPACITY] = {};

    UErrorCode error = U_ZERO_ERROR;
    uloc_forLanguageTag(locale_str.c_str(), locale_ID, ULOC_FULLNAME_CAPACITY,
                        nullptr, &error);
    if (U_FAILURE(error)) {
      LOG(ERROR) << "Ignoring invalid locale representation " << locale_str;
      continue;
    }

    error = U_ZERO_ERROR;
    uloc_getLanguage(locale_ID, language_code_buffer, ULOC_LANG_CAPACITY,
                     &error);
    if (U_FAILURE(error)) {
      LOG(ERROR) << "Ignoring invalid locale representation " << locale_str;
      continue;
    }

    error = U_ZERO_ERROR;
    uloc_getCountry(locale_ID, country_code_buffer, ULOC_COUNTRY_CAPACITY,
                    &error);
    if (U_FAILURE(error)) {
      LOG(ERROR) << "Ignoring invalid locale representation " << locale_str;
      continue;
    }

    std::string language_code(language_code_buffer);
    std::string country_code(country_code_buffer);
    std::string language_tag(language_code + "-" + country_code);

    if (bool inserted = seen_tags.insert(language_tag).second; !inserted) {
      continue;
    }

    unique_locale_list.push_back(std::make_pair(language_code, country_code));
  }

  // If language is not in the accept languages list, also add language
  // code. A language code should only be inserted after the last
  // languageTag that contains that language.
  // This will work with the IDS_ACCEPT_LANGUAGE localized strings bundled
  // with Chrome but may fail on arbitrary lists of language tags due to
  // differences in case and whitespace.
  absl::flat_hash_set<std::string> seen_languages;
  std::vector<std::string> output_list;
  for (const auto& [language_code, country_code] :
       std::views::reverse(unique_locale_list)) {
    if (seen_languages.insert(language_code).second) {
      output_list.push_back(language_code);
    }
    if (!country_code.empty())
      output_list.push_back(base::StrCat({language_code, "-", country_code}));
  }

  std::ranges::reverse(output_list);
  *accept_languages = base::JoinString(output_list, ",");
}

static void JNI_TranslateBridge_ResetAcceptLanguages(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::string& default_locale) {
  std::string accept_languages(l10n_util::GetStringUTF8(IDS_ACCEPT_LANGUAGES));

  TranslateBridge::PrependToAcceptLanguagesIfNecessary(default_locale,
                                                       &accept_languages);
  GetPrefService(j_profile)->SetString(language::prefs::kSelectedLanguages,
                                       accept_languages);
}

static void JNI_TranslateBridge_GetChromeAcceptLanguages(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const JavaRef<jobject>& list) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));

  std::vector<translate::TranslateLanguageInfo> languages;
  std::string app_locale = g_browser_process->GetApplicationLocale();
  translate_prefs->GetLanguageInfoList(
      app_locale, translate_prefs->IsTranslateAllowedByPolicy(), &languages);

  for (const auto& info : languages) {
    Java_TranslateBridge_addNewLanguageItemToList(
        env, list, info.code, info.display_name, info.native_display_name,
        info.supports_translate);
  }
}

static ScopedJavaLocalRef<jobjectArray>
JNI_TranslateBridge_GetUserAcceptLanguages(JNIEnv* env,
                                           const JavaRef<jobject>& j_profile) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));

  std::vector<base::i18n::LanguageTag> languages =
      translate_prefs->GetLanguageList();
  std::vector<std::string> language_strings;
  language_strings.reserve(languages.size());
  for (const auto& tag : languages) {
    language_strings.push_back(std::string(tag.tag_string()));
  }
  return ToJavaArrayOfStrings(env, language_strings);
}

static void JNI_TranslateBridge_SetLanguageOrder(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::vector<std::string>& order) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));
  translate_prefs->SetLanguageOrder(order);
}

static void JNI_TranslateBridge_UpdateUserAcceptLanguages(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::string& language_code,
    bool is_add) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));

  std::optional<base::i18n::LanguageTag> parsed_tag =
      base::i18n::GetLanguageTagFromString(language_code);
  if (parsed_tag) {
    if (is_add) {
      translate_prefs->AddToLanguageList(*parsed_tag, false /*force_blocked=*/);
    } else {
      translate_prefs->RemoveFromLanguageList(*parsed_tag);
    }
  }
}

static void JNI_TranslateBridge_MoveAcceptLanguage(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::string& language_code,
    int32_t offset) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));

  std::vector<base::i18n::LanguageTag> languages =
      translate_prefs->GetLanguageList();
  std::vector<std::string> language_strings;
  language_strings.reserve(languages.size());
  for (const auto& tag : languages) {
    language_strings.push_back(std::string(tag.tag_string()));
  }

  translate::TranslatePrefs::RearrangeSpecifier where =
      translate::TranslatePrefs::kNone;

  if (offset > 0) {
    where = translate::TranslatePrefs::kDown;
  } else {
    offset = -offset;
    where = translate::TranslatePrefs::kUp;
  }

  translate_prefs->RearrangeLanguage(language_code, where, offset,
                                     language_strings);
}

static void JNI_TranslateBridge_SetLanguageBlockedState(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile,
    const std::string& language_code,
    bool blocked) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));

  if (blocked) {
    translate_prefs->BlockLanguage(language_code);
  } else {
    translate_prefs->UnblockLanguage(language_code);
  }
}

static bool JNI_TranslateBridge_GetAppLanguagePromptShown(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));
  return translate_prefs->GetAppLanguagePromptShown();
}

static void JNI_TranslateBridge_SetAppLanguagePromptShown(
    JNIEnv* env,
    const JavaRef<jobject>& j_profile) {
  std::unique_ptr<translate::TranslatePrefs> translate_prefs =
      ChromeTranslateClient::CreateTranslatePrefs(GetPrefService(j_profile));
  translate_prefs->SetAppLanguagePromptShown();
}

static std::string JNI_TranslateBridge_GetCurrentLanguage(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(j_web_contents);
  ChromeTranslateClient* client =
      ChromeTranslateClient::FromWebContents(web_contents);
  CHECK(client);
  return client->GetLanguageState().current_language();
}

static bool JNI_TranslateBridge_IsPageTranslated(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(j_web_contents);
  ChromeTranslateClient* client =
      ChromeTranslateClient::FromWebContents(web_contents);
  CHECK(client);
  return client->GetLanguageState().IsPageTranslated();
}

static int64_t JNI_TranslateBridge_AddTranslationObserver(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents,
    const base::android::JavaRef<jobject>& j_observer) {
  auto* observer = new TranslationObserver(env, j_observer);
  GetTranslateClient(j_web_contents)
      ->translate_driver()
      ->AddTranslationObserver(observer);
  return reinterpret_cast<int64_t>(observer);
}

static void JNI_TranslateBridge_RemoveTranslationObserver(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_web_contents,
    int64_t j_observer_native_ptr) {
  TranslationObserver* observer =
      reinterpret_cast<TranslationObserver*>(j_observer_native_ptr);
  GetTranslateClient(j_web_contents)
      ->translate_driver()
      ->RemoveTranslationObserver(observer);
  delete observer;
}

static void JNI_TranslateBridge_SetIgnoreMissingKeyForTesting(  // IN-TEST
    JNIEnv* env,
    bool ignore) {
  translate::TranslateManager::SetIgnoreMissingKeyForTesting(  // IN-TEST
      ignore);
}

DEFINE_JNI(TranslateBridge)
DEFINE_JNI(TranslationObserver)
