package com.atlassian.bitbucket.i18n;

import com.google.common.base.Joiner;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import javax.annotation.Nonnull;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

import static org.apache.commons.lang3.StringUtils.defaultString;

/**
 * A simplified implementation of the {@link I18nService} which uses {@code MessageFormat} to directly format the
 * fallback messages, rather than attempting to perform any real internationalisation.
 * <p>
 * This implementation has been built purely to support testing; it is not intended for use in production code.
 */
public class SimpleI18nService implements I18nService {

    private final Mode mode;

    /**
     * Constructs a {@code SimpleI18nService} which will format arguments into the fallback message and return it.
     *
     * @see Mode#FORMAT_MESSAGES
     */
    public SimpleI18nService() {
        this(Mode.RETURN_KEYS);
    }

    /**
     * Constructs a {@code SimpleI18nService} which will use the specified {@link Mode mode} for handling messages.
     *
     * @see Mode
     */
    public SimpleI18nService(Mode mode) {
        this.mode = mode;
    }

    @Override
    @Nonnull
    public KeyedMessage createKeyedMessage(@Nonnull String key, @Nonnull Object... arguments) {
        return getKeyedText(key, null, arguments);
    }

    @Override
    @Nonnull
    public String getMessage(@Nonnull String key, @Nonnull Object... arguments) {
        return getText(key, null, arguments);
    }

    @Nonnull
    @Override
    public String getMessage(@Nonnull I18nKey key) {
        return getText(key.getKey(), null, key.getArguments());
    }

    @Override
    @Nonnull
    public Map<String, String> getAllTranslationsForPrefix(@Nonnull String prefix) {
        return Collections.emptyMap();
    }

    @Override
    @Nonnull
    public Map<String, String> getAllTranslationsForPrefix(@Nonnull String prefix, @Nonnull Locale locale) {
        return getAllTranslationsForPrefix(prefix);
    }

    @Override
    @Nonnull
    public KeyedMessage getKeyedText(@Nonnull String key, String fallbackMessage, @Nonnull Object... arguments) {
        String message = getText(key, fallbackMessage, arguments);

        return new KeyedMessage(defaultString(key), defaultString(message), defaultString(message));
    }

    @Override
    public String getMessagePattern(@Nonnull String key) {
        return getMessagePattern(key, null);
    }

    @Override
    public String getMessagePattern(@Nonnull String key, String fallbackPattern) {
        return fallbackPattern;
    }

    @Override
    public String getMessagePattern(@Nonnull Locale locale, @Nonnull String key) {
        return getMessagePattern(key, null);
    }

    @Override
    @Nonnull
    public KeyedMessage getKeyedText(@Nonnull I18nKey i18nKey) {
        return createKeyedMessage(i18nKey.getKey(), i18nKey.getArguments());
    }

    @Override
    @Nonnull
    public String getText(@Nonnull Locale locale, @Nonnull String key, String fallbackMessage, @Nonnull Object... arguments) {
        return getText(key, fallbackMessage, arguments);
    }

    @Override
    @Nonnull
    public String getText(@Nonnull String key, String fallbackMessage, @Nonnull Object... arguments) {
        return mode.apply(key, fallbackMessage, arguments);
    }

    /**
     * Return a matcher that will match a message generated with {@link #createKeyedMessage(String, Object...)}.
     *
     * @param key the i18n key in the message to match.
     * @param arguments the arguments to match.
     *
     * @return a matcher that will match a message generated with {@link #createKeyedMessage(String, Object...)}.
     */
    public Matcher<KeyedMessage> keyedMessageMatcherFor(String key, Object... arguments) {
        return new KeyedMessageMatcher(mode, key, null, arguments);
    }

    /**
     * Enumerates the possible modes for the {@link SimpleI18nService}.
     */
    public enum Mode {

        /**
         * Uses {@code MessageFormat} to format all arguments into the fallback message and return the result.
         */
        FORMAT_MESSAGES {

            @Override
            public String apply(String key, String fallbackMessage, Object... arguments) {
                return MessageFormat.format(fallbackMessage, arguments);
            }
        },
        /**
         * Returns the message key, which can be useful in verifying that the expected message is being looked up.
         */
        RETURN_KEYS {

            @Override
            public String apply(String key, String fallbackMessage, Object... arguments) {
                return key;
            }
        },
        /**
         * Returns the message key and the arguments, which can be useful in verifying that the expected message is
         * being looked up with the expected arguments.
         */
        RETURN_KEYS_WITH_ARGUMENTS {

            @Override
            public String apply(String key, String fallbackMessage, Object... arguments) {
                if (arguments.length == 0) {
                    return key;
                }
                return key + "(" + Joiner.on(", ").join(arguments) + ")";
            }
        },
        /**
         * Returns the fallback message <i>without</i> formatting any arguments into it. Placeholders are left intact,
         * which can be useful in verifying format strings.
         */
        RETURN_MESSAGES {

            @Override
            public String apply(String key, String fallbackMessage, Object... arguments) {
                return fallbackMessage;
            }
        };

        public abstract String apply(String key, String fallbackMessage, Object... arguments);
    }

    private static class KeyedMessageMatcher extends TypeSafeDiagnosingMatcher<KeyedMessage> {
        private final String key;
        private final String message;

        private KeyedMessageMatcher(Mode mode, String key, String fallback, Object...arguments) {
            this.key = key;
            this.message = mode.apply(key, fallback, arguments);
        }

        @Override
        protected boolean matchesSafely(KeyedMessage item, Description mismatchDescription) {
            if (Objects.equals(key, item.getKey())
                && Objects.equals(message, item.getRootMessage())
                && Objects.equals(message, item.getLocalisedMessage())) {
                return true;
            } else {
                mismatchDescription.appendValue(String.format("{key: %s, localMessage: %s, rootMessage: %s}",
                        item.getKey(), item.getLocalisedMessage(), item.getRootMessage()));
                return false;
            }
        }

        @Override
        public void describeTo(Description description) {
            description.appendValue(String.format("{key: %1$s, localMessage: %2$s, rootMessage: %2$s}", key, message));
        }
    }
}
