// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.omnibox;

import android.os.Bundle;
import android.speech.RecognizerIntent;

import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.search_engines.TemplateUrlService;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * A search provider that processes and stores voice recognition results and makes them available
 * in the omnibox results. The voice results are added to the end of the omnibox results.
 */
public class VoiceSuggestionProvider {
    private static final float CONFIDENCE_THRESHOLD_SHOW = 0.3f;
    private static final float CONFIDENCE_THRESHOLD_HIDE_ALTS = 0.8f;

    private final List<VoiceResult> mResults = new ArrayList<VoiceResult>();

    private final float mConfidenceThresholdShow;
    private final float mConfidenceThresholdHideAlts;

    /**
     * Creates an instance of a {@link VoiceSuggestionProvider} class.
     */
    public VoiceSuggestionProvider() {
        mConfidenceThresholdShow = CONFIDENCE_THRESHOLD_SHOW;
        mConfidenceThresholdHideAlts = CONFIDENCE_THRESHOLD_HIDE_ALTS;
    }

    /**
     * Creates an instance of a {@link VoiceSuggestionProvider} class.
     * @param confidenceThresholdShow The minimum confidence a result can have to be shown (does not
     *                                include the first result).  Confidence values are between
     *                                0.0 and 1.0.
     * @param confidenceThresholdHideAlts The maximum confidence the first result can have before it
     *                                    will no longer show alternates.  Confidence values are
     *                                    between 0.0 and 1.0.
     */
    @VisibleForTesting
    protected VoiceSuggestionProvider(float confidenceThresholdShow,
            float confidenceThresholdHideAlts) {
        mConfidenceThresholdShow = confidenceThresholdShow;
        mConfidenceThresholdHideAlts = confidenceThresholdHideAlts;
    }

    /**
     * Clears the current voice search results.
     */
    public void clearVoiceSearchResults() {
        mResults.clear();
    }

    /**
     * @return The current voice search results.  This could be {@code null} if no results are
     *         currently present.
     */
    public List<VoiceResult> getResults() {
        return Collections.unmodifiableList(mResults);
    }

    /**
     * Takes and processes the results from a recognition action. It parses the confidence and
     * string values and stores the processed results here so they are made available to the
     * {@link AutocompleteController} and show up in the omnibox results. This method does not
     * reorder the voice results that come back from the recognizer.
     * @param extras The {@link Bundle} that contains the recognition results from a
     *               {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH} action.
     */
    public void setVoiceResultsFromIntentBundle(Bundle extras) {
        clearVoiceSearchResults();

        if (extras == null) return;

        ArrayList<String> strings = extras.getStringArrayList(
                RecognizerIntent.EXTRA_RESULTS);
        float[] confidences = extras.getFloatArray(
                RecognizerIntent.EXTRA_CONFIDENCE_SCORES);

        if (strings == null || confidences == null) return;

        assert (strings.size() == confidences.length);
        if (strings.size() != confidences.length) return;

        for (int i = 0; i < strings.size(); ++i) {
            // Remove any spaces in the voice search match when determining whether it
            // appears to be a URL. This is to prevent cases like (
            // "tech crunch.com" and "www. engadget .com" from not appearing like URLs)
            // from not navigating to the URL.
            // If the string appears to be a URL, then use it instead of the string returned from
            // the voice engine.
            String culledString = strings.get(i).replaceAll(" ", "");
            String url = AutocompleteController.nativeQualifyPartialURLQuery(culledString);
            mResults.add(new VoiceResult(
                    url == null ? strings.get(i) : culledString, confidences[i]));
        }
    }

    /**
     * Adds the currently stored voice recognition results to the current list of
     * {@link OmniboxSuggestion}s passed in.  Returns the new list to the caller.
     * @param suggestions The current list of {@link OmniboxSuggestion}s.
     * @param maxVoiceResults The maximum number of voice results that should be added.
     * @return A new list of {@link OmniboxSuggestion}s, which can include voice results.
     */
    public List<OmniboxSuggestion> addVoiceSuggestions(
            List<OmniboxSuggestion> suggestions, int maxVoiceResults) {
        if (mResults.size() == 0 || maxVoiceResults == 0) return suggestions;

        List<OmniboxSuggestion> newSuggestions = new ArrayList<OmniboxSuggestion>();
        if (suggestions != null && suggestions.size() > 0) {
            newSuggestions.addAll(suggestions);
        }

        VoiceResult firstResult = mResults.get(0);
        addVoiceResultToOmniboxSuggestions(newSuggestions, firstResult, 0);

        final int suggestionLength = suggestions != null ? suggestions.size() : 0;
        if (firstResult.getConfidence() < mConfidenceThresholdHideAlts) {
            for (int i = 1; i < mResults.size()
                    && newSuggestions.size() < suggestionLength + maxVoiceResults; ++i) {
                addVoiceResultToOmniboxSuggestions(newSuggestions, mResults.get(i),
                        mConfidenceThresholdShow);
            }
        }

        return newSuggestions;
    }

    private void addVoiceResultToOmniboxSuggestions(List<OmniboxSuggestion> suggestions,
            VoiceResult result, float confidenceThreshold) {
        if (doesVoiceResultHaveMatch(suggestions, result)) return;
        if (result.getConfidence() < confidenceThreshold && result.getConfidence() > 0) return;
        String voiceUrl = TemplateUrlService.getInstance().getUrlForVoiceSearchQuery(
                result.getMatch());
        suggestions.add(new OmniboxSuggestion(
                OmniboxSuggestion.Type.VOICE_SUGGEST.nativeType(),
                0,
                1,
                result.getMatch(),
                null,
                null,
                null,
                null,
                voiceUrl,
                voiceUrl,
                false,
                false));
    }

    private boolean doesVoiceResultHaveMatch(List<OmniboxSuggestion> suggestions,
            VoiceResult result) {
        for (OmniboxSuggestion suggestion : suggestions) {
            if (suggestion.getDisplayText().equals(result.getMatch())) return true;
        }

        return false;
    }

    /**
     * A storage class that holds voice recognition string matches and confidence scores.
     */
    public static class VoiceResult {
        private final String mMatch;
        private final float mConfidence;

        /**
         * Creates an instance of a VoiceResult.
         * @param match The text match from the voice recognition.
         * @param confidence The confidence value of the recognition that should go from 0.0 to 1.0.
         */
        public VoiceResult(String match, float confidence) {
            mMatch = match;
            mConfidence = confidence;
        }

        /**
         * @return The text match from the voice recognition.
         */
        public String getMatch() {
            return mMatch;
        }

        /**
         * @return The confidence value of the recognition that should go from 0.0 to 1.0.
         */
        public float getConfidence() {
            return mConfidence;
        }
    }
}
