# frozen_string_literal: true

# Parse an amount from a string
class MoneyParser
  class MoneyFormatError < ArgumentError; end

  MARKS = %w[. , · ’ ˙ '] + [' ']

  ESCAPED_MARKS = Regexp.escape(MARKS.join)
  ESCAPED_NON_SPACE_MARKS = Regexp.escape((MARKS - [' ']).join)
  ESCAPED_NON_DOT_MARKS = Regexp.escape((MARKS - ['.']).join)
  ESCAPED_NON_COMMA_MARKS = Regexp.escape((MARKS - [',']).join)

  NUMERIC_REGEX = /(
    [\+\-]?
    [\d#{ESCAPED_NON_SPACE_MARKS}][\d#{ESCAPED_MARKS}]*
  )/ix

  # 1,234,567.89
  DOT_DECIMAL_REGEX = /\A
    [\+\-]?
    (?:
      (?:\d+)
      (?:[#{ESCAPED_NON_DOT_MARKS}]\d{3})+
      (?:\.\d{2,})?
    )
  \z/ix

  # 1.234.567,89
  COMMA_DECIMAL_REGEX = /\A
    [\+\-]?
    (?:
      (?:\d+)
      (?:[#{ESCAPED_NON_COMMA_MARKS}]\d{3})+
      (?:\,\d{2,})?
    )
  \z/ix

  # 12,34,567.89
  INDIAN_NUMERIC_REGEX = /\A
    [\+\-]?
    (?:
      (?:\d+)
      (?:\,\d{2})+
      (?:\,\d{3})
      (?:\.\d{2})?
    )
  \z/ix

  # 1,1123,4567.89
  CHINESE_NUMERIC_REGEX = /\A
    [\+\-]?
    (?:
      (?:\d+)
      (?:\,\d{4})+
      (?:\.\d{2})?
    )
  \z/ix

  def self.parse(input, currency = nil, **options)
    new.parse(input, currency, **options)
  end

  def parse(input, currency = nil, strict: false)
    currency = Money::Helpers.value_to_currency(currency)
    amount = extract_amount_from_string(input, currency, strict)
    Money.new(amount, currency)
  end

  private

  def extract_amount_from_string(input, currency, strict)
    unless input.is_a?(String)
      return input
    end

    if input.strip.empty?
      return '0'
    end

    number = input.scan(NUMERIC_REGEX).flatten.first
    number = number.to_s.strip

    if number.empty?
      if Money.config.legacy_deprecations && !strict
        Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
        return '0'
      else
        raise MoneyFormatError, "invalid money string: #{input}"
      end
    end

    marks = number.scan(/[#{ESCAPED_MARKS}]/).flatten
    if marks.empty?
      return number
    end

    if marks.size == 1
      return normalize_number(number, marks, currency)
    end

    # remove end of string mark
    number.sub!(/[#{ESCAPED_MARKS}]\z/, '')

    if amount = number[DOT_DECIMAL_REGEX] || number[INDIAN_NUMERIC_REGEX] || number[CHINESE_NUMERIC_REGEX]
      return amount.tr(ESCAPED_NON_DOT_MARKS, '')
    end

    if amount = number[COMMA_DECIMAL_REGEX]
      return amount.tr(ESCAPED_NON_COMMA_MARKS, '').sub(',', '.')
    end

    if Money.config.legacy_deprecations && !strict
      Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
    else
      raise MoneyFormatError, "invalid money string: #{input}"
    end

    normalize_number(number, marks, currency)
  end

  def normalize_number(number, marks, currency)
    digits = number.rpartition(marks.last)
    digits.first.tr!(ESCAPED_MARKS, '')

    if last_digits_decimals?(digits, marks, currency)
      "#{digits.first}.#{digits.last}"
    else
      "#{digits.first}#{digits.last}"
    end
  end

  def last_digits_decimals?(digits, marks, currency)
    # Thousands marks are always different from decimal marks
    # Example: 1,234,456
    *other_marks, last_mark = marks
    other_marks.uniq!
    if other_marks.size == 1
      return other_marks.first != last_mark
    end

    # Thousands always have more than 2 digits
    # Example: 1,23 must be 1 dollar and 23 cents
    if digits.last.size < 3
      return !digits.last.empty?
    end

    # 0 before the final mark indicates last digits are decimals
    # Example: 0,23
    if digits.first.to_i.zero?
      return true
    end

    # legacy support for 1.000 USD
    if digits.last.size == 3 && digits.first.size <= 3 && currency.minor_units < 3
      return false
    end

    # The last mark matches the one used by the provided currency to delimiter decimals
    currency.decimal_mark == last_mark
  end
end