defmodule Cldr.Number.PluralRule do
  @moduledoc """
  Defines the plural rule implementation
  modules. The functions in this module
  generate code to implement the plural
  rules of CLDR.

  """

  @type operand :: any()

  @typedoc """
  Defines the plural types into which a number can be categorised.

  """
  @type plural_type :: :zero | :one | :two | :few | :many | :other

  @typedoc """
  Defines the structure of a plural rule before compilation.

  """
  @type plural_rule :: Keyword.t()

  @doc """
  Returns a list of the possible pluralization
  types

  """
  @spec known_plural_types :: [plural_type(), ...]
  @plural_types [:zero, :one, :two, :few, :many, :other]
  def known_plural_types do
    @plural_types
  end

  @doc """
  Returns the plural type for a given number.

  ## Arguments

  * `number` is an integer, float or Decimal number.

  * `backend` is any module that includes `use Cldr` and therefore
    is a `Cldr` backend module.  The default is `Cldr.default_backend!/0`.

  * `options` is a keyword list of options.

  ## Options

  * `locale` is any valid locale name returned by `Cldr.known_locale_names/1`
    or a `Cldr.LanguageTag` struct returned by `Cldr.Locale.new!/2`, The
    default is `Cldr.get_locale/0`.

  * `backend` is any module that includes `use Cldr` and therefore
     is a `Cldr` backend module.  The default is `Cldr.default_backend!/0`.
     This option allows the backend to be specified as an argument or an option.

  * `type` is either `Cardinal` or `Ordinal`. The default is `Cardinal`.

  ## Examples

    	iex> Cldr.Number.PluralRule.plural_type(123)
    	:other

    	iex> Cldr.Number.PluralRule.plural_type(123, type: Ordinal)
    	:few

    	iex> Cldr.Number.PluralRule.plural_type(123, type: Cardinal)
    	:other

    	iex> Cldr.Number.PluralRule.plural_type(2, locale: "de")
    	:other

  """
  def plural_type(number, backend \\ nil, options \\ [])

  def plural_type(number, options, []) when is_list(options) do
    {_locale, backend} = Cldr.locale_and_backend_from(options)
    plural_type(number, backend, options)
  end

  def plural_type(number, nil, options) do
    {_locale, backend} = Cldr.locale_and_backend_from(options)
    plural_type(number, backend, options)
  end

  def plural_type(number, backend, options) do
    locale = Keyword.get_lazy(options, :locale, &Cldr.get_locale/0)
    type = Keyword.get(options, :type, Cardinal)
    module = Module.concat([backend, Number, type])
    module.plural_rule(number, locale)
  end

  defmacro __using__(opts) do
    module_name = Keyword.get(opts, :type)

    unless module_name in [:cardinal, :ordinal] do
      raise ArgumentError,
            "Invalid option #{inspect(opts)}. `type: :cardinal` or " <>
              "`type: :ordinal` are the only valid options"
    end

    quote location: :keep, generated: true do
      alias Cldr.Math
      alias Cldr.LanguageTag
      alias Cldr.Locale

      import Cldr.Digits,
        only: [number_of_integer_digits: 1, remove_trailing_zeros: 1, fraction_as_integer: 2]

      import Cldr.Math, only: [mod: 2, within: 2]
      import Cldr.Number.PluralRule.Compiler
      import Cldr.Number.PluralRule.Transformer

      @module Atom.to_string(unquote(module_name)) |> String.capitalize()

      @rules Cldr.Config.cldr_data_dir()
             |> Path.join("/plural_rules.json")
             |> File.read!()
             |> Cldr.Config.json_library().decode!
             |> Map.get(Atom.to_string(unquote(module_name)))
             |> Cldr.Config.normalize_plural_rules()
             |> Map.new()

      @rules_locales @rules
                     |> Map.keys()
                     |> Enum.sort()

      @config Keyword.get(unquote(opts), :config)
      @backend Map.get(@config, :backend)

      @known_locale_names @rules_locales
                          |> MapSet.new()
                          |> MapSet.intersection(
                            MapSet.new(Cldr.Locale.Loader.known_locale_names(@config))
                          )
                          |> MapSet.to_list()
                          |> Enum.sort()

      @doc """
      The locale names for which plural rules are defined.

      """
      def available_locale_names do
        @rules_locales
      end

      @doc """
      The configured locales for which plural rules are defined.

      Returns the intersection of `#{inspect(@backend)}.known_locale_names/0` and
      the locales for which #{@module} plural rules are defined.

      There are many `Cldr` locales which don't have their own plural
      rules so this list is the intersection of `Cldr`'s configured
      locales and those that have rules.

      """
      @spec known_locale_names :: [Locale.locale_name(), ...]
      def known_locale_names do
        @known_locale_names
      end

      @doc """
      Returns all the plural rules defined in CLDR.

      """
      @spec plural_rules :: %{
              Cldr.Locale.locale_name() => [
                {
                  plural_type :: Cldr.Number.PluralRule.plural_type(),
                  plural_rules :: [Cldr.Number.PluralRule.plural_rule(), ...]
                },
                ...
              ]
            }

      def plural_rules do
        @rules
      end

      if unquote(module_name) == :cardinal do
        @doc """
        Pluralize a number using #{unquote(module_name)} plural rules
        and a substitution map.

        ## Arguments

        * `number` is an integer, float or Decimal

        * `locale` is any locale returned by `#{inspect(@backend)}.Locale.new!/1` or any
          `locale_name` returned by `#{inspect(@backend)}.known_locale_names/0`

        * `substitutions` is a map that maps plural keys to a string.
          The valid substitution keys are `:zero`, `:one`, `:two`,
          `:few`, `:many` and `:other`.

        See also `#{inspect(__MODULE__)}.#{@module}.plural_rule/3`.

        ## Examples

            iex> #{inspect(__MODULE__)}.pluralize 1, "en", %{one: "one"}
            "one"

            iex> #{inspect(__MODULE__)}.pluralize 2, "en", %{one: "one"}
            nil

            iex> #{inspect(__MODULE__)}.pluralize 2, "en", %{one: "one", two: "two", other: "other"}
            "other"

            iex> #{inspect(__MODULE__)}.pluralize 22, "en", %{one: "one", two: "two", other: "other"}
            "other"

            iex> #{inspect(__MODULE__)}.pluralize Decimal.new(1), "en", %{one: "one"}
            "one"

            iex> #{inspect(__MODULE__)}.pluralize Decimal.new(2), "en", %{one: "one"}
            nil

            iex> #{inspect(__MODULE__)}.pluralize Decimal.new(2), "en", %{one: "one", two: "two"}
            nil

            iex> #{inspect(__MODULE__)}.pluralize 1..10, "ar", %{one: "one", few: "few", other: "other"}
            "few"

            iex> #{inspect(__MODULE__)}.pluralize 1..10, "en", %{one: "one", few: "few", other: "other"}
            "other"

        """
      else
        @doc """
        Pluralize a number using #{unquote(module_name)} plural rules
        and a substitution map.

        ## Arguments

        * `number` is an integer, float or Decimal or a `Range.t{}`.  When a range, The
          is that in any usage, the start value is strictly less than the end value,
          and that no values are negative. Results for any cases that do not meet
          these criteria are undefined.

        * `locale` is any locale returned by `#{inspect(@backend)}.Locale.new!/1` or any
          `locale_name` returned by `#{inspect(@backend)}.known_locale_names/0`

        * `substitutions` is a map that maps plural keys to a string.
          The valid substitution keys are `:zero`, `:one`, `:two`,
          `:few`, `:many` and `:other`.

        See also `#{inspect(__MODULE__)}.#{@module}.plural_rule/3`.

        ## Examples

            iex> #{inspect(__MODULE__)}.pluralize 1, :en, %{one: "one"}
            "one"

            iex> #{inspect(__MODULE__)}.pluralize 2, :en, %{one: "one"}
            nil

            iex> #{inspect(__MODULE__)}.pluralize 2, :en, %{one: "one", two: "two"}
            "two"

            iex> #{inspect(__MODULE__)}.pluralize 22, :en, %{one: "one", two: "two", other: "other"}
            "two"

            iex> #{inspect(__MODULE__)}.pluralize Decimal.new(1), :en, %{one: "one"}
            "one"

            iex> #{inspect(__MODULE__)}.pluralize Decimal.new(2), :en, %{one: "one"}
            nil

            iex> #{inspect(__MODULE__)}.pluralize Decimal.new(2), :en, %{one: "one", two: "two"}
            "two"

            iex> #{inspect(__MODULE__)}.pluralize 1..10, "ar", %{one: "one", few: "few", other: "other"}
            "other"

            iex> #{inspect(__MODULE__)}.pluralize 1..10, "en", %{one: "one", few: "few", other: "other"}
            "other"

        """
      end

      @default_substitution :other
      @spec pluralize(Math.number_or_decimal() | Range.t(), Locale.locale_reference(), %{}) ::
              any()

      def pluralize(%Range{first: first, last: last}, locale_name, substitutions) do
        with {:ok, language_tag} <- @backend.validate_locale(locale_name) do
          first_rule = plural_rule(first, language_tag)
          last_rule = plural_rule(last, language_tag)

          combined_rule =
            @backend.Number.PluralRule.Range.plural_rule(first_rule, last_rule, language_tag)

          substitutions[combined_rule] || substitutions[@default_substitution]
        end
      end

      def pluralize(number, locale_name, substitutions)
          when is_atom(locale_name) or is_binary(locale_name) do
        with {:ok, language_tag} <- @backend.validate_locale(locale_name) do
          pluralize(number, language_tag, substitutions)
        end
      end

      def pluralize(number, %LanguageTag{} = locale, %{} = substitutions)
          when is_number(number) do
        do_pluralize(number, locale, substitutions)
      end

      def pluralize(%Decimal{sign: sign, coef: coef, exp: 0} = number, locale, substitutions)
          when is_integer(coef) do
        number
        |> Decimal.to_integer()
        |> do_pluralize(locale, substitutions)
      end

      def pluralize(%Decimal{sign: sign, coef: coef, exp: exp}, locale, substitutions)
          when is_integer(coef) and exp > 0 do
        number = %Decimal{sign: sign, coef: coef * 10, exp: exp - 1}
        pluralize(number, locale, substitutions)
      end

      def pluralize(%Decimal{sign: sign, coef: coef, exp: exp}, locale, substitutions)
          when is_integer(coef) and exp < 0 and rem(coef, 10) == 0 do
        number = %Decimal{sign: sign, coef: Kernel.div(coef, 10), exp: exp + 1}
        pluralize(number, locale, substitutions)
      end

      def pluralize(%Decimal{} = number, %LanguageTag{} = locale, %{} = substitutions) do
        number
        |> Decimal.to_float()
        |> do_pluralize(locale, substitutions)
      end

      defp do_pluralize(number, %LanguageTag{} = locale, %{} = substitutions) do
        plural = plural_rule(number, locale)
        number = if (truncated = trunc(number)) == number, do: truncated, else: number
        substitutions[number] || substitutions[plural] || substitutions[@default_substitution]
      end

      @doc """
      Return the plural rules for a locale.

      ## Arguments

      * `locale` is any locale returned by `#{inspect(@backend)}.Locale.new!/1` or any
        `locale_name` returned by `#{inspect(@backend)}.known_locale_names/0`

      The rules are returned in AST form after parsing.

      """
      @spec plural_rules_for(Locale.locale_name() | LanguageTag.t()) :: [{atom(), list()}, ...]

      def plural_rules_for(%LanguageTag{cldr_locale_name: cldr_locale_name, language: language}) do
        plural_rules()[cldr_locale_name] || plural_rules()[language]
      end

      def plural_rules_for(locale_name) do
        with {:ok, locale} <- @backend.validate_locale(locale_name) do
          plural_rules_for(locale)
        end
      end

      # Plural Operand Meanings as defined in CLDR plural rules and used
      # in the generated code
      #
      # Symbol  Value
      # n       absolute value of the source number (integer and decimals).
      # i       integer digits of n.
      # v       number of visible fraction digits in n, with trailing zeros.
      # w       number of visible fraction digits in n, without trailing zeros.
      # f       visible fractional digits in n, with trailing zeros.
      # t       visible fractional digits in n, without trailing zeros.

      if unquote(module_name) == :cardinal do
        @doc """
        Return the plural key for a given number in a given locale

        Returns which plural key (`:zero`, `:one`, `:two`, `:few`,
        `:many` or `:other`) a given number fits into within the
        context of a given locale.

        Note that these key names should not be interpreted
        literally.  For example, the key returned from
        `Cldr.Number.Ordinal.plural_rule(0, "en")` is actually
        `:other`, not `:zero`.

        This key can then be used to format a number, date, time, unit,
        list or other content in a plural-sensitive way.

        ## Arguments

        * `number` is any `integer`, `float` or `Decimal`

        * `locale` is any locale returned by `Cldr.Locale.new!/2` or any
          `locale_name` returned by `#{inspect(@backend)}.known_locale_names/0`

        * `rounding` is one of `#{inspect(Cldr.Math.rounding_modes())}`.  The
          default is `#{inspect(Cldr.Math.default_rounding_mode())}`.

        ## Examples

            iex> #{inspect(__MODULE__)}.plural_rule 0, "fr"
            :one

            iex> #{inspect(__MODULE__)}.plural_rule 0, "en"
            :other

        """
      else
        @doc """
        Return the plural key for a given number in a given locale

        Returns which plural key (`:zero`, `:one`, `:two`, `:few`,
        `:many` or `:other`) a given number fits into within the
        context of a given locale.

        Note that these key names should not be interpreted
        literally.  For example, the key returned from
        `Cldr.Number.Ordinal.plural_rule(0, "en")` is actually
        `:other`, not `:zero`.

        This key can then be used to format a number, date, time, unit,
        list or other content in a plural-sensitive way.

        ## Arguments

        * `number` is any `integer`, `float` or `Decimal`

        * `locale` is any locale returned by `Cldr.Locale.new!/2` or any
          `locale_name` returned by `#{inspect(@backend)}.known_locale_names/0`

        * `rounding` is one of `#{inspect(Cldr.Math.rounding_modes())}`.  The
          default is `#{inspect(Cldr.Math.default_rounding_mode())}`.

        ## Examples

            iex> #{inspect(__MODULE__)}.plural_rule 0, "fr"
            :other

            iex> #{inspect(__MODULE__)}.plural_rule 1, "en"
            :one

        """
      end

      @spec plural_rule(
              Math.number_or_decimal(),
              Locale.locale_name() | LanguageTag.t(),
              atom() | pos_integer()
            ) :: Cldr.Number.PluralRule.plural_type() | {:error, {module, String.t()}}

      def plural_rule(number, locale, rounding \\ Math.default_rounding())

      def plural_rule(number, %LanguageTag{} = locale, rounding) when is_binary(number) do
        plural_rule(Decimal.new(number), locale, rounding)
      end

      # Plural rule for an integer
      def plural_rule(number, %LanguageTag{} = locale, _rounding) when is_integer(number) do
        n = abs(number)
        i = n
        v = 0
        w = 0
        f = 0
        t = 0
        e = 0
        do_plural_rule(locale, n, i, v, w, f, t, e)
      end

      # For a compact integer
      def plural_rule({number, e}, %LanguageTag{} = locale, _rounding) when is_integer(number) do
        n = abs(number)
        i = n
        v = 0
        w = 0
        f = 0
        t = 0
        do_plural_rule(locale, n, i, v, w, f, t, e)
      end

      # Plural rule for a float
      def plural_rule(number, %LanguageTag{} = locale, rounding)
          when is_float(number) and is_integer(rounding) and rounding > 0 do
        # Testing shows that this is working but just in case we
        # can go back to casting the number to a decimal and
        # using that path
        # plural_rule(Decimal.new(number), locale, rounding)
        n = Float.round(abs(number), rounding)
        i = trunc(n)
        v = rounding
        t = fraction_as_integer(n - i, rounding)
        w = number_of_integer_digits(t)
        f = trunc(t * Math.power_of_10(v - w))
        e = 0
        do_plural_rule(locale, n, i, v, w, f, t, e)
      end

      # Plural rule for a compact float
      def plural_rule({number, e}, %LanguageTag{} = locale, rounding)
          when is_float(number) and is_integer(rounding) and rounding > 0 do
        # Testing shows that this is working but just in case we
        # can go back to casting the number to a decimal and
        # using that path
        # plural_rule(Decimal.new(number), locale, rounding)
        n = Float.round(abs(number), rounding)
        i = trunc(n)
        v = rounding
        t = fraction_as_integer(n - i, rounding)
        w = number_of_integer_digits(t)
        f = trunc(t * Math.power_of_10(v - w))
        do_plural_rule(locale, n, i, v, w, f, t, e)
      end

      # Plural rule for a %Decimal{}
      def plural_rule(%Decimal{} = number, %LanguageTag{} = locale, rounding)
          when is_integer(rounding) and rounding > 0 do
        # n absolute value of the source number (integer and decimals).
        n = Decimal.abs(number)

        # i integer digits of n.
        i = Decimal.round(n, 0, :floor)

        # v number of visible fraction digits in n, with trailing zeros.
        v = abs(n.exp)

        # f visible fractional digits in n, with trailing zeros.
        f =
          n
          |> Decimal.sub(i)
          |> Decimal.mult(Decimal.new(Math.power_of_10(v)))
          |> Decimal.round(0, :floor)
          |> Decimal.to_integer()

        #   t visible fractional digits in n, without trailing zeros.
        t = remove_trailing_zeros(f)

        # w number of visible fraction digits in n, without trailing zeros.
        w = number_of_integer_digits(t)

        i = Decimal.to_integer(i)
        n = Math.to_float(n)
        e = 0

        do_plural_rule(locale, n, i, v, w, f, t, e)
      end

      # Plural rule for a compact %Decimal{}
      def plural_rule({%Decimal{} = number, e}, %LanguageTag{} = locale, rounding)
          when is_integer(rounding) and rounding > 0 do
        # n absolute value of the source number (integer and decimals).
        n = Decimal.abs(number)

        # i integer digits of n.
        i = Decimal.round(n, 0, :floor)

        # v number of visible fraction digits in n, with trailing zeros.
        v = abs(n.exp)

        # f visible fractional digits in n, with trailing zeros.
        f =
          n
          |> Decimal.sub(i)
          |> Decimal.mult(Decimal.new(Math.power_of_10(v)))
          |> Decimal.round(0, :floor)
          |> Decimal.to_integer()

        #   t visible fractional digits in n, without trailing zeros.
        t = remove_trailing_zeros(f)

        # w number of visible fraction digits in n, without trailing zeros.
        w = number_of_integer_digits(t)

        i = Decimal.to_integer(i)
        n = Math.to_float(n)

        do_plural_rule(locale, n, i, v, w, f, t, e)
      end

      def plural_rule(number, locale_name, rounding) do
        with {:ok, locale} <- @backend.validate_locale(locale_name) do
          plural_rule(number, locale, rounding)
        end
      end
    end
  end

  @doc false
  def define_ordinal_and_cardinal_modules(config) do
    quote location: :keep do
      defmodule Number.Ordinal do
        @moduledoc false
        if Cldr.Config.include_module_docs?(unquote(config.generate_docs)) do
          @moduledoc """
          Implements ordinal plural rules for numbers.
          """
        end

        use Cldr.Number.PluralRule, type: :ordinal, config: unquote(Macro.escape(config))
        alias Cldr.LanguageTag

        unquote(Cldr.Number.PluralRule.define_plural_rules())
      end

      defmodule Number.Cardinal do
        @moduledoc false
        if Cldr.Config.include_module_docs?(unquote(config.generate_docs)) do
          @moduledoc """
          Implements cardinal plural rules for numbers.
          """
        end

        use Cldr.Number.PluralRule, type: :cardinal, config: unquote(Macro.escape(config))
        alias Cldr.LanguageTag

        unquote(Cldr.Number.PluralRule.define_plural_rules())
      end
    end
  end

  @doc false
  def define_plural_ranges(config) do
    quote location: :keep, bind_quoted: [config: Macro.escape(config)] do
      defmodule Number.PluralRule.Range do
        @moduledoc false
        if Cldr.Config.include_module_docs?(config.generate_docs) do
          @moduledoc """
          Implements plural rules for ranges

          """
        end

        alias Cldr.Number.PluralRule

        @doc """
        Returns a final plural type for a start-of-range plural
        type, an end-of-range plural type and a locale.

        ## Arguments

        * `first` is a plural type for the start of a range

        * `last` is a plural type for the end of a range

        * `locale` is any `Cldr.LanguageTag.t` or a language name
          (not locale name)

        ## Example

            iex> #{inspect(__MODULE__)}.plural_rule :other, :few, "ar"
            :few

        """
        @spec plural_rule(
                first :: Cldr.Number.PluralRule.plural_type(),
                last :: Cldr.Number.PluralRule.plural_type(),
                locale :: Cldr.Locale.locale_name() | Cldr.LanguageTag.t()
              ) :: Cldr.Number.PluralRule.plural_type() | {:error, {module, String.t()}}

        def plural_rule(first, last, %Cldr.LanguageTag{language: language}) do
          plural_rule(first, last, language)
        end

        for %{locales: locales, ranges: ranges} <- Cldr.Config.plural_ranges(),
            range <- ranges do
          def plural_rule(unquote(range.start), unquote(range.end), locale)
              when locale in unquote(locales) do
            unquote(range.result)
          end
        end

        def plural_rule(_start, _end, _locale) do
          :other
        end
      end
    end
  end

  @doc false
  def define_plural_rules do
    quote bind_quoted: [], location: :keep do
      alias Cldr.Number.PluralRule

      # Silence warnings since the success typing of do_plural_rule will depend
      # on the locale used.
      @dialyzer {:nowarn_function, [do_plural_rule: 8]}

      # Generate the functions to process plural rules
      @spec do_plural_rule(
              LanguageTag.t(),
              number(),
              PluralRule.operand(),
              PluralRule.operand(),
              PluralRule.operand(),
              PluralRule.operand(),
              [integer(), ...] | integer(),
              number()
            ) :: :zero | :one | :two | :few | :many | :other

      # Function body is the AST of the function which needs to be injected
      # into the function definition.
      for locale_name <- @known_locale_names do
        function_body =
          @rules
          |> Map.get(locale_name)
          |> rules_to_condition_statement(__MODULE__)

        defp do_plural_rule(
               %LanguageTag{cldr_locale_name: unquote(locale_name)},
               n,
               i,
               v,
               w,
               f,
               t,
               e
             ) do
          # silence unused variable warnings
          _ = {n, i, v, w, f, t, e}
          unquote(function_body)
        end
      end

      # If we get here then it means that the locale doesn't have a plural rule,
      # but the language might
      defp do_plural_rule(%LanguageTag{} = language_tag, n, i, v, w, f, t, e) do
        if language_tag.language == language_tag.cldr_locale_name do
          {
            :error,
            {
              Cldr.UnknownPluralRules,
              "No #{@module} plural rules available for #{inspect(language_tag)}"
            }
          }
        else
          language_tag
          |> Map.put(:cldr_locale_name, String.to_atom(language_tag.language))
          |> do_plural_rule(n, i, v, w, f, t, e)
        end
      end
    end
  end
end
