/**
* @file
* Declares utility functions for setting the locale.
*
* Boost.Locale is not available on all platforms. This header is implemented
* so that it can switch between using boost::locale::format and boost::format
* (without localization) by defining LEATHERMAN_I18N. Because these classes
* do not use the same substitution characters, but gettext replacement relies
* on matching a string, specify that both "%N%" (Boost.Format) and "{N}"
* (Boost.Locale) should be considered substitution characters when using
* leatherman::locale::format, and "{N}" should be preferred. When i18n is
* disabled, it will regex replace "{(\d+)}" to "%\1%" for use with Boost.Format.
*/
#pragma once
#include <locale>
#include <vector>
#include <functional>

#include <boost/format.hpp>
#include <boost/regex.hpp>

#ifdef LEATHERMAN_I18N
#include <boost/locale/format.hpp>
#else
// Unset PROJECT_NAME so we only create a single locale.
#undef PROJECT_NAME
#define PROJECT_NAME ""
#undef PROJECT_DIR
#define PROJECT_DIR
#endif

namespace leatherman { namespace locale {

    /**
     * Gets a locale object for the specified locale id.
     * @param id The locale ID, defaults to a UTF-8 compatible system default.
     * @param domain The catalog domain to use for i18n via gettext.
     * @param paths Search paths for localization files.
     * @return The locale. If a locale for the specified domain already exists, it returns
     * the same locale until clear_domain is called for that domain.
     * Throws boost::locale::conv::conversion_error if the system locale is invalid or
     * the catalog for the specified language can't be used with the system locale encoding.
     *
     * Unsafe to use with GCC on AIX or Solaris, as std::locale is busted.
     */
    const std::locale get_locale(std::string const& id = "",
                                 std::string const& domain = PROJECT_NAME,
                                 std::vector<std::string> const& paths = {PROJECT_DIR});

    /**
     * Clears the locale for a specific domain.
     * WARNING: This may invalidate existing references, so only use for testing.
     * @param domain The catalog domain to clear.
     */
    void clear_domain(std::string const& domain = PROJECT_NAME);

    /**
     * Translate text using the locale initialized by this library.
     * If localization encounters an error, the original message will be returned.
     * @param msg The string to translate.
     * @param domain The catalog domain to use for i18n via gettext.
     * @return The translated string.
     */
    std::string translate(std::string const& msg, std::string const& domain = PROJECT_NAME);

    /**
     * Translate text in a given context using the locale initialized by this library.
     * Context can be used to disambiguate the same word used multiple different ways.
     * If localization encounters an error, the original message will be returned.
     * @param context The context string.
     * @param msg The string to translate.
     * @param domain The catalog domain to use for i18n via gettext.
     * @return The translated string.
     */
    std::string translate_p(std::string const& context, std::string const& msg, std::string const& domain = PROJECT_NAME);

    /**
     * Translate plural text using the locale initialized by this library.
     * If localization encounters an error, the `single` message will be returned for n == 1,
     * and the `plural` message will be returned for all other values of n.
     * @param single The singuar form to translate.
     * @param plural The plural form to translate.
     * @param n Number of items, used to choose singular or plural.
     * @param domain The catalog domain to use for i18n via gettext.
     * @return The translated string.
     */
    std::string translate_n(std::string const& single, std::string const& plural, int n, std::string const& domain = PROJECT_NAME);

    /**
     * Translate plural text in a given context using the locale initialized by this library.
     * Context can be used to disambiguate the same word used multiple different ways.
     * If localization encounters an error, the `single` message will be returned for n == 1,
     * and the `plural` message will be returned for all other values of n.
     * @param context The context string.
     * @param single The singuar form to translate.
     * @param plural The plural form to translate.
     * @param n Number of items, used to choose singular or plural.
     * @param domain The catalog domain to use for i18n via gettext.
     * @return The translated string.
     */
    std::string translate_np(std::string const& context, std::string const& single, std::string const& plural, int n, std::string const& domain = PROJECT_NAME);

    namespace {
        /*
         * Anonymous namespace, limiting access to current namespace
         */

        /**
         * Translates and formats text using boost::regex and boost::format.
         * @param trans The translation function.
         * @param domain Domain name.
         * @param args Format arguments.
         * @return The string generated by translating the format string, then applying the arguments.
         */
        template <typename... TArgs>
        std::string format_disabled_locales(std::function<std::string(const std::string&)>&& trans, std::string domain, TArgs... args) {
            // When locales are disabled, use boost::format, which expects %N% style formatting
            static const boost::regex match{"\\{(\\d+)\\}"};
            static const std::string repl{"%\\1%"};
            boost::format form{boost::regex_replace(trans(domain), match, repl)};
            (void) std::initializer_list<int>{ ((void)(form % args), 0)... };
            return form.str();
        }

        /**
         * Translates and formats text using the locale initialized by this library.
         * @param trans The translation function.
         * @param args Format arguments.
         * @return The string generated by translating the format string, then applying the arguments.
         */
        template <typename... TArgs>
        std::string format_common(std::function<std::string(const std::string&)>&& trans, TArgs... args)
        {
            // Create and apply formatter here, as we want to guarantee the lifetime of the arguments.
            // boost::locale::format doesn't make copies, and a common gotcha is using temporary arguments
            // to build up the formatter.
            // Technique for the one-liner explained at http://florianjw.de/en/variadic_templates.html.
            static const std::string domain{PROJECT_NAME};
#ifdef LEATHERMAN_I18N
            boost::locale::format form{trans(domain)};
            (void) std::initializer_list<int>{ ((void)(form % args), 0)... };
            try {
                return form.str(get_locale("", domain));
            } catch (const std::exception &e) {
                return format_disabled_locales(std::move(trans), domain, std::forward<TArgs>(args)...);
            }
#else
            return format_disabled_locales(std::move(trans), domain, std::forward<TArgs>(args)...);
#endif
        }
    }

    /**
     * Translates and formats text using the locale initialized by this library.
     * Use the default domain, i.e. PROJECT_NAME.
     * Replaces the use of boost::format with a variadic function call.
     * Specialized to the PROJECT_NAME domain.
     * @param fmt The format string.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template <typename... TArgs>
    std::string format(std::string const& fmt, TArgs... args)
    {
        auto trans = [&fmt](const std::string& domain) {return translate(fmt, domain);};
        return format_common(std::move(trans), std::forward<TArgs>(args)...);
    }

    /**
     * Translates and formats text using the locale initialized by this library
     * Alias for format(...); Convenience function for adding i18n support.
     * @param fmt The format string.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template<typename... TArgs>
    inline std::string _(std::string const& fmt, TArgs&&... args)
    {
        return format(std::forward<decltype(fmt)>(fmt), std::forward<TArgs>(args)...);
    }

    /**
     * Translates and formats text in a given context using the locale initialized by this library.
     * Use the default domain, i.e. PROJECT_NAME.
     * Replaces the use of boost::format with a variadic function call.
     * Specialized to the PROJECT_NAME domain.
     * @param context The context string.
     * @param fmt The format string.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template <typename... TArgs>
    std::string format_p(std::string const& context, std::string const& fmt, TArgs... args)
    {
        auto trans = [&context, &fmt](const std::string& domain) {return translate_p(context, fmt, domain);};
        return format_common(std::move(trans), std::forward<TArgs>(args)...);
    }

    /**
     * Translates and formats text using the locale initialized by this library
     * Alias for format_p(...); Convenience function for adding i18n support.
     * @param context The context string.
     * @param fmt The format string.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template<typename... TArgs>
    inline std::string p_(std::string const& context, std::string const& fmt, TArgs&&... args)
    {
        return format_p(std::forward<decltype(context)>(context), std::forward<decltype(fmt)>(fmt), std::forward<TArgs>(args)...);
    }

    /**
     * Translates and formats plural text using the locale initialized by this library.
     * Use the default domain, i.e. PROJECT_NAME.
     * Replaces the use of boost::format with a variadic function call.
     * Specialized to the PROJECT_NAME domain.
     * @param single The singular format string.
     * @param plural The plural format string.
     * @param n Number of items, used to choose singular or plural.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template <typename... TArgs>
    std::string format_n(std::string const& single, std::string const& plural, int n, TArgs... args)
    {
        auto trans = [&single, &plural, n](const std::string& domain) {return translate_n(single, plural, n, domain);};
        return format_common(std::move(trans), std::forward<TArgs>(args)...);
    }

    /**
     * Translates and formats plural text using the locale initialized by this library.
     * Alias for format_n(...); Convenience function for adding i18n support.
     * @param single The singular format string.
     * @param plural The plural format string.
     * @param n Number of items, used to choose singular or plural.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template<typename... TArgs>
    inline std::string n_(std::string const& single, std::string const& plural, int n, TArgs&&... args)
    {
        return format_n(std::forward<decltype(single)>(single), std::forward<decltype(plural)>(plural), std::forward<decltype(n)>(n), std::forward<TArgs>(args)...);
    }

    /**
     * Translates and formats plural text in a given context using the locale initialized by this library.
     * Replaces the use of boost::format with a variadic function call.
     * Use the default domain, i.e. PROJECT_NAME.
     * Specialized to the PROJECT_NAME domain.
     * @param context The context string.
     * @param single The singular format string.
     * @param plural The plural format string.
     * @param n Number of items, used to choose singular or plural.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template <typename... TArgs>
    std::string format_np(std::string const& context, std::string const& single, std::string const& plural, int n, TArgs... args)
    {
        auto trans = [&context, &single, &plural, n](const std::string& domain) {return translate_np(context, single, plural, n, domain);};
        return format_common(std::move(trans), std::forward<TArgs>(args)...);
    }

    /**
     * Translates and formats plural text in a given context using the locale initialized by this library.
     * Alias for format_np(...); Convenience function for adding i18n support.
     * @param context The context string.
     * @param single The singular format string.
     * @param plural The plural format string.
     * @param n Number of items, used to choose singular or plural.
     * @param args Format arguments.
     * @return The string generated by translating the format string, then applying the arguments.
     */
    template<typename... TArgs>
    inline std::string np_(std::string const& context, std::string const& single, std::string const& plural, int n, TArgs&&... args)
    {
        return format_np(std::forward<decltype(context)>(context), std::forward<decltype(single)>(single), std::forward<decltype(plural)>(plural), std::forward<decltype(n)>(n), std::forward<TArgs>(args)...);
    }
}}  // namespace leatherman::locale
