package android.speech.tts;

import android.speech.tts.TextToSpeechClient.EngineStatus;

import java.util.Locale;

/**
 * Set of common heuristics for selecting {@link VoiceInfo} from
 * {@link TextToSpeechClient#getEngineStatus()} output.
 */
public final class RequestConfigHelper {
    private RequestConfigHelper() {}

    /**
     * Interface for scoring VoiceInfo object.
     */
    public static interface VoiceScorer {
        /**
         * Score VoiceInfo. If the score is less than or equal to zero, that voice is discarded.
         * If two voices have same desired primary characteristics (highest quality, lowest
         * latency or others), the one with the higher score is selected.
         */
        public int scoreVoice(VoiceInfo voiceInfo);
    }

    /**
     * Score positively voices that exactly match the locale supplied to the constructor.
     */
    public static final class ExactLocaleMatcher implements VoiceScorer {
        private final Locale mLocale;

        /**
         * Score positively voices that exactly match the given locale
         * @param locale Reference locale. If null, the default locale will be used.
         */
        public ExactLocaleMatcher(Locale locale) {
            if (locale == null) {
                mLocale = Locale.getDefault();
            } else {
                mLocale = locale;
            }
        }
        @Override
        public int scoreVoice(VoiceInfo voiceInfo) {
            return mLocale.equals(voiceInfo.getLocale()) ? 1 : 0;
        }
    }

    /**
     * Score positively voices that match exactly the given locale (score 3)
     * or that share same language and country (score 2), or that share just a language (score 1).
     */
    public static final class LanguageMatcher implements VoiceScorer {
        private final Locale mLocale;

        /**
         * Score positively voices with similar locale.
         * @param locale Reference locale. If null, default will be used.
         */
        public LanguageMatcher(Locale locale) {
            if (locale == null) {
                mLocale = Locale.getDefault();
            } else {
                mLocale = locale;
            }
        }

        @Override
        public int scoreVoice(VoiceInfo voiceInfo) {
            final Locale voiceLocale = voiceInfo.getLocale();
            if (mLocale.equals(voiceLocale)) {
                return 3;
            } else {
                if (mLocale.getLanguage().equals(voiceLocale.getLanguage())) {
                    if (mLocale.getCountry().equals(voiceLocale.getCountry())) {
                        return 2;
                    }
                    return 1;
                }
                return 0;
            }
        }
    }

    /**
     * Get the highest quality voice from voices that score more than zero from the passed scorer.
     * If there is more than one voice with the same highest quality, then this method returns one
     * with the highest score. If they share same score as well, one with the lower index in the
     * voices list is returned.
     *
     * @param engineStatus
     *            Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call.
     * @param voiceScorer
     *            Used to discard unsuitable voices and help settle cases where more than
     *            one voice has the desired characteristic.
     * @param hasToBeEmbedded
     *            If true, require the voice to be an embedded voice (no network
     *            access will be required for synthesis).
     */
    private static VoiceInfo getHighestQualityVoice(EngineStatus engineStatus,
            VoiceScorer voiceScorer, boolean hasToBeEmbedded) {
        VoiceInfo bestVoice = null;
        int bestScoreMatch = 1;
        int bestVoiceQuality = 0;

        for (VoiceInfo voice : engineStatus.getVoices()) {
            int score = voiceScorer.scoreVoice(voice);
            if (score <= 0 || hasToBeEmbedded && voice.getRequiresNetworkConnection()
                    || voice.getQuality() < bestVoiceQuality) {
                continue;
            }

            if (bestVoice == null ||
                    voice.getQuality() > bestVoiceQuality ||
                    score > bestScoreMatch) {
                bestVoice = voice;
                bestScoreMatch = score;
                bestVoiceQuality = voice.getQuality();
            }
        }
        return bestVoice;
    }

    /**
     * Get highest quality voice.
     *
     * Highest quality voice is selected from voices that score more than zero from the passed
     * scorer. If there is more than one voice with the same highest quality, then this method
     * will return one with the highest score. If they share same score as well, one with the lower
     * index in the voices list is returned.

     * @param engineStatus
     *            Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call.
     * @param hasToBeEmbedded
     *            If true, require the voice to be an embedded voice (no network
     *            access will be required for synthesis).
     * @param voiceScorer
     *            Scorer is used to discard unsuitable voices and help settle cases where more than
     *            one voice has highest quality.
     * @return RequestConfig with selected voice or null if suitable voice was not found.
     */
    public static RequestConfig highestQuality(EngineStatus engineStatus,
            boolean hasToBeEmbedded, VoiceScorer voiceScorer) {
        VoiceInfo voice = getHighestQualityVoice(engineStatus, voiceScorer, hasToBeEmbedded);
        if (voice == null) {
            return null;
        }
        return RequestConfig.Builder.newBuilder().setVoice(voice).build();
    }

    /**
     * Get highest quality voice for the default locale.
     *
     * Call {@link #highestQuality(EngineStatus, boolean, VoiceScorer)} with
     * {@link LanguageMatcher} set to device default locale.
     *
     * @param engineStatus
     *            Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call.
     * @param hasToBeEmbedded
     *            If true, require the voice to be an embedded voice (no network
     *            access will be required for synthesis).
     * @return RequestConfig with selected voice or null if suitable voice was not found.
     */
    public static RequestConfig highestQuality(EngineStatus engineStatus,
            boolean hasToBeEmbedded) {
        return highestQuality(engineStatus, hasToBeEmbedded,
                new LanguageMatcher(Locale.getDefault()));
    }

}
