# See Pagy::I18n API documentation https://ddnexus.github.io/pagy/docs/api/i18n
# frozen_string_literal: true

require 'yaml'

class Pagy
  # Pagy i18n implementation, compatible with the I18n gem, just a lot faster and lighter
  module I18n
    extend self

    # Pluralization rules
    module P11n
      # Pluralization variables
      from0to1   = (0..1).to_a.freeze
      from2to4   = (2..4).to_a.freeze
      from3to10  = (3..10).to_a.freeze
      from5to9   = (5..9).to_a.freeze
      from11to14 = (11..14).to_a.freeze
      from11to99 = (11..99).to_a.freeze
      from12to14 = (12..14).to_a.freeze

      from0to1_from5to9 = from0to1 + from5to9

      # Store the proc defining each pluralization RULE
      # Logic adapted from https://github.com/svenfuchs/rails-i18n
      RULE = {
        arabic:
          lambda do |n = 0|
            mod100 = n % 100
            case
            when n == 0                      then 'zero' # rubocop:disable Style/NumericPredicate
            when n == 1                      then 'one'
            when n == 2                      then 'two'
            when from3to10.include?(mod100)  then 'few'
            when from11to99.include?(mod100) then 'many'
            else                                  'other'
            end
          end,

        east_slavic:
          lambda do |n = 0|
            mod10  = n % 10
            mod100 = n % 100
            case
            when mod10 == 1 && mod100 != 11                                            then 'one'
            when from2to4.include?(mod10) && !from12to14.include?(mod100)              then 'few'
            when mod10 == 0 || from5to9.include?(mod10) || from11to14.include?(mod100) then 'many' # rubocop:disable Style/NumericPredicate
            else                                                                            'other'
            end
          end,

        one_other:
          ->(n) { n == 1 ? 'one' : 'other' }, # default RULE

        one_two_other:
          lambda do |n|
            case n
            when 1 then 'one'
            when 2 then 'two'
            else        'other'
            end
          end,

        one_upto_two_other:
          ->(n) { n && n >= 0 && n < 2 ? 'one' : 'other' },

        other:
          ->(*) { 'other' },

        polish:
          lambda do |n = 0|
            mod10  = n % 10
            mod100 = n % 100
            case
            when n == 1                                                           then 'one'
            when from2to4.include?(mod10) && !from12to14.include?(mod100)         then 'few'
            when from0to1_from5to9.include?(mod10) || from12to14.include?(mod100) then 'many'
            else                                                                       'other'
            end
          end,

        west_slavic:
          lambda do |n|
            case n
            when 1         then 'one'
            when *from2to4 then 'few'
            else                'other'
            end
          end

      }.freeze

      # Store the RULE to apply to each LOCALE
      # the :one_other RULE is the default for locales missing from this list
      LOCALE = Hash.new(RULE[:one_other]).tap do |hash|
                 hash['ar']    = RULE[:arabic]
                 hash['be']    = RULE[:east_slavic]
                 hash['bs']    = RULE[:east_slavic]
                 hash['ckb']   = RULE[:other]
                 hash['cs']    = RULE[:west_slavic]
                 hash['id']    = RULE[:other]
                 hash['fr']    = RULE[:one_upto_two_other]
                 hash['hr']    = RULE[:east_slavic]
                 hash['ja']    = RULE[:other]
                 hash['km']    = RULE[:other]
                 hash['ko']    = RULE[:other]
                 hash['pl']    = RULE[:polish]
                 hash['ru']    = RULE[:east_slavic]
                 hash['sk']    = RULE[:west_slavic]
                 hash['sr']    = RULE[:east_slavic]
                 hash['tr']    = RULE[:other]
                 hash['uk']    = RULE[:east_slavic]
                 hash['vi']    = RULE[:other]
                 hash['zh-CN'] = RULE[:other]
                 hash['zh-HK'] = RULE[:other]
                 hash['zh-TW'] = RULE[:other]
                 hash['dz']    = RULE[:other]
               end.freeze
    end

    # Stores the i18n DATA structure for each loaded locale
    # default on the first locale DATA
    DATA = Hash.new { |hash,| hash.first[1] }

    private

    # Create a flat hash with dotted notation keys
    def flatten(initial, prefix = '')
      initial.each.reduce({}) do |hash, (key, value)|
        hash.merge!(value.is_a?(Hash) ? flatten(value, "#{prefix}#{key}.") : { "#{prefix}#{key}" => value })
      end
    end

    # Build the DATA hash out of the passed locales
    def build(*locales)
      locales.each do |locale|
        locale[:filepath]  ||= Pagy.root.join('locales', "#{locale[:locale]}.yml")
        locale[:pluralize] ||= P11n::LOCALE[locale[:locale]]
        dictionary = YAML.safe_load(File.read(locale[:filepath], encoding: 'UTF-8'))
        raise I18nError, %(expected :locale "#{locale[:locale]}" not found in :filepath "#{locale[:filepath].inspect}") \
              unless dictionary.key?(locale[:locale])

        DATA[locale[:locale]] = [flatten(dictionary[locale[:locale]]), locale[:pluralize]]
      end
    end
    # Build the default at require time
    build(locale: 'en')

    public

    # Public method to configure the locales: overrides the default, build the DATA and freezes it
    def load(*locales)
      DATA.clear
      build(*locales)
      DATA.freeze
    end

    # Translate and pluralize the key with the locale DATA
    def translate(locale, key, **opts)
      data, pluralize = DATA[locale]
      translation = data[key] || (opts[:count] && data[key += ".#{pluralize.call(opts[:count])}"]) \
                      or return %([translation missing: "#{key}"])
      translation.gsub(/%{[^}]+?}/) { |match| opts[:"#{match[2..-2]}"] || match }
    end
    alias t translate
  end
end
