/*
 * Copyright (C) 2022 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.settings.accessibility;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.Log;
import android.view.Choreographer;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;

import com.android.settings.R;
import com.android.settings.accessibility.TextReadingPreferenceFragment.EntryPoint;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.instrumentation.SettingsStatsLog;
import com.android.settings.display.PreviewPagerAdapter;
import com.android.settings.widget.LabeledSeekBarPreference;

import java.util.Objects;

/**
 * A {@link BasePreferenceController} for controlling the preview pager of the text and reading
 * options.
 */
class TextReadingPreviewController extends BasePreferenceController implements
        PreviewSizeSeekBarController.ProgressInteractionListener {
    private static final String TAG = "TextReadingPreviewCtrl";
    private static final int LAYER_INITIAL_INDEX = 0;
    private static final int FRAME_INITIAL_INDEX = 0;
    private static final String PREVIEW_KEY = "preview";
    private static final String FONT_SIZE_KEY = "font_size";
    private static final String DISPLAY_SIZE_KEY = "display_size";
    private static final long MIN_COMMIT_INTERVAL_MS = 800;
    private static final long CHANGE_BY_SEEKBAR_DELAY_MS = 100;
    private static final long CHANGE_BY_BUTTON_DELAY_MS = 300;
    private final FontSizeData mFontSizeData;
    private final DisplaySizeData mDisplaySizeData;
    private int mLastFontProgress;
    private int mLastDisplayProgress;
    private long mLastCommitTime;
    private TextReadingPreviewPreference mPreviewPreference;
    private LabeledSeekBarPreference mFontSizePreference;
    private LabeledSeekBarPreference mDisplaySizePreference;

    @EntryPoint
    private int mEntryPoint;

    private final Choreographer.FrameCallback mCommit = f -> {
        tryCommitFontSizeConfig();
        tryCommitDisplaySizeConfig();

        mLastCommitTime = SystemClock.elapsedRealtime();
    };

    TextReadingPreviewController(Context context, String preferenceKey,
            @NonNull FontSizeData fontSizeData, @NonNull DisplaySizeData displaySizeData) {
        super(context, preferenceKey);
        mFontSizeData = fontSizeData;
        mDisplaySizeData = displaySizeData;
    }

    @Override
    public int getAvailabilityStatus() {
        return AVAILABLE;
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);

        mPreviewPreference = screen.findPreference(PREVIEW_KEY);

        mFontSizePreference = screen.findPreference(FONT_SIZE_KEY);
        mDisplaySizePreference = screen.findPreference(DISPLAY_SIZE_KEY);
        Objects.requireNonNull(mFontSizePreference,
                /* message= */ "Font size preference is null, the preview controller "
                        + "couldn't get the info");
        Objects.requireNonNull(mDisplaySizePreference,
                /* message= */ "Display size preference is null, the preview controller"
                        + " couldn't get the info");

        mLastFontProgress = mFontSizeData.getInitialIndex();
        mLastDisplayProgress = mDisplaySizeData.getInitialIndex();

        final Configuration origConfig = mContext.getResources().getConfiguration();
        final boolean isLayoutRtl =
                origConfig.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
        final int[] previewSamples = getPreviewSampleLayouts(mContext);
        final PreviewPagerAdapter pagerAdapter = new PreviewPagerAdapter(mContext, isLayoutRtl,
                previewSamples, createConfig(origConfig));
        mPreviewPreference.setPreviewAdapter(pagerAdapter);
        mPreviewPreference.setCurrentItem(
                isLayoutRtl ? previewSamples.length - 1 : FRAME_INITIAL_INDEX);

        final int initialPagerIndex =
                mLastFontProgress * mDisplaySizeData.getValues().size() + mLastDisplayProgress;
        mPreviewPreference.setLastLayerIndex(initialPagerIndex);
        pagerAdapter.setPreviewLayer(initialPagerIndex, LAYER_INITIAL_INDEX,
                FRAME_INITIAL_INDEX, /* animate= */ false);
    }

    @Override
    public void notifyPreferenceChanged() {
        mPreviewPreference.notifyPreviewPagerChanged(getPagerIndex());
    }

    @Override
    public void onProgressChanged() {
        postCommitDelayed(CHANGE_BY_BUTTON_DELAY_MS);
    }

    @Override
    public void onEndTrackingTouch() {
        postCommitDelayed(CHANGE_BY_SEEKBAR_DELAY_MS);
    }

    void setCurrentItem(int index) {
        mPreviewPreference.setCurrentItem(index);
    }

    int getCurrentItem() {
        return mPreviewPreference.getCurrentItem();
    }

    /**
     * The entry point is used for logging.
     *
     * @param entryPoint from which settings page
     */
    void setEntryPoint(@EntryPoint int entryPoint) {
        mEntryPoint = entryPoint;
    }

    /**
     * Avoids the flicker when switching to the previous or next level.
     *
     * <p><br>[Flickering problem steps] commit()-> snapshot in framework(old screenshot) ->
     * app update the preview -> snapshot(old screen) fade out</p>
     *
     * <p><br>To prevent flickering problem, we make sure that we update the local preview
     * first and then we do the commit later. </p>
     *
     * <p><br><b>Note:</b> It doesn't matter that we use
     * Choreographer or main thread handler since the delay time is longer
     * than 1 frame. Use Choreographer to let developer understand it's a
     * window update.</p>
     *
     * @param commitDelayMs the interval time after a action.
     */
    void postCommitDelayed(long commitDelayMs) {
        if (SystemClock.elapsedRealtime() - mLastCommitTime < MIN_COMMIT_INTERVAL_MS) {
            commitDelayMs += MIN_COMMIT_INTERVAL_MS;
        }

        final Choreographer choreographer = Choreographer.getInstance();
        choreographer.removeFrameCallback(mCommit);
        choreographer.postFrameCallbackDelayed(mCommit, commitDelayMs);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    static int[] getPreviewSampleLayouts(Context context) {
        TypedArray previews = context.getResources().obtainTypedArray(
                R.array.config_text_reading_preview_samples);
        int previewCount = previews.length();
        int[] previewSamples = new int[previewCount];
        for (int i = 0; i < previewCount; i++) {
            previewSamples[i] = previews.getResourceId(i, R.layout.screen_zoom_preview_1);
        }
        previews.recycle();
        return previewSamples;
    }

    private int getPagerIndex() {
        final int displayDataSize = mDisplaySizeData.getValues().size();
        final int fontSizeProgress = mFontSizePreference.getProgress();
        final int displaySizeProgress = mDisplaySizePreference.getProgress();

        // To be consistent with the {@link PreviewPagerAdapter#setPreviewLayer(int, int, int,
        // boolean)} behavior, here also needs the same design. In addition, please also refer to
        // the {@link #createConfig(Configuration)}.
        return fontSizeProgress * displayDataSize + displaySizeProgress;
    }

    private void tryCommitFontSizeConfig() {
        final int fontProgress = mFontSizePreference.getProgress();
        if (fontProgress != mLastFontProgress) {
            mFontSizeData.commit(fontProgress);
            mLastFontProgress = fontProgress;

            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Font size: " + fontProgress);
            }

            SettingsStatsLog.write(
                    SettingsStatsLog.ACCESSIBILITY_TEXT_READING_OPTIONS_CHANGED,
                    AccessibilityStatsLogUtils.convertToItemKeyName(mFontSizePreference.getKey()),
                    fontProgress,
                    AccessibilityStatsLogUtils.convertToEntryPoint(mEntryPoint));
        }
    }

    private void tryCommitDisplaySizeConfig() {
        final int displayProgress = mDisplaySizePreference.getProgress();
        if (displayProgress != mLastDisplayProgress) {
            mDisplaySizeData.commit(displayProgress);
            mLastDisplayProgress = displayProgress;

            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Display size: " + displayProgress);
            }

            SettingsStatsLog.write(
                    SettingsStatsLog.ACCESSIBILITY_TEXT_READING_OPTIONS_CHANGED,
                    AccessibilityStatsLogUtils.convertToItemKeyName(
                            mDisplaySizePreference.getKey()),
                    displayProgress,
                    AccessibilityStatsLogUtils.convertToEntryPoint(mEntryPoint));
        }
    }

    private Configuration[] createConfig(Configuration origConfig) {
        final int fontDataSize = mFontSizeData.getValues().size();
        final int displayDataSize = mDisplaySizeData.getValues().size();
        final int totalNum = fontDataSize * displayDataSize;
        final Configuration[] configurations = new Configuration[totalNum];

        for (int i = 0; i < fontDataSize; ++i) {
            for (int j = 0; j < displayDataSize; ++j) {
                final Configuration config = new Configuration(origConfig);
                config.fontScale = mFontSizeData.getValues().get(i);
                config.densityDpi = mDisplaySizeData.getValues().get(j);

                configurations[i * displayDataSize + j] = config;
            }
        }

        return configurations;
    }
}
