1/*
2 * Copyright (c) 2021 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16#include "ecmascript/js_plural_rules.h"
17
18#include "ecmascript/object_factory-inl.h"
19#include "ecmascript/js_number_format.h"
20#include "ecmascript/checkpoint/thread_state_transition.h"
21
22namespace panda::ecmascript {
23constexpr int32_t STRING_SEPARATOR_LENGTH = 4;
24
25icu::number::LocalizedNumberFormatter *JSPluralRules::GetIcuNumberFormatter() const
26{
27    ASSERT(GetIcuNF().IsJSNativePointer());
28    auto result = JSNativePointer::Cast(GetIcuNF().GetTaggedObject())->GetExternalPointer();
29    return reinterpret_cast<icu::number::LocalizedNumberFormatter *>(result);
30}
31
32void JSPluralRules::FreeIcuNumberFormatter([[maybe_unused]] void *env, void *pointer, void* hint)
33{
34    if (pointer == nullptr) {
35        return;
36    }
37    auto icuNumberFormatter = reinterpret_cast<icu::number::LocalizedNumberFormatter *>(pointer);
38    icuNumberFormatter->~LocalizedNumberFormatter();
39    if (hint != nullptr) {
40        reinterpret_cast<EcmaVM *>(hint)->GetNativeAreaAllocator()->FreeBuffer(pointer);
41    }
42}
43
44void JSPluralRules::SetIcuNumberFormatter(JSThread *thread, const JSHandle<JSPluralRules> &pluralRules,
45    const icu::number::LocalizedNumberFormatter &icuNF, const NativePointerCallback &callback)
46{
47    EcmaVM *ecmaVm = thread->GetEcmaVM();
48    ObjectFactory *factory = ecmaVm->GetFactory();
49
50    icu::number::LocalizedNumberFormatter *icuPointer =
51        ecmaVm->GetNativeAreaAllocator()->New<icu::number::LocalizedNumberFormatter>(icuNF);
52    ASSERT(icuPointer != nullptr);
53    JSTaggedValue data = pluralRules->GetIcuNF();
54    if (data.IsHeapObject() && data.IsJSNativePointer()) {
55        JSNativePointer *native = JSNativePointer::Cast(data.GetTaggedObject());
56        native->ResetExternalPointer(thread, icuPointer);
57        return;
58    }
59    JSHandle<JSNativePointer> pointer = factory->NewJSNativePointer(icuPointer, callback, ecmaVm);
60    pluralRules->SetIcuNF(thread, pointer.GetTaggedValue());
61}
62
63icu::PluralRules *JSPluralRules::GetIcuPluralRules() const
64{
65    ASSERT(GetIcuPR().IsJSNativePointer());
66    auto result = JSNativePointer::Cast(GetIcuPR().GetTaggedObject())->GetExternalPointer();
67    return reinterpret_cast<icu::PluralRules *>(result);
68}
69
70void JSPluralRules::FreeIcuPluralRules([[maybe_unused]] void *env, void *pointer, void* hint)
71{
72    if (pointer == nullptr) {
73        return;
74    }
75    auto icuPluralRules = reinterpret_cast<icu::PluralRules *>(pointer);
76    icuPluralRules->~PluralRules();
77    if (hint != nullptr) {
78        reinterpret_cast<EcmaVM *>(hint)->GetNativeAreaAllocator()->FreeBuffer(pointer);
79    }
80}
81
82void JSPluralRules::SetIcuPluralRules(JSThread *thread, const JSHandle<JSPluralRules> &pluralRules,
83    const icu::PluralRules &icuPR, const NativePointerCallback &callback)
84{
85    [[maybe_unused]] EcmaHandleScope scope(thread);
86    EcmaVM *ecmaVm = thread->GetEcmaVM();
87    ObjectFactory *factory = ecmaVm->GetFactory();
88
89    icu::PluralRules *icuPointer = ecmaVm->GetNativeAreaAllocator()->New<icu::PluralRules>(icuPR);
90    ASSERT(icuPointer != nullptr);
91    JSTaggedValue data = pluralRules->GetIcuPR();
92    if (data.IsHeapObject() && data.IsJSNativePointer()) {
93        JSNativePointer *native = JSNativePointer::Cast(data.GetTaggedObject());
94        native->ResetExternalPointer(thread, icuPointer);
95        return;
96    }
97    JSHandle<JSNativePointer> pointer = factory->NewJSNativePointer(icuPointer, callback, ecmaVm);
98    pluralRules->SetIcuPR(thread, pointer.GetTaggedValue());
99}
100
101JSHandle<TaggedArray> JSPluralRules::BuildLocaleSet(JSThread *thread, const std::set<std::string> &icuAvailableLocales)
102{
103    EcmaVM *ecmaVm = thread->GetEcmaVM();
104    ObjectFactory *factory = ecmaVm->GetFactory();
105    JSHandle<TaggedArray> locales = factory->NewTaggedArray(icuAvailableLocales.size());
106    int32_t index = 0;
107
108    for (const std::string &locale : icuAvailableLocales) {
109        JSHandle<EcmaString> localeStr = factory->NewFromStdString(locale);
110        locales->Set(thread, index++, localeStr);
111    }
112    return locales;
113}
114
115bool GetNextLocale(icu::StringEnumeration *locales, std::string &localeStr, int32_t *len)
116{
117    UErrorCode status = U_ZERO_ERROR;
118    const char *locale = nullptr;
119    locale = locales->next(len, status);
120    if (!U_SUCCESS(status) || locale == nullptr) {
121        localeStr = "";
122        return false;
123    }
124    localeStr = std::string(locale);
125    return true;
126}
127
128JSHandle<TaggedArray> JSPluralRules::GetAvailableLocales(JSThread *thread)
129{
130    UErrorCode status = U_ZERO_ERROR;
131    std::unique_ptr<icu::StringEnumeration> locales(icu::PluralRules::getAvailableLocales(status));
132    ASSERT(U_SUCCESS(status));
133    std::set<std::string> set;
134    std::string localeStr;
135    int32_t len = 0;
136    {
137        ThreadNativeScope nativeScope(thread);
138        while (GetNextLocale(locales.get(), localeStr, &len)) {
139            if (len >= STRING_SEPARATOR_LENGTH) {
140                std::replace(localeStr.begin(), localeStr.end(), '_', '-');
141            }
142            set.insert(localeStr);
143        }
144    }
145    return BuildLocaleSet(thread, set);
146}
147
148// InitializePluralRules ( pluralRules, locales, options )
149JSHandle<JSPluralRules> JSPluralRules::InitializePluralRules(JSThread *thread,
150                                                             const JSHandle<JSPluralRules> &pluralRules,
151                                                             const JSHandle<JSTaggedValue> &locales,
152                                                             const JSHandle<JSTaggedValue> &options)
153{
154    EcmaVM *ecmaVm = thread->GetEcmaVM();
155    ObjectFactory *factory = ecmaVm->GetFactory();
156    auto globalConst = thread->GlobalConstants();
157
158    // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
159    JSHandle<TaggedArray> requestedLocales = intl::LocaleHelper::CanonicalizeLocaleList(thread, locales);
160    RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSPluralRules, thread);
161
162    // 2&3. If options is undefined, then Let options be ObjectCreate(null). else Let options be ? ToObject(options).
163    JSHandle<JSObject> prOptions;
164    if (!options->IsUndefined()) {
165        prOptions = JSTaggedValue::ToObject(thread, options);
166        RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSPluralRules, thread);
167    } else {
168        prOptions = factory->CreateNullJSObject();
169    }
170
171    // 5. Let matcher be ? GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit").
172    LocaleMatcherOption matcher =
173        JSLocale::GetOptionOfString(thread, prOptions, globalConst->GetHandledLocaleMatcherString(),
174                                    {LocaleMatcherOption::LOOKUP, LocaleMatcherOption::BEST_FIT},
175                                    {"lookup", "best fit"}, LocaleMatcherOption::BEST_FIT);
176    RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSPluralRules, thread);
177
178    // 7. Let t be ? GetOption(options, "type", "string", « "cardinal", "ordinal" », "cardinal").
179    JSHandle<JSTaggedValue> property = JSHandle<JSTaggedValue>::Cast(globalConst->GetHandledTypeString());
180    TypeOption type =
181        JSLocale::GetOptionOfString(thread, prOptions, property, { TypeOption::CARDINAL, TypeOption::ORDINAL },
182                                    { "cardinal", "ordinal" }, TypeOption::CARDINAL);
183    RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSPluralRules, thread);
184
185    // set pluralRules.[[type]] to type
186    pluralRules->SetType(type);
187
188    // Let r be ResolveLocale(%PluralRules%.[[AvailableLocales]], requestedLocales, opt,
189    // %PluralRules%.[[RelevantExtensionKeys]], localeData).
190    JSHandle<TaggedArray> availableLocales;
191    if (requestedLocales->GetLength() == 0) {
192        availableLocales = factory->EmptyArray();
193    } else {
194        availableLocales = GetAvailableLocales(thread);
195    }
196    std::set<std::string> relevantExtensionKeys{""};
197    ResolvedLocale r =
198        JSLocale::ResolveLocale(thread, availableLocales, requestedLocales, matcher, relevantExtensionKeys);
199    RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSPluralRules, thread);
200    icu::Locale icuLocale = r.localeData;
201
202    // Get ICU numberFormatter with given locale
203    icu::number::LocalizedNumberFormatter icuNumberFormatter =
204        icu::number::NumberFormatter::withLocale(icuLocale).roundingMode(UNUM_ROUND_HALFUP);
205
206    bool success = true;
207    UErrorCode status = U_ZERO_ERROR;
208    UPluralType icuType = UPLURAL_TYPE_CARDINAL;
209    // Trans typeOption to ICU typeOption
210    switch (type) {
211        case TypeOption::ORDINAL:
212            icuType = UPLURAL_TYPE_ORDINAL;
213            break;
214        case TypeOption::CARDINAL:
215            icuType = UPLURAL_TYPE_CARDINAL;
216            break;
217        default:
218            LOG_ECMA(FATAL) << "this branch is unreachable";
219            UNREACHABLE();
220    }
221    std::unique_ptr<icu::PluralRules> icuPluralRules(icu::PluralRules::forLocale(icuLocale, icuType, status));
222    if (U_FAILURE(status)) {  // NOLINT(readability-implicit-bool-conversion)
223        success = false;
224    }
225
226    // Trans typeOption to ICU typeOption
227    if (!success || icuPluralRules == nullptr) {
228        icu::Locale noExtensionLocale(icuLocale.getBaseName());
229        status = U_ZERO_ERROR;
230        switch (type) {
231            case TypeOption::ORDINAL:
232                icuType = UPLURAL_TYPE_ORDINAL;
233                break;
234            case TypeOption::CARDINAL:
235                icuType = UPLURAL_TYPE_CARDINAL;
236                break;
237            default:
238                LOG_ECMA(FATAL) << "this branch is unreachable";
239                UNREACHABLE();
240        }
241        icuPluralRules.reset(icu::PluralRules::forLocale(icuLocale, icuType, status));
242    }
243    if (U_FAILURE(status) || icuPluralRules == nullptr) {  // NOLINT(readability-implicit-bool-conversion)
244        THROW_RANGE_ERROR_AND_RETURN(thread, "cannot create icuPluralRules", pluralRules);
245    }
246
247    // 9. Perform ? SetNumberFormatDigitOptions(pluralRules, options, 0, 3, "standard").
248    JSLocale::SetNumberFormatDigitOptions(thread, pluralRules, JSHandle<JSTaggedValue>::Cast(prOptions), MNFD_DEFAULT,
249                                          MXFD_DEFAULT, NotationOption::STANDARD);
250    RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSPluralRules, thread);
251    icuNumberFormatter = JSNumberFormat::SetICUFormatterDigitOptions(icuNumberFormatter, pluralRules);
252
253    // Set pluralRules.[[IcuPluralRules]] to icuPluralRules
254    SetIcuPluralRules(thread, pluralRules, *icuPluralRules, JSPluralRules::FreeIcuPluralRules);
255
256    // Set pluralRules.[[IcuNumberFormat]] to icuNumberFormatter
257    SetIcuNumberFormatter(thread, pluralRules, icuNumberFormatter, JSPluralRules::FreeIcuNumberFormatter);
258
259    // 12. Set pluralRules.[[Locale]] to the value of r.[[locale]].
260    JSHandle<EcmaString> localeStr = intl::LocaleHelper::ToLanguageTag(thread, icuLocale);
261    RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSPluralRules, thread);
262    pluralRules->SetLocale(thread, localeStr.GetTaggedValue());
263
264    // 13. Return pluralRules.
265    return pluralRules;
266}
267
268JSHandle<EcmaString> FormatNumericToString(JSThread *thread, const icu::number::LocalizedNumberFormatter *icuFormatter,
269                                           const icu::PluralRules *icuPluralRules, double n)
270{
271    UErrorCode status = U_ZERO_ERROR;
272    icu::number::FormattedNumber formatted = icuFormatter->formatDouble(n, status);
273    if (U_FAILURE(status)) {  // NOLINT(readability-implicit-bool-conversion)
274        JSHandle<JSTaggedValue> exception(thread, JSTaggedValue::Exception());
275        THROW_RANGE_ERROR_AND_RETURN(thread, "invalid resolve number", JSHandle<EcmaString>::Cast(exception));
276    }
277
278    icu::UnicodeString uString = icuPluralRules->select(formatted, status);
279    if (U_FAILURE(status)) {  // NOLINT(readability-implicit-bool-conversion)
280        JSHandle<JSTaggedValue> exception(thread, JSTaggedValue::Exception());
281        THROW_RANGE_ERROR_AND_RETURN(thread, "invalid resolve number", JSHandle<EcmaString>::Cast(exception));
282    }
283
284    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
285    JSHandle<EcmaString> result =
286        factory->NewFromUtf16(reinterpret_cast<const uint16_t *>(uString.getBuffer()), uString.length());
287    return result;
288}
289JSHandle<EcmaString> JSPluralRules::ResolvePlural(JSThread *thread, const JSHandle<JSPluralRules> &pluralRules,
290                                                  double n)
291{
292    icu::PluralRules *icuPluralRules = pluralRules->GetIcuPluralRules();
293    icu::number::LocalizedNumberFormatter *icuFormatter = pluralRules->GetIcuNumberFormatter();
294    if (icuPluralRules == nullptr || icuFormatter == nullptr) {
295        return JSHandle<EcmaString>(thread, JSTaggedValue::Undefined());
296    }
297
298    JSHandle<EcmaString> result = FormatNumericToString(thread, icuFormatter, icuPluralRules, n);
299    RETURN_HANDLE_IF_ABRUPT_COMPLETION(EcmaString, thread);
300    return result;
301}
302
303void JSPluralRules::ResolvedOptions(JSThread *thread, const JSHandle<JSPluralRules> &pluralRules,
304                                    const JSHandle<JSObject> &options)
305{
306    EcmaVM *ecmaVm = thread->GetEcmaVM();
307    ObjectFactory *factory = ecmaVm->GetFactory();
308    auto globalConst = thread->GlobalConstants();
309
310    // [[Locale]]
311    JSHandle<JSTaggedValue> property = JSHandle<JSTaggedValue>::Cast(globalConst->GetHandledLocaleString());
312    JSHandle<EcmaString> locale(thread, pluralRules->GetLocale());
313    PropertyDescriptor localeDesc(thread, JSHandle<JSTaggedValue>::Cast(locale), true, true, true);
314    JSObject::DefineOwnProperty(thread, options, property, localeDesc);
315
316    // [[type]]
317    property = JSHandle<JSTaggedValue>::Cast(globalConst->GetHandledTypeString());
318    JSHandle<JSTaggedValue> typeValue;
319    if (pluralRules->GetType() == TypeOption::CARDINAL) {
320        typeValue = globalConst->GetHandledCardinalString();
321    } else {
322        typeValue = globalConst->GetHandledOrdinalString();
323    }
324    PropertyDescriptor typeDesc(thread, typeValue, true, true, true);
325    JSObject::DefineOwnProperty(thread, options, property, typeDesc);
326
327    // [[MinimumIntegerDigits]]
328    property = JSHandle<JSTaggedValue>::Cast(globalConst->GetHandledMinimumIntegerDigitsString());
329    JSHandle<JSTaggedValue> minimumIntegerDigits(thread, pluralRules->GetMinimumIntegerDigits());
330    JSObject::CreateDataPropertyOrThrow(thread, options, property, minimumIntegerDigits);
331    RETURN_IF_ABRUPT_COMPLETION(thread);
332
333    RoundingType roundingType = pluralRules->GetRoundingType();
334    if (roundingType == RoundingType::SIGNIFICANTDIGITS) {
335        // [[MinimumSignificantDigits]]
336        property = globalConst->GetHandledMinimumSignificantDigitsString();
337        JSHandle<JSTaggedValue> minimumSignificantDigits(thread, pluralRules->GetMinimumSignificantDigits());
338        JSObject::CreateDataPropertyOrThrow(thread, options, property, minimumSignificantDigits);
339        RETURN_IF_ABRUPT_COMPLETION(thread);
340        // [[MaximumSignificantDigits]]
341        property = globalConst->GetHandledMaximumSignificantDigitsString();
342        JSHandle<JSTaggedValue> maximumSignificantDigits(thread, pluralRules->GetMaximumSignificantDigits());
343        JSObject::CreateDataPropertyOrThrow(thread, options, property, maximumSignificantDigits);
344        RETURN_IF_ABRUPT_COMPLETION(thread);
345    } else {
346        // [[MinimumFractionDigits]]
347        property = globalConst->GetHandledMinimumFractionDigitsString();
348        JSHandle<JSTaggedValue> minimumFractionDigits(thread, pluralRules->GetMinimumFractionDigits());
349        JSObject::CreateDataPropertyOrThrow(thread, options, property, minimumFractionDigits);
350        RETURN_IF_ABRUPT_COMPLETION(thread);
351        // [[MaximumFractionDigits]]
352        property = globalConst->GetHandledMaximumFractionDigitsString();
353        JSHandle<JSTaggedValue> maximumFractionDigits(thread, pluralRules->GetMaximumFractionDigits());
354        JSObject::CreateDataPropertyOrThrow(thread, options, property, maximumFractionDigits);
355        RETURN_IF_ABRUPT_COMPLETION(thread);
356    }
357
358    // 5. Let pluralCategories be a List of Strings representing the possible results of PluralRuleSelect
359    // for the selected locale pr.[[Locale]]. This List consists of unique String values,
360    // from the the list "zero", "one", "two", "few", "many" and "other",
361    // that are relevant for the locale whose localization is specified in LDML Language Plural Rules.
362    UErrorCode status = U_ZERO_ERROR;
363    icu::PluralRules *icuPluralRules = pluralRules->GetIcuPluralRules();
364    ASSERT(icuPluralRules != nullptr);
365    std::unique_ptr<icu::StringEnumeration> categories(icuPluralRules->getKeywords(status));
366    int32_t count = categories->count(status);
367    ASSERT(U_SUCCESS(status));
368    JSHandle<TaggedArray> pluralCategories = factory->NewTaggedArray(count);
369    for (int32_t i = 0; i < count; i++) {
370        const icu::UnicodeString *category = categories->snext(status);
371        ASSERT(U_SUCCESS(status));
372        JSHandle<EcmaString> value = intl::LocaleHelper::UStringToString(thread, *category);
373        pluralCategories->Set(thread, i, value);
374    }
375
376    // 6. Perform ! CreateDataProperty(options, "pluralCategories", CreateArrayFromList(pluralCategories)).
377    property = globalConst->GetHandledPluralCategoriesString();
378    JSHandle<JSArray> jsPluralCategories = JSArray::CreateArrayFromList(thread, pluralCategories);
379    JSObject::CreateDataPropertyOrThrow(thread, options, property, JSHandle<JSTaggedValue>::Cast(jsPluralCategories));
380    RETURN_IF_ABRUPT_COMPLETION(thread);
381}
382}  // namespace panda::ecmascript
383