/*
 * Copyright (C) 2020 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#pragma once

#include "BuiltinNames.h"
#include "IntlNumberFormat.h"
#include "IntlObjectInlines.h"
#include "JSGlobalObject.h"
#include "JSGlobalObjectFunctions.h"

namespace JSC  {

template<typename IntlType>
void setNumberFormatDigitOptions(JSGlobalObject* globalObject, IntlType* intlInstance, JSObject* options, unsigned minimumFractionDigitsDefault, unsigned maximumFractionDigitsDefault, IntlNotation notation)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    unsigned minimumIntegerDigits = intlNumberOption(globalObject, options, vm.propertyNames->minimumIntegerDigits, 1, 21, 1);
    RETURN_IF_EXCEPTION(scope, void());

    JSValue minimumFractionDigitsValue = jsUndefined();
    JSValue maximumFractionDigitsValue = jsUndefined();
    JSValue minimumSignificantDigitsValue = jsUndefined();
    JSValue maximumSignificantDigitsValue = jsUndefined();
    if (options) {
        minimumFractionDigitsValue = options->get(globalObject, vm.propertyNames->minimumFractionDigits);
        RETURN_IF_EXCEPTION(scope, void());

        maximumFractionDigitsValue = options->get(globalObject, vm.propertyNames->maximumFractionDigits);
        RETURN_IF_EXCEPTION(scope, void());

        minimumSignificantDigitsValue = options->get(globalObject, vm.propertyNames->minimumSignificantDigits);
        RETURN_IF_EXCEPTION(scope, void());

        maximumSignificantDigitsValue = options->get(globalObject, vm.propertyNames->maximumSignificantDigits);
        RETURN_IF_EXCEPTION(scope, void());
    }

    intlInstance->m_minimumIntegerDigits = minimumIntegerDigits;

    IntlRoundingPriority roundingPriority = intlOption<IntlRoundingPriority>(globalObject, options, vm.propertyNames->roundingPriority, { { "auto"_s, IntlRoundingPriority::Auto }, { "morePrecision"_s, IntlRoundingPriority::MorePrecision }, { "lessPrecision"_s, IntlRoundingPriority::LessPrecision } }, "roundingPriority must be either \"auto\", \"morePrecision\", or \"lessPrecision\""_s, IntlRoundingPriority::Auto);
    RETURN_IF_EXCEPTION(scope, void());

    bool hasSd = !minimumSignificantDigitsValue.isUndefined() || !maximumSignificantDigitsValue.isUndefined();
    bool hasFd = !minimumFractionDigitsValue.isUndefined() || !maximumFractionDigitsValue.isUndefined();
    bool needSd = hasSd || roundingPriority != IntlRoundingPriority::Auto;
    bool needFd = (!hasSd && notation != IntlNotation::Compact) || roundingPriority != IntlRoundingPriority::Auto;

    if (needSd) {
        if (hasSd) {
            unsigned minimumSignificantDigits = intlDefaultNumberOption(globalObject, minimumSignificantDigitsValue, vm.propertyNames->minimumSignificantDigits, 1, 21, 1);
            RETURN_IF_EXCEPTION(scope, void());
            unsigned maximumSignificantDigits = intlDefaultNumberOption(globalObject, maximumSignificantDigitsValue, vm.propertyNames->maximumSignificantDigits, minimumSignificantDigits, 21, 21);
            RETURN_IF_EXCEPTION(scope, void());
            intlInstance->m_minimumSignificantDigits = minimumSignificantDigits;
            intlInstance->m_maximumSignificantDigits = maximumSignificantDigits;
        } else {
            intlInstance->m_minimumSignificantDigits = 1;
            intlInstance->m_maximumSignificantDigits = 21;
        }
    }

    if (needFd) {
        if (hasFd) {
            constexpr unsigned undefinedValue = UINT32_MAX;
            unsigned minimumFractionDigits = intlDefaultNumberOption(globalObject, minimumFractionDigitsValue, vm.propertyNames->minimumFractionDigits, 0, 20, undefinedValue);
            RETURN_IF_EXCEPTION(scope, void());
            unsigned maximumFractionDigits = intlDefaultNumberOption(globalObject, maximumFractionDigitsValue, vm.propertyNames->maximumFractionDigits, 0, 20, undefinedValue);
            RETURN_IF_EXCEPTION(scope, void());

            if (minimumFractionDigits == undefinedValue)
                minimumFractionDigits = std::min(minimumFractionDigitsDefault, maximumFractionDigits);
            else if (maximumFractionDigits == undefinedValue)
                maximumFractionDigits = std::max(maximumFractionDigitsDefault, minimumFractionDigits);
            else if (minimumFractionDigits > maximumFractionDigits) {
                throwRangeError(globalObject, scope, "Computed minimumFractionDigits is larger than maximumFractionDigits"_s);
                return;
            }

            intlInstance->m_minimumFractionDigits = minimumFractionDigits;
            intlInstance->m_maximumFractionDigits = maximumFractionDigits;
        } else {
            intlInstance->m_minimumFractionDigits = minimumFractionDigitsDefault;
            intlInstance->m_maximumFractionDigits = maximumFractionDigitsDefault;
        }
    }

    if (needSd || needFd) {
        if (roundingPriority == IntlRoundingPriority::MorePrecision)
            intlInstance->m_roundingType = IntlRoundingType::MorePrecision;
        else if (roundingPriority == IntlRoundingPriority::LessPrecision)
            intlInstance->m_roundingType = IntlRoundingType::LessPrecision;
        else if (hasSd)
            intlInstance->m_roundingType = IntlRoundingType::SignificantDigits;
        else
            intlInstance->m_roundingType = IntlRoundingType::FractionDigits;
    } else {
        intlInstance->m_roundingType = IntlRoundingType::MorePrecision;
        intlInstance->m_minimumFractionDigits = 0;
        intlInstance->m_maximumFractionDigits = 0;
        intlInstance->m_minimumSignificantDigits = 1;
        intlInstance->m_maximumSignificantDigits = 2;
    }
}

template<typename IntlType>
void appendNumberFormatDigitOptionsToSkeleton(IntlType* intlInstance, StringBuilder& skeletonBuilder)
{
    // https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md#integer-width
    skeletonBuilder.append(" integer-width/", WTF::ICU::majorVersion() >= 67 ? '*' : '+'); // Prior to ICU 67, use the symbol + instead of *.
    for (unsigned i = 0; i < intlInstance->m_minimumIntegerDigits; ++i)
        skeletonBuilder.append('0');

    if (intlInstance->m_roundingIncrement != 1) {
        skeletonBuilder.append(" precision-increment/");
        auto string = numberToStringUnsigned<Vector<LChar, 10>>(intlInstance->m_roundingIncrement);
        if (intlInstance->m_maximumFractionDigits >= string.size()) {
            skeletonBuilder.append("0.");
            for (unsigned i = 0; i < (intlInstance->m_maximumFractionDigits - string.size()); ++i)
                skeletonBuilder.append('0');
            skeletonBuilder.appendCharacters(string.data(), string.size());
        } else {
            unsigned nonFraction = string.size() - intlInstance->m_maximumFractionDigits;
            skeletonBuilder.appendCharacters(string.data(), nonFraction);
            skeletonBuilder.append('.');
            skeletonBuilder.appendCharacters(string.data() + nonFraction, intlInstance->m_maximumFractionDigits);
        }
    } else {
        switch (intlInstance->m_roundingType) {
        case IntlRoundingType::FractionDigits: {
            // https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md#fraction-precision
            skeletonBuilder.append(" .");
            for (unsigned i = 0; i < intlInstance->m_minimumFractionDigits; ++i)
                skeletonBuilder.append('0');
            for (unsigned i = 0; i < intlInstance->m_maximumFractionDigits - intlInstance->m_minimumFractionDigits; ++i)
                skeletonBuilder.append('#');
            break;
        }
        case IntlRoundingType::SignificantDigits: {
            // https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md#significant-digits-precision
            skeletonBuilder.append(' ');
            for (unsigned i = 0; i < intlInstance->m_minimumSignificantDigits; ++i)
                skeletonBuilder.append('@');
            for (unsigned i = 0; i < intlInstance->m_maximumSignificantDigits - intlInstance->m_minimumSignificantDigits; ++i)
                skeletonBuilder.append('#');
            break;
        }
        case IntlRoundingType::MorePrecision:
        case IntlRoundingType::LessPrecision:
            // Before Intl.NumberFormat v3, it was CompactRounding mode, where we do not configure anything.
            // So, if linked ICU is ~68, we do nothing.
            if (WTF::ICU::majorVersion() >= 69) {
                // https://github.com/unicode-org/icu/commit/d7db6c1f8655bb53153695b09a50029fd04a8364
                // https://github.com/unicode-org/icu/blob/main/docs/userguide/format_parse/numbers/skeletons.md#precision
                skeletonBuilder.append(" .");
                for (unsigned i = 0; i < intlInstance->m_minimumFractionDigits; ++i)
                    skeletonBuilder.append('0');
                for (unsigned i = 0; i < intlInstance->m_maximumFractionDigits - intlInstance->m_minimumFractionDigits; ++i)
                    skeletonBuilder.append('#');
                skeletonBuilder.append('/');
                for (unsigned i = 0; i < intlInstance->m_minimumSignificantDigits; ++i)
                    skeletonBuilder.append('@');
                for (unsigned i = 0; i < intlInstance->m_maximumSignificantDigits - intlInstance->m_minimumSignificantDigits; ++i)
                    skeletonBuilder.append('#');
                skeletonBuilder.append(intlInstance->m_roundingType == IntlRoundingType::MorePrecision ? 'r' : 's');
            }
            break;
        }
    }
}

class IntlFieldIterator {
public:
    WTF_MAKE_NONCOPYABLE(IntlFieldIterator);

    explicit IntlFieldIterator(UFieldPositionIterator& iterator)
        : m_iterator(iterator)
    {
    }

    int32_t next(int32_t& beginIndex, int32_t& endIndex, UErrorCode&)
    {
        return ufieldpositer_next(&m_iterator, &beginIndex, &endIndex);
    }

private:
    UFieldPositionIterator& m_iterator;
};

// https://tc39.es/ecma402/#sec-unwrapnumberformat
inline IntlNumberFormat* IntlNumberFormat::unwrapForOldFunctions(JSGlobalObject* globalObject, JSValue thisValue)
{
    return unwrapForLegacyIntlConstructor<IntlNumberFormat>(globalObject, thisValue, globalObject->numberFormatConstructor());
}

// https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-tointlmathematicalvalue
inline IntlMathematicalValue toIntlMathematicalValue(JSGlobalObject* globalObject, JSValue value)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (auto number = JSBigInt::tryExtractDouble(value))
        return IntlMathematicalValue { number.value() };

    JSValue primitive = value.toPrimitive(globalObject, PreferredPrimitiveType::PreferNumber);
    RETURN_IF_EXCEPTION(scope, { });

    auto bigIntToIntlMathematicalValue = [](JSGlobalObject* globalObject, JSValue value) -> IntlMathematicalValue {
        VM& vm = globalObject->vm();
        auto scope = DECLARE_THROW_SCOPE(vm);

        if (auto number = JSBigInt::tryExtractDouble(value))
            return IntlMathematicalValue { number.value() };

        auto* bigInt = value.asHeapBigInt();
        auto string = bigInt->toString(globalObject, 10);
        RETURN_IF_EXCEPTION(scope, { });
        return IntlMathematicalValue {
            IntlMathematicalValue::NumberType::Integer,
            bigInt->sign(),
            string.ascii(),
        };
    };

    if (primitive.isBigInt())
        RELEASE_AND_RETURN(scope, bigIntToIntlMathematicalValue(globalObject, primitive));

    if (!primitive.isString())
        RELEASE_AND_RETURN(scope, IntlMathematicalValue { primitive.toNumber(globalObject) });

    String string = asString(primitive)->value(globalObject);
    RETURN_IF_EXCEPTION(scope, { });

    JSValue bigInt = JSBigInt::stringToBigInt(globalObject, string);
    if (bigInt) {
        // If it is -0, we cannot handle it in JSBigInt. Reparse the string as double.
#if USE(BIGINT32)
        if (bigInt.isBigInt32() && !value.bigInt32AsInt32())
            return IntlMathematicalValue { jsToNumber(string) };
#endif
        if (bigInt.isHeapBigInt() && !asHeapBigInt(bigInt)->length())
            return IntlMathematicalValue { jsToNumber(string) };
        RELEASE_AND_RETURN(scope, bigIntToIntlMathematicalValue(globalObject, bigInt));
    }

    return IntlMathematicalValue { jsToNumber(string) };
}

} // namespace JSC
