// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/platform/mac/spellcheck_mac.h"

#include "base/platform/mac/base_utilities_mac.h"
#include "spellcheck/third_party/language_cld3.h"

#import <AppKit/NSSpellChecker.h>
#import <QuartzCore/QuartzCore.h>

#include <QtCore/QLocale>
#include <set>

using Platform::Q2NSString;
using Platform::NS2QString;

namespace {

// +[NSSpellChecker sharedSpellChecker] can throw exceptions depending
// on the state of the pasteboard, or possibly as a result of
// third-party code (when setting up services entries).  The following
// receives nil if an exception is thrown, in which case
// spell-checking will not work, but it also will not crash the
// browser.
NSSpellChecker *SharedSpellChecker() {
	@try {
		return [NSSpellChecker sharedSpellChecker];
	} @catch (id exception) {
		return nil;
	}
}

inline auto SystemLanguages() {
	static auto languages = std::vector<QString>();
	if (!languages.size()) {
		const auto uiLanguages = QLocale::system().uiLanguages();
		languages = (
			uiLanguages
		) | ranges::views::transform([&](const auto &lang) {
			return lang.left(std::max(lang.indexOf('_'), lang.indexOf('-')));
		}) | ranges::views::unique | ranges::to_vector;
	}
	return languages;
}

[[nodiscard]] auto RegionalVariantMap() {
	static auto map = std::map<QString, QString>();
	static auto initialized = false;
	if (!initialized) {
		initialized = true;
		const auto uiLanguages = QLocale::system().uiLanguages();
		for (const auto &ui : uiLanguages) {
			if (ui == u"en-AU"_q || ui == u"en_AU"_q) {
				map[u"en"_q] = ui;
			} else if (ui == u"en-GB"_q || ui == u"en_GB"_q) {
				if (!map.contains(u"en"_q)) {
					map[u"en"_q] = ui;
				}
			} else if (ui == u"en-CA"_q || ui == u"en_CA"_q) {
				if (!map.contains(u"en"_q)) {
					map[u"en"_q] = ui;
				}
			} else if (ui == u"pt-BR"_q || ui == u"pt_BR"_q) {
				map[u"pt"_q] = ui;
			}
		}
	}
	return map;
}

[[nodiscard]] auto PreferredRegionalVariant(const QString &lang) {
	const auto &map = RegionalVariantMap();
	if (const auto it = map.find(lang); it != map.end()) {
		return it->second;
	}
	return lang;
}

} // namespace

namespace Platform::Spellchecker {

void Init() {
}

std::vector<QString> ActiveLanguages() {
	return SystemLanguages();
}

bool CheckSpelling(const QString &word) {
	if (@available(macOS 10.14, *)) {
		const auto lang = PreferredRegionalVariant(
			Language::Recognize(word).twoLetterCode());
		return [SharedSpellChecker()
			checkSpellingOfString:Q2NSString(word)
			startingAt:0
			language:Q2NSString(lang)
			wrap:false
			inSpellDocumentWithTag:0
			wordCount:nil].location == NSNotFound;
	}
	const auto wordLength = word.length();
	NSArray<NSTextCheckingResult*> *spellRanges =
		[SharedSpellChecker()
			checkString:Q2NSString(word)
			range:NSMakeRange(0, wordLength)
			types:NSTextCheckingTypeSpelling
			options:nil
			inSpellDocumentWithTag:0
			orthography:nil
			wordCount:nil];
	// If the length of the misspelled word == 0,
	// then there is no misspelled word.
	return (spellRanges.count == 0);
}


// There's no need to check the language on the Mac.
void CheckSpellingText(
	const QString &text,
	MisspelledWords *misspelledWords) {
// Probably never gonna be defined.
#ifdef SPELLCHECKER_MAC_AUTO_CHECK_TEXT

	NSArray<NSTextCheckingResult*> *spellRanges =
		[SharedSpellChecker()
			checkString:Q2NSString(text)
			range:NSMakeRange(0, text.length())
			types:NSTextCheckingTypeSpelling
			options:nil
			inSpellDocumentWithTag:0
			orthography:nil
			wordCount:nil];

	misspelledWords->reserve(spellRanges.count);
	for (NSTextCheckingResult *result in spellRanges) {
		if (result.resultType != NSTextCheckingTypeSpelling) {
			continue;
		}
		misspelledWords->push_back({
			result.range.location,
			result.range.length});
	}

#else
// Known Issue: Despite the explicitly defined parameter,
// the correctness of a single word depends on the rest of the text.
// For example, "testt testtttyy" - this string will be marked as correct.
// But at the same time "testtttyy" will be marked as misspelled word.

// So we have to manually split the text into words and check them separately.
	*misspelledWords = ::Spellchecker::RangesFromText(
		text,
		::Spellchecker::CheckSkipAndSpell);

#endif
}

void FillSuggestionList(
	const QString &wrongWord,
	std::vector<QString> *optionalSuggestions) {

	const auto wordRange = NSMakeRange(0, wrongWord.length());
	auto *nsWord = Q2NSString(wrongWord);
	const auto guesses = [&](auto *lang) {
		return [SharedSpellChecker() guessesForWordRange:wordRange
			inString:nsWord
			language:lang
			inSpellDocumentWithTag:0];
	};

	auto wordCounter = 0;
	const auto wordScript = ::Spellchecker::WordScript(wrongWord);
	optionalSuggestions->reserve(kMaxSuggestions);

	// for (NSString *lang in [SharedSpellChecker() availableLanguages]) {
	for (const auto &lang : SystemLanguages()) {
		if (wordScript != ::Spellchecker::LocaleToScriptCode(lang)) {
			continue;
		}
		for (NSString *guess in guesses(Q2NSString(lang))) {
			optionalSuggestions->push_back(NS2QString(guess));
			if (++wordCounter >= kMaxSuggestions) {
				return;
			}
		}
	}
}

void AddWord(const QString &word) {
	[SharedSpellChecker() learnWord:Q2NSString(word)];
}

void RemoveWord(const QString &word) {
	[SharedSpellChecker() unlearnWord:Q2NSString(word)];
}

void IgnoreWord(const QString &word) {
	[SharedSpellChecker() ignoreWord:Q2NSString(word)
		inSpellDocumentWithTag:0];
}

bool IsWordInDictionary(const QString &wordToCheck) {
	return [SharedSpellChecker() hasLearnedWord:Q2NSString(wordToCheck)];
}

bool IsSystemSpellchecker() {
	return true;
}

void UpdateLanguages(std::vector<int> languages) {
	::Spellchecker::UpdateSupportedScripts(SystemLanguages());
}

} // namespace Platform::Spellchecker
