/*
 * Copyright (c) 2010, 2023 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {Locale, numbers, RoundingMode, scout, strings} from '../index';

/**
 * Provides formatting of numbers using java format pattern.
 *
 * Compared to the java DecimalFormat the following pattern characters are not considered:
 * - E
 * - %
 */
export class DecimalFormat {
  positivePrefix: string;
  positiveSuffix: string;
  negativePrefix: string;
  negativeSuffix: string;
  groupingChar: string;
  lenientGroupingChars: string;
  groupLength: number;
  decimalSeparatorChar: string;
  zeroBefore: number;
  zeroAfter: number;
  allAfter: number;
  pattern: string;
  multiplier: number;
  roundingMode: RoundingMode;

  constructor(locale: Locale, options?: string | DecimalFormatOptions) {
    // format function will use these (defaults)
    this.positivePrefix = '';
    this.positiveSuffix = '';
    this.negativePrefix = locale.decimalFormatSymbols.minusSign;
    this.negativeSuffix = '';
    this.groupingChar = locale.decimalFormatSymbols.groupingSeparator;
    // we want to be lenient when it comes to grouping separators, try the locale default plus a few others
    this.lenientGroupingChars = '\'´`’' + // apostrophe and variations
      '\u00B7' + // middle dot
      '\u0020' + // space
      '\u00A0' + // no-break space
      '\u2009' + // thin space
      '\u202F'; // narrow no-break space
    this.groupLength = 0;
    this.decimalSeparatorChar = locale.decimalFormatSymbols.decimalSeparator;
    this.zeroBefore = 1;
    this.zeroAfter = 0;
    this.allAfter = 0;

    if (typeof options === 'string') {
      this.pattern = options;
      this.multiplier = 1;
      this.roundingMode = RoundingMode.HALF_UP;
    } else {
      options = options || {pattern: null};
      this.pattern = this.pattern || options.pattern || locale.decimalFormatPatternDefault;
      this.multiplier = options.multiplier || 1;
      this.roundingMode = options.roundingMode || RoundingMode.HALF_UP;
    }

    let SYMBOLS = DecimalFormat.PATTERN_SYMBOLS;
    // Check if there are separate subpatterns for positive and negative numbers ("PositivePattern;NegativePattern")
    let split = this.pattern.split(SYMBOLS.patternSeparator);
    // Use the first subpattern as positive prefix/suffix
    let positivePrefixAndSuffix = findPrefixAndSuffix(split[0]);
    this.positivePrefix = positivePrefixAndSuffix.prefix;
    this.positiveSuffix = positivePrefixAndSuffix.suffix;
    if (split.length > 1) {
      // Yes, there is a negative subpattern
      let negativePrefixAndSuffix = findPrefixAndSuffix(split[1]);
      this.negativePrefix = negativePrefixAndSuffix.prefix;
      this.negativeSuffix = negativePrefixAndSuffix.suffix;
      // from now on, only look at the positive subpattern
      this.pattern = split[0];
    } else {
      // No, there is no negative subpattern, so the positive prefix/suffix are used for both positive and negative numbers.
      // Check if there is a minus sign in the prefix/suffix.
      if (this.positivePrefix.indexOf(SYMBOLS.minusSign) !== -1 || this.positiveSuffix.indexOf(SYMBOLS.minusSign) !== -1) {
        // Yes, there is a minus sign in the prefix/suffix. Use this a negativePrefix/Suffix and remove the minus sign from the posistivePrefix/Suffix.
        this.negativePrefix = this.positivePrefix.replace(SYMBOLS.minusSign, locale.decimalFormatSymbols.minusSign);
        this.negativeSuffix = this.positiveSuffix.replace(SYMBOLS.minusSign, locale.decimalFormatSymbols.minusSign);
        this.positivePrefix = this.positivePrefix.replace(SYMBOLS.minusSign, '');
        this.positiveSuffix = this.positiveSuffix.replace(SYMBOLS.minusSign, '');
      } else {
        // No, there is no minus sign in the prefix/suffix. Therefore, use the default negativePrefix/Suffix, but append the positivePrefix/Suffix
        this.negativePrefix = this.positivePrefix + this.negativePrefix;
        this.negativeSuffix = this.negativeSuffix + this.positiveSuffix;
      }
    }

    // find group length
    let posDecimalSeparator = this.pattern.indexOf(SYMBOLS.decimalSeparator);
    if (posDecimalSeparator === -1) {
      posDecimalSeparator = this.pattern.length; // assume decimal separator at end
    }
    let posGroupingSeparator = this.pattern.lastIndexOf(SYMBOLS.groupingSeparator, posDecimalSeparator); // only search before decimal separator
    if (posGroupingSeparator > 0) {
      this.groupLength = posDecimalSeparator - posGroupingSeparator - 1;
    }
    this.pattern = this.pattern.replace(new RegExp('[' + SYMBOLS.groupingSeparator + ']', 'g'), '');

    // split on decimal point
    split = this.pattern.split(SYMBOLS.decimalSeparator);

    // find digits before and after decimal point
    this.zeroBefore = strings.count(split[0], SYMBOLS.zeroDigit);
    if (split.length > 1) { // has decimal point?
      this.zeroAfter = strings.count(split[1], SYMBOLS.zeroDigit);
      this.allAfter = this.zeroAfter + strings.count(split[1], SYMBOLS.digit);
    }

    // Returns an object with the properties 'prefix' and 'suffix', which contain all characters
    // before or after any 'digit-like' character in the given pattern string.
    function findPrefixAndSuffix(pattern) {
      let result = {
        prefix: '',
        suffix: ''
      };
      // Find prefix (anything before the first 'digit-like' character)
      let digitLikeCharacters = SYMBOLS.digit + SYMBOLS.zeroDigit + SYMBOLS.decimalSeparator + SYMBOLS.groupingSeparator;
      let r = new RegExp('^(.*?)[' + digitLikeCharacters + '].*$');
      let matches = r.exec(pattern);
      if (matches !== null) {
        // Ignore single quotes (for special, quoted characters - e.g. Java quotes percentage sign like '%')
        result.prefix = matches[1].replace(new RegExp('\'([^\']+)\'', 'g'), '$1');
      }
      // Find suffix (anything before the first 'digit-like' character)
      r = new RegExp('^.*[' + digitLikeCharacters + '](.*?)$');
      matches = r.exec(pattern);
      if (matches !== null) {
        // Ignore single quotes (for special, quoted characters - e.g. Java quotes percentage sign like '%')
        result.suffix = matches[1].replace(new RegExp('\'([^\']+)\'', 'g'), '$1');
      }
      return result;
    }
  }

  /**
   * Converts the numberString into a number and applies the multiplier.
   * @param evaluateNumberFunction optional function for custom evaluation. The function gets a normalized string and has to return a Number
   * @returns A number for the given numberString, if the string can be converted into a number. Throws an Error otherwise
   */
  parse(numberString: string, evaluateNumberFunction?: (normalizedNumberString: string) => number): number {
    if (strings.empty(numberString)) {
      return null;
    }
    let normalizedNumberString = this.normalize(numberString);
    evaluateNumberFunction = evaluateNumberFunction || Number;
    let number = evaluateNumberFunction(normalizedNumberString);

    if (isNaN(number)) {
      throw new Error(numberString + ' is not a number (NaN)');
    }
    if (this.multiplier !== 1) {
      number /= this.multiplier;
    }
    return number;
  }

  format(number: number, applyMultiplier?: boolean): string {
    if (number === null || number === undefined) {
      return null;
    }

    let prefix = this.positivePrefix;
    let suffix = this.positiveSuffix;

    // apply multiplier
    applyMultiplier = scout.nvl(applyMultiplier, true);
    if (applyMultiplier && this.multiplier !== 1) {
      number *= this.multiplier;
    }

    // round
    number = this.round(number);

    // after decimal point
    let after = '';
    if (this.allAfter) {
      after = number.toFixed(this.allAfter).split('.')[1];
      for (let j = after.length - 1; j > this.zeroAfter - 1; j--) {
        if (after[j] !== '0') {
          break;
        }
        after = after.slice(0, -1);
      }
      if (after) { // did we find any non-zero characters?
        after = this.decimalSeparatorChar + after;
      }
    }

    // absolute value
    if (number < 0) {
      prefix = this.negativePrefix;
      suffix = this.negativeSuffix;
      number = -number;
    }

    // before decimal point
    let b = Math.floor(number);
    let before = (b === 0) ? '' : String(b);
    before = strings.padZeroLeft(before, this.zeroBefore);

    // group digits
    if (this.groupLength) {
      for (let i = before.length - this.groupLength; i > 0; i -= this.groupLength) {
        before = before.substr(0, i) + this.groupingChar + before.substr(i);
      }
    }

    // put together and return
    return prefix + before + after + suffix;
  }

  /**
   * Rounds a number according to the properties of the DecimalFormat.
   */
  round(number: number, applyMultiplier?: boolean): number {
    applyMultiplier = scout.nvl(applyMultiplier, true);
    if (number === null || number === undefined) {
      return null;
    }

    // apply multiplier
    if (applyMultiplier && this.multiplier !== 1) {
      number *= this.multiplier;
    }
    // round
    number = numbers.round(number, this.roundingMode, this.allAfter);
    // un-apply multiplier
    if (applyMultiplier && this.multiplier !== 1) {
      number /= this.multiplier;
    }
    return number;
  }

  /**
   * Convert to JS number format:
   * - remove groupingChar and lenientGroupingChars
   * - replace decimalSeparatorChar with '.'
   * - remove positiveSuffix and negativeSuffix
   * - replace positivePrefix with '+'
   * - replace negativePrefix with '-'
   */
  normalize(numberString: string): string {
    if (!numberString) {
      return numberString;
    }
    let result = numberString
      .replace(new RegExp('[' + this.groupingChar + this.lenientGroupingChars + ']', 'g'), '')
      .replace(new RegExp('[' + this.decimalSeparatorChar + ']', 'g'), '.');

    if (strings.hasText(this.positivePrefix)) {
      result = result.replace(new RegExp(this.positivePrefix, 'g'), '+');
    }
    if (strings.hasText(this.positiveSuffix)) {
      result = result.replace(new RegExp(this.positiveSuffix, 'g'), '');
    }
    if (strings.hasText(this.negativePrefix)) {
      result = result.replace(new RegExp(this.negativePrefix, 'g'), '-');
    }
    if (strings.hasText(this.negativeSuffix)) {
      result = result.replace(new RegExp(this.negativeSuffix, 'g'), '');
    }

    return result.replace(/\s/g, '');
  }

  /* --- STATIC HELPERS ------------------------------------------------------------- */

  /**
   * Literal (not localized!) pattern symbols as defined in http://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html
   */
  static PATTERN_SYMBOLS = {
    digit: '#',
    zeroDigit: '0',
    decimalSeparator: '.',
    groupingSeparator: ',',
    minusSign: '-',
    patternSeparator: ';'
  } as const;

  static ensure(locale: Locale, format: DecimalFormat | string | DecimalFormatOptions): DecimalFormat {
    if (!format) {
      return format as DecimalFormat;
    }
    if (format instanceof DecimalFormat) {
      return format;
    }
    return new DecimalFormat(locale, format);
  }
}

export interface DecimalFormatOptions {

  pattern: string;

  /**
   * default is 1
   */
  multiplier?: number;

  /**
   * default is {@link RoundingMode.HALF_UP}
   */
  roundingMode?: RoundingMode;
}
