/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.car.settings.tts;

import android.car.drivingstate.CarUxRestrictions;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TtsEngines;
import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;

import com.android.car.settings.R;
import com.android.car.settings.common.ActivityResultCallback;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.common.SeekBarPreference;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Locale;
import java.util.Objects;

/**
 * Business logic for configuring and listening to the current TTS voice. This preference controller
 * handles the following:
 *
 * <ol>
 * <li>Changing the TTS language
 * <li>Changing the TTS speech rate
 * <li>Changing the TTS voice pitch
 * <li>Resetting the TTS configuration
 * </ol>
 */
public class TtsPlaybackPreferenceController extends
        PreferenceController<PreferenceGroup> implements ActivityResultCallback {

    private static final Logger LOG = new Logger(TtsPlaybackPreferenceController.class);

    @VisibleForTesting
    static final int VOICE_DATA_CHECK = 1;
    @VisibleForTesting
    static final int GET_SAMPLE_TEXT = 2;

    private TtsEngines mEnginesHelper;
    private TtsPlaybackSettingsManager mTtsPlaybackManager;
    private TextToSpeech mTts;
    private int mSelectedLocaleIndex;

    private ListPreference mDefaultLanguagePreference;
    private SeekBarPreference mSpeechRatePreference;
    private SeekBarPreference mVoicePitchPreference;
    private Preference mResetPreference;

    private String mSampleText;
    private Locale mSampleTextLocale;

    private Handler mUiHandler;
    @VisibleForTesting
    Handler mBackgroundHandler;
    private HandlerThread mBackgroundHandlerThread;

    /** True if initialized with no errors. */
    private boolean mTtsInitialized = false;

    @VisibleForTesting
    final TextToSpeech.OnInitListener mOnInitListener = status -> {
        if (status == TextToSpeech.SUCCESS) {
            mTtsInitialized = true;
            mTtsPlaybackManager = new TtsPlaybackSettingsManager(getContext(), mTts,
                    mEnginesHelper);
            mTts.setSpeechRate(mTtsPlaybackManager.getCurrentSpeechRate()
                    / TtsPlaybackSettingsManager.SCALING_FACTOR);
            mTts.setPitch(mTtsPlaybackManager.getCurrentVoicePitch()
                    / TtsPlaybackSettingsManager.SCALING_FACTOR);
            startEngineVoiceDataCheck(mTts.getCurrentEngine());
            mBackgroundHandler.post(() -> {
                checkOrUpdateSampleText();
                mUiHandler.post(this::refreshUi);
            });
        }
    };

    public TtsPlaybackPreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
        this(context, preferenceKey, fragmentController, uxRestrictions, new TtsEngines(context));
    }

    @VisibleForTesting
    TtsPlaybackPreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions,
            TtsEngines enginesHelper) {
        super(context, preferenceKey, fragmentController, uxRestrictions);
        mEnginesHelper = enginesHelper;
    }

    @Override
    protected Class<PreferenceGroup> getPreferenceType() {
        return PreferenceGroup.class;
    }

    @Override
    protected void onCreateInternal() {
        mDefaultLanguagePreference = initDefaultLanguagePreference();
        mSpeechRatePreference = initSpeechRatePreference();
        mVoicePitchPreference = initVoicePitchPreference();
        mResetPreference = initResetTtsPlaybackPreference();

        mUiHandler = new Handler(Looper.getMainLooper());
        mBackgroundHandlerThread = new HandlerThread(/* name= */"BackgroundHandlerThread");
        mBackgroundHandlerThread.start();
        mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper());

        mTts = createTts();
    }

    @Override
    protected void onDestroyInternal() {
        if (mBackgroundHandlerThread != null) {
            mBackgroundHandlerThread.quit();
            mBackgroundHandler = null;
            mBackgroundHandlerThread = null;
        }
        if (mTts != null) {
            mTts.shutdown();
            mTts = null;
            mTtsPlaybackManager = null;
        }
    }

    @Override
    protected void updateState(PreferenceGroup preference) {
        boolean isValid = isDefaultLocaleValid();
        mDefaultLanguagePreference.setEnabled(isValid);
        // Always hide default language preference for now.
        // TODO: Unhide once product requirements are clarified.
        mDefaultLanguagePreference.setVisible(false);
        mSpeechRatePreference.setEnabled(isValid);
        mVoicePitchPreference.setEnabled(isValid);
        mResetPreference.setEnabled(isValid);
        if (!isValid && mDefaultLanguagePreference.getEntries() != null) {
            mDefaultLanguagePreference.setEnabled(true);
        }

        if (mDefaultLanguagePreference.getEntries() != null) {
            mDefaultLanguagePreference.setValueIndex(mSelectedLocaleIndex);
            mDefaultLanguagePreference.setSummary(
                    mDefaultLanguagePreference.getEntries()[mSelectedLocaleIndex]);
        }

        if (mTtsInitialized) {
            mSpeechRatePreference.setValue(mTtsPlaybackManager.getCurrentSpeechRate());
            mVoicePitchPreference.setValue(mTtsPlaybackManager.getCurrentVoicePitch());
        }
    }

    @Override
    public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        switch (requestCode) {
            case VOICE_DATA_CHECK:
                onVoiceDataIntegrityCheckDone(resultCode, data);
                break;
            case GET_SAMPLE_TEXT:
                onSampleTextReceived(resultCode, data);
                break;
            default:
                LOG.e("Got unknown activity result");
        }
    }

    private void startEngineVoiceDataCheck(String engine) {
        Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
        intent.setPackage(engine);
        try {
            LOG.d("Updating engine: Checking voice data: " + intent.toUri(0));
            getFragmentController().startActivityForResult(intent, VOICE_DATA_CHECK,
                    this);
        } catch (ActivityNotFoundException ex) {
            LOG.e("Failed to check TTS data, no activity found for " + intent);
        }
    }

    /**
     * Ask the current default engine to return a string of sample text to be
     * spoken to the user.
     */
    private void startGetSampleText() {
        String currentEngine = mTts.getCurrentEngine();
        if (TextUtils.isEmpty(currentEngine)) {
            currentEngine = mTts.getDefaultEngine();
        }

        Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
        mSampleTextLocale = mTtsPlaybackManager.getEffectiveTtsLocale();
        if (mSampleTextLocale == null) {
            return;
        }
        intent.putExtra(TextToSpeech.Engine.KEY_PARAM_LANGUAGE, mSampleTextLocale.getLanguage());
        intent.putExtra(TextToSpeech.Engine.KEY_PARAM_COUNTRY, mSampleTextLocale.getCountry());
        intent.putExtra(TextToSpeech.Engine.KEY_PARAM_VARIANT, mSampleTextLocale.getVariant());
        intent.setPackage(currentEngine);

        try {
            LOG.d("Getting sample text: " + intent.toUri(0));
            getFragmentController().startActivityForResult(intent, GET_SAMPLE_TEXT, this);
        } catch (ActivityNotFoundException ex) {
            LOG.e("Failed to get sample text, no activity found for " + intent + ")");
        }
    }

    /** The voice data check is complete. */
    private void onVoiceDataIntegrityCheckDone(int resultCode, Intent data) {
        String engine = mTts.getCurrentEngine();
        if (engine == null) {
            LOG.e("Voice data check complete, but no engine bound");
            return;
        }

        if (data == null || resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) {
            LOG.e("Engine failed voice data integrity check (null return or invalid result code)"
                    + mTts.getCurrentEngine());
            return;
        }

        Settings.Secure.putString(getContext().getContentResolver(),
                Settings.Secure.TTS_DEFAULT_SYNTH, engine);

        ArrayList<String> availableLangs =
                data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
        if (availableLangs == null || availableLangs.size() == 0) {
            refreshUi();
            return;
        }
        updateDefaultLanguagePreference(availableLangs);
        mSelectedLocaleIndex = findLocaleIndex(mTtsPlaybackManager.getStoredTtsLocale());
        if (mSelectedLocaleIndex < 0) {
            mSelectedLocaleIndex = 0;
        }
        mBackgroundHandler.post(() -> {
            startGetSampleText();
            mUiHandler.post(this::refreshUi);
        });
    }

    private void onSampleTextReceived(int resultCode, Intent data) {
        String sample = getContext().getString(R.string.tts_default_sample_string);

        if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
            String tmp = data.getStringExtra(TextToSpeech.Engine.EXTRA_SAMPLE_TEXT);
            if (!TextUtils.isEmpty(tmp)) {
                sample = tmp;
            }
            LOG.d("Got sample text: " + sample);
        } else {
            LOG.d("Using default sample text :" + sample);
        }

        mSampleText = sample;
    }

    private void updateLanguageTo(Locale locale) {
        int selectedLocaleIndex = findLocaleIndex(locale);
        if (selectedLocaleIndex == -1) {
            LOG.w("updateLanguageTo called with unknown locale argument");
            return;
        }

        if (mTtsPlaybackManager.updateTtsLocale(locale)) {
            mSelectedLocaleIndex = selectedLocaleIndex;
            checkOrUpdateSampleText();
            refreshUi();
        } else {
            LOG.e("updateLanguageTo failed to update tts language");
        }
    }

    private int findLocaleIndex(Locale locale) {
        String localeString = (locale != null) ? locale.toString() : "";
        return mDefaultLanguagePreference.findIndexOfValue(localeString);
    }

    private boolean isDefaultLocaleValid() {
        if (!mTtsInitialized) {
            return false;
        }

        if (mSampleTextLocale == null) {
            LOG.e("Default language was not retrieved from engine " + mTts.getCurrentEngine());
            return false;
        }

        if (mDefaultLanguagePreference.getEntries() == null) {
            return false;
        }

        int index = mDefaultLanguagePreference.findIndexOfValue(mSampleTextLocale.toString());
        if (index < 0) {
            return false;
        }
        return true;
    }

    private void checkOrUpdateSampleText() {
        if (!mTtsInitialized) {
            return;
        }
        Locale defaultLocale = mTtsPlaybackManager.getEffectiveTtsLocale();
        if (defaultLocale == null) {
            LOG.e("Failed to get default language from engine " + mTts.getCurrentEngine());
            return;
        }

        if (!Objects.equals(defaultLocale, mSampleTextLocale)) {
            mSampleText = null;
            mSampleTextLocale = null;
        }

        if (mSampleText == null) {
            startGetSampleText();
        }
    }

    @VisibleForTesting
    TextToSpeech createTts() {
        return new TextToSpeech(getContext(), mOnInitListener);
    }

    @VisibleForTesting
    String getSampleText() {
        return mSampleText;
    }

    /* ***************************************************************************************** *
     * Preference initialization/update code.                                                    *
     * ***************************************************************************************** */

    private ListPreference initDefaultLanguagePreference() {
        ListPreference defaultLanguagePreference = (ListPreference) getPreference().findPreference(
                getContext().getString(R.string.pk_tts_default_language));
        defaultLanguagePreference.setOnPreferenceChangeListener((preference, newValue) -> {
            String localeString = (String) newValue;
            updateLanguageTo(!TextUtils.isEmpty(localeString) ? mEnginesHelper.parseLocaleString(
                    localeString) : null);
            checkOrUpdateSampleText();
            return true;
        });
        return defaultLanguagePreference;
    }

    private void updateDefaultLanguagePreference(@NonNull ArrayList<String> availableLangs) {
        // Sort locales by display name.
        ArrayList<Locale> locales = new ArrayList<>();
        for (int i = 0; i < availableLangs.size(); i++) {
            Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i));
            if (locale != null) {
                locales.add(locale);
            }
        }
        Collections.sort(locales,
                (lhs, rhs) -> lhs.getDisplayName().compareToIgnoreCase(rhs.getDisplayName()));

        // Separate pairs into two separate arrays.
        CharSequence[] entries = new CharSequence[availableLangs.size() + 1];
        CharSequence[] entryValues = new CharSequence[availableLangs.size() + 1];

        entries[0] = getContext().getString(R.string.tts_lang_use_system);
        entryValues[0] = "";

        int i = 1;
        for (Locale locale : locales) {
            entries[i] = locale.getDisplayName();
            entryValues[i++] = locale.toString();
        }

        mDefaultLanguagePreference.setEntries(entries);
        mDefaultLanguagePreference.setEntryValues(entryValues);
    }

    private SeekBarPreference initSpeechRatePreference() {
        SeekBarPreference speechRatePreference = (SeekBarPreference) getPreference().findPreference(
                getContext().getString(R.string.pk_tts_speech_rate));
        speechRatePreference.setMin(TtsPlaybackSettingsManager.MIN_SPEECH_RATE);
        speechRatePreference.setMax(TtsPlaybackSettingsManager.MAX_SPEECH_RATE);
        speechRatePreference.setShowSeekBarValue(false);
        speechRatePreference.setContinuousUpdate(false);
        speechRatePreference.setOnPreferenceChangeListener((preference, newValue) -> {
            if (mTtsPlaybackManager != null) {
                mTtsPlaybackManager.updateSpeechRate((Integer) newValue);
                mTtsPlaybackManager.speakSampleText(mSampleText);
                return true;
            }
            LOG.e("speech rate preference enabled before it is allowed");
            return false;
        });

        // Initially disable.
        speechRatePreference.setEnabled(false);
        return speechRatePreference;
    }

    private SeekBarPreference initVoicePitchPreference() {
        SeekBarPreference pitchPreference = (SeekBarPreference) getPreference().findPreference(
                getContext().getString(R.string.pk_tts_pitch));
        pitchPreference.setMin(TtsPlaybackSettingsManager.MIN_VOICE_PITCH);
        pitchPreference.setMax(TtsPlaybackSettingsManager.MAX_VOICE_PITCH);
        pitchPreference.setShowSeekBarValue(false);
        pitchPreference.setContinuousUpdate(false);
        pitchPreference.setOnPreferenceChangeListener((preference, newValue) -> {
            if (mTtsPlaybackManager != null) {
                mTtsPlaybackManager.updateVoicePitch((Integer) newValue);
                mTtsPlaybackManager.speakSampleText(mSampleText);
                return true;
            }
            LOG.e("speech pitch preference enabled before it is allowed");
            return false;
        });

        // Initially disable.
        pitchPreference.setEnabled(false);
        return pitchPreference;
    }

    private Preference initResetTtsPlaybackPreference() {
        Preference resetPreference = getPreference().findPreference(
                getContext().getString(R.string.pk_tts_reset));
        resetPreference.setOnPreferenceClickListener(preference -> {
            if (mTtsPlaybackManager != null) {
                mTtsPlaybackManager.resetVoicePitch();
                mTtsPlaybackManager.resetSpeechRate();
                refreshUi();
                return true;
            }
            LOG.e("reset preference enabled before it is allowed");
            return false;
        });

        // Initially disable.
        resetPreference.setEnabled(false);
        return resetPreference;
    }
}
