/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/intl/NumberRangeFormat.h"

#include "mozilla/Try.h"
#include "mozilla/intl/ICU4CGlue.h"
#include "mozilla/intl/NumberFormat.h"
#include "NumberFormatFields.h"
#include "NumberFormatterSkeleton.h"
#include "ScopedICUObject.h"

#include "unicode/uformattedvalue.h"
#include "unicode/unumberrangeformatter.h"
#include "unicode/upluralrules.h"

namespace mozilla::intl {

/*static*/ Result<UniquePtr<NumberRangeFormat>, ICUError>
NumberRangeFormat::TryCreate(std::string_view aLocale,
                             const NumberRangeFormatOptions& aOptions) {
  UniquePtr<NumberRangeFormat> nrf = MakeUnique<NumberRangeFormat>();
  MOZ_TRY(nrf->initialize(aLocale, aOptions));
  return nrf;
}

NumberRangeFormat::~NumberRangeFormat() {
  if (mFormattedNumberRange) {
    unumrf_closeResult(mFormattedNumberRange);
  }
  if (mNumberRangeFormatter) {
    unumrf_close(mNumberRangeFormatter);
  }
}

Result<Ok, ICUError> NumberRangeFormat::initialize(
    std::string_view aLocale, const NumberRangeFormatOptions& aOptions) {
  mFormatForUnit = aOptions.mUnit.isSome();

  NumberFormatterSkeleton skeleton(aOptions);
  mNumberRangeFormatter = skeleton.toRangeFormatter(
      aLocale, aOptions.mRangeCollapse, aOptions.mRangeIdentityFallback);
  if (mNumberRangeFormatter) {
    UErrorCode status = U_ZERO_ERROR;
    mFormattedNumberRange = unumrf_openResult(&status);
    if (U_FAILURE(status)) {
      return Err(ToICUError(status));
    }
    return Ok();
  }
  return Err(ICUError::InternalError);
}

Result<int32_t, ICUError> NumberRangeFormat::selectForRange(
    double start, double end, char16_t* keyword, int32_t keywordSize,
    const UPluralRules* pluralRules) const {
  MOZ_ASSERT(keyword);
  MOZ_ASSERT(pluralRules);

  MOZ_TRY(format(start, end));

  UErrorCode status = U_ZERO_ERROR;
  int32_t utf16KeywordLength = uplrules_selectForRange(
      pluralRules, mFormattedNumberRange, keyword, keywordSize, &status);
  if (U_FAILURE(status)) {
    return Err(ToICUError(status));
  }

  return utf16KeywordLength;
}

Result<int32_t, ICUError> NumberRangeFormat::selectForRange(
    std::string_view start, std::string_view end, char16_t* keyword,
    int32_t keywordSize, const UPluralRules* pluralRules) const {
  MOZ_ASSERT(keyword);
  MOZ_ASSERT(pluralRules);

  MOZ_TRY(format(start, end));

  UErrorCode status = U_ZERO_ERROR;
  int32_t utf16KeywordLength = uplrules_selectForRange(
      pluralRules, mFormattedNumberRange, keyword, keywordSize, &status);
  if (U_FAILURE(status)) {
    return Err(ToICUError(status));
  }

  return utf16KeywordLength;
}

bool NumberRangeFormat::formatInternal(double start, double end) const {
  // ICU incorrectly formats NaN values with the sign bit set, as if they
  // were negative.  Replace all NaNs with a single pattern with sign bit
  // unset ("positive", that is) until ICU is fixed.
  if (MOZ_UNLIKELY(std::isnan(start))) {
    start = SpecificNaN<double>(0, 1);
  }
  if (MOZ_UNLIKELY(std::isnan(end))) {
    end = SpecificNaN<double>(0, 1);
  }

  UErrorCode status = U_ZERO_ERROR;
  unumrf_formatDoubleRange(mNumberRangeFormatter, start, end,
                           mFormattedNumberRange, &status);
  return U_SUCCESS(status);
}

bool NumberRangeFormat::formatInternal(std::string_view start,
                                       std::string_view end) const {
  UErrorCode status = U_ZERO_ERROR;
  unumrf_formatDecimalRange(mNumberRangeFormatter, start.data(), start.size(),
                            end.data(), end.size(), mFormattedNumberRange,
                            &status);
  return U_SUCCESS(status);
}

Result<std::u16string_view, ICUError> NumberRangeFormat::formatResult() const {
  UErrorCode status = U_ZERO_ERROR;

  const UFormattedValue* formattedValue =
      unumrf_resultAsValue(mFormattedNumberRange, &status);
  if (U_FAILURE(status)) {
    return Err(ToICUError(status));
  }

  int32_t utf16Length;
  const char16_t* utf16Str =
      ufmtval_getString(formattedValue, &utf16Length, &status);
  if (U_FAILURE(status)) {
    return Err(ToICUError(status));
  }

  return std::u16string_view(utf16Str, static_cast<size_t>(utf16Length));
}

Result<std::u16string_view, ICUError> NumberRangeFormat::formatResultToParts(
    Maybe<double> start, bool startIsNegative, Maybe<double> end,
    bool endIsNegative, NumberPartVector& parts) const {
  UErrorCode status = U_ZERO_ERROR;

  const UFormattedValue* formattedValue =
      unumrf_resultAsValue(mFormattedNumberRange, &status);
  if (U_FAILURE(status)) {
    return Err(ToICUError(status));
  }

  int32_t utf16Length;
  const char16_t* utf16Str =
      ufmtval_getString(formattedValue, &utf16Length, &status);
  if (U_FAILURE(status)) {
    return Err(ToICUError(status));
  }

  UConstrainedFieldPosition* fpos = ucfpos_open(&status);
  if (U_FAILURE(status)) {
    return Err(ToICUError(status));
  }
  ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos);

  Maybe<double> number = start;
  bool isNegative = startIsNegative;

  NumberPartSourceMap sourceMap;

  // Vacuum up fields in the overall formatted string.
  NumberFormatFields fields;

  while (true) {
    bool hasMore = ufmtval_nextPosition(formattedValue, fpos, &status);
    if (U_FAILURE(status)) {
      return Err(ToICUError(status));
    }
    if (!hasMore) {
      break;
    }

    int32_t category = ucfpos_getCategory(fpos, &status);
    if (U_FAILURE(status)) {
      return Err(ToICUError(status));
    }

    int32_t fieldName = ucfpos_getField(fpos, &status);
    if (U_FAILURE(status)) {
      return Err(ToICUError(status));
    }

    int32_t beginIndex, endIndex;
    ucfpos_getIndexes(fpos, &beginIndex, &endIndex, &status);
    if (U_FAILURE(status)) {
      return Err(ToICUError(status));
    }

    if (category == UFIELD_CATEGORY_NUMBER_RANGE_SPAN) {
      // The special field category UFIELD_CATEGORY_NUMBER_RANGE_SPAN has only
      // two allowed values (0 or 1), indicating the begin of the start resp.
      // end number.
      MOZ_ASSERT(fieldName == 0 || fieldName == 1,
                 "span category has unexpected value");

      if (fieldName == 0) {
        number = start;
        isNegative = startIsNegative;

        sourceMap.start = {uint32_t(beginIndex), uint32_t(endIndex)};
      } else {
        number = end;
        isNegative = endIsNegative;

        sourceMap.end = {uint32_t(beginIndex), uint32_t(endIndex)};
      }

      continue;
    }

    // Ignore categories other than UFIELD_CATEGORY_NUMBER.
    if (category != UFIELD_CATEGORY_NUMBER) {
      continue;
    }

    Maybe<NumberPartType> partType = GetPartTypeForNumberField(
        UNumberFormatFields(fieldName), number, isNegative, mFormatForUnit);
    if (!partType || !fields.append(*partType, beginIndex, endIndex)) {
      return Err(ToICUError(status));
    }
  }

  if (!fields.toPartsVector(utf16Length, sourceMap, parts)) {
    return Err(ToICUError(status));
  }

  return std::u16string_view(utf16Str, static_cast<size_t>(utf16Length));
}

}  // namespace mozilla::intl
