/*
 * Copyright (C) 2021 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 android.widget;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.TextUtils;
import android.text.method.TransformationMethod;
import android.text.method.TranslationTransformationMethod;
import android.util.Log;
import android.view.View;
import android.view.translation.UiTranslationManager;
import android.view.translation.ViewTranslationCallback;
import android.view.translation.ViewTranslationRequest;
import android.view.translation.ViewTranslationResponse;

import java.lang.ref.WeakReference;

/**
 * Default implementation for {@link ViewTranslationCallback} for {@link TextView} components.
 * This class handles how to display the translated information for {@link TextView}.
 *
 * @hide
 */
public class TextViewTranslationCallback implements ViewTranslationCallback {

    private static final String TAG = "TextViewTranslationCb";

    private static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);

    private TranslationTransformationMethod mTranslationTransformation;
    private boolean mIsShowingTranslation = false;
    private boolean mAnimationRunning = false;
    private boolean mIsTextPaddingEnabled = false;
    private boolean mOriginalIsTextSelectable = false;
    private int mOriginalFocusable = 0;
    private boolean mOriginalFocusableInTouchMode = false;
    private boolean mOriginalClickable = false;
    private boolean mOriginalLongClickable = false;
    private CharSequence mPaddedText;
    private int mAnimationDurationMillis = 250; // default value

    private CharSequence mContentDescription;

    private void clearTranslationTransformation() {
        if (DEBUG) {
            Log.v(TAG, "clearTranslationTransformation: " + mTranslationTransformation);
        }
        mTranslationTransformation = null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onShowTranslation(@NonNull View view) {
        if (mIsShowingTranslation) {
            if (DEBUG) {
                Log.d(TAG, view + " is already showing translated text.");
            }
            return false;
        }
        ViewTranslationResponse response = view.getViewTranslationResponse();
        if (response == null) {
            Log.e(TAG, "onShowTranslation() shouldn't be called before "
                    + "onViewTranslationResponse().");
            return false;
        }
        // It is possible user changes text and new translation response returns, system should
        // update the translation response to keep the result up to date.
        // Because TextView.setTransformationMethod() will skip the same TransformationMethod
        // instance, we should create a new one to let new translation can work.
        TextView theTextView = (TextView) view;
        if (mTranslationTransformation == null
                || !response.equals(mTranslationTransformation.getViewTranslationResponse())) {
            TransformationMethod originalTranslationMethod =
                    theTextView.getTransformationMethod();
            mTranslationTransformation = new TranslationTransformationMethod(response,
                    originalTranslationMethod);
        }
        final TransformationMethod transformation = mTranslationTransformation;
        WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
        runChangeTextWithAnimationIfNeeded(
                theTextView,
                () -> {
                    mIsShowingTranslation = true;
                    mAnimationRunning = false;

                    TextView textView = textViewRef.get();
                    if (textView == null) {
                        return;
                    }
                    // TODO(b/177214256): support selectable text translation.
                    // We use the TransformationMethod to implement showing the translated text. The
                    // TextView does not support the text length change for TransformationMethod.
                    // If the text is selectable or editable, it will crash while selecting the
                    // text. To support being able to select translated text, we need broader
                    // changes to text APIs. For now, the callback makes the text non-selectable
                    // while translated, and makes it selectable again after translation.
                    mOriginalIsTextSelectable = textView.isTextSelectable();
                    if (mOriginalIsTextSelectable) {
                        // According to documentation for `setTextIsSelectable()`, it sets the
                        // flags focusable, focusableInTouchMode, clickable, and longClickable
                        // to the same value. We get the original values to restore when translation
                        // is hidden.
                        mOriginalFocusableInTouchMode = textView.isFocusableInTouchMode();
                        mOriginalFocusable = textView.getFocusable();
                        mOriginalClickable = textView.isClickable();
                        mOriginalLongClickable = textView.isLongClickable();
                        textView.setTextIsSelectable(false);
                    }

                    // TODO(b/233406028): We should NOT restore the original
                    //  TransformationMethod and selectable state if it was changed WHILE
                    //  translation was being shown.
                    textView.setTransformationMethod(transformation);
                });
        if (response.getKeys().contains(ViewTranslationRequest.ID_CONTENT_DESCRIPTION)) {
            CharSequence translatedContentDescription =
                    response.getValue(ViewTranslationRequest.ID_CONTENT_DESCRIPTION).getText();
            if (!TextUtils.isEmpty(translatedContentDescription)) {
                mContentDescription = view.getContentDescription();
                view.setContentDescription(translatedContentDescription);
            }
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onHideTranslation(@NonNull View view) {
        if (view.getViewTranslationResponse() == null) {
            Log.e(TAG, "onHideTranslation() shouldn't be called before "
                    + "onViewTranslationResponse().");
            return false;
        }
        // Restore to original text content.
        if (mTranslationTransformation != null) {
            final TransformationMethod transformation =
                    mTranslationTransformation.getOriginalTransformationMethod();
            TextView theTextView = (TextView) view;
            WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
            runChangeTextWithAnimationIfNeeded(
                    theTextView,
                    () -> {
                        mIsShowingTranslation = false;
                        mAnimationRunning = false;

                        TextView textView = textViewRef.get();
                        if (textView == null) {
                            return;
                        }
                        // TODO(b/233406028): We should NOT restore the original
                        //  TransformationMethod and selectable state if it was changed WHILE
                        //  translation was being shown.
                        textView.setTransformationMethod(transformation);

                        if (mOriginalIsTextSelectable && !textView.isTextSelectable()) {
                            // According to documentation for `setTextIsSelectable()`, it sets the
                            // flags focusable, focusableInTouchMode, clickable, and longClickable
                            // to the same value, and you must call `setFocusable()`, etc. to
                            // restore all previous flag values.
                            textView.setTextIsSelectable(true);
                            textView.setFocusableInTouchMode(mOriginalFocusableInTouchMode);
                            textView.setFocusable(mOriginalFocusable);
                            textView.setClickable(mOriginalClickable);
                            textView.setLongClickable(mOriginalLongClickable);
                        }
                    });
            if (!TextUtils.isEmpty(mContentDescription)) {
                view.setContentDescription(mContentDescription);
            }
        } else {
            if (DEBUG) {
                Log.w(TAG, "onHideTranslation(): no translated text.");
            }
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onClearTranslation(@NonNull View view) {
        // Restore to original text content and clear TranslationTransformation
        if (mTranslationTransformation != null) {
            onHideTranslation(view);
            clearTranslationTransformation();
            mPaddedText = null;
            mContentDescription = null;
        } else {
            if (DEBUG) {
                Log.w(TAG, "onClearTranslation(): no translated text.");
            }
            return false;
        }
        return true;
    }

    public boolean isShowingTranslation() {
        return mIsShowingTranslation;
    }

    /**
     * Returns whether the view is running animation to show or hide the translation.
     */
    public boolean isAnimationRunning() {
        return mAnimationRunning;
    }

    @Override
    public void enableContentPadding() {
        mIsTextPaddingEnabled = true;
    }

    /**
     * Returns whether readers of the view text should receive padded text for compatibility
     * reasons. The view's original text will be padded to match the length of the translated text.
     */
    boolean isTextPaddingEnabled() {
        return mIsTextPaddingEnabled;
    }

    /**
     * Returns the view's original text with padding added. If the translated text isn't longer than
     * the original text, returns the original text itself.
     *
     * @param text the view's original text
     * @param translatedText the view's translated text
     * @see #isTextPaddingEnabled()
     */
    @Nullable
    CharSequence getPaddedText(CharSequence text, CharSequence translatedText) {
        if (text == null) {
            return null;
        }
        if (mPaddedText == null) {
            mPaddedText = computePaddedText(text, translatedText);
        }
        return mPaddedText;
    }

    @NonNull
    private CharSequence computePaddedText(CharSequence text, CharSequence translatedText) {
        if (translatedText == null) {
            return text;
        }
        int newLength = translatedText.length();
        if (newLength <= text.length()) {
            return text;
        }
        StringBuilder sb = new StringBuilder(newLength);
        sb.append(text);
        for (int i = text.length(); i < newLength; i++) {
            sb.append(COMPAT_PAD_CHARACTER);
        }
        return sb;
    }

    private static final char COMPAT_PAD_CHARACTER = '\u2002';

    @Override
    public void setAnimationDurationMillis(int durationMillis) {
        mAnimationDurationMillis = durationMillis;
    }

    /**
     * Applies a simple text alpha animation when toggling between original and translated text. The
     * text is fully faded out, then swapped to the new text, then the fading is reversed.
     *
     * @param changeTextRunnable the operation to run on the view after the text is faded out, to
     * change to displaying the original or translated text.
     */
    private void runChangeTextWithAnimationIfNeeded(TextView view, Runnable changeTextRunnable) {
        boolean areAnimatorsEnabled = ValueAnimator.areAnimatorsEnabled();
        if (!areAnimatorsEnabled) {
            // The animation is disabled, just change display text
            changeTextRunnable.run();
            return;
        }
        if (mAnimator != null) {
            mAnimator.end();
            // Note: mAnimator is now null; do not use again here.
        }
        mAnimationRunning = true;
        int fadedOutColor = colorWithAlpha(view.getCurrentTextColor(), 0);
        mAnimator = ValueAnimator.ofArgb(view.getCurrentTextColor(), fadedOutColor);
        mAnimator.addUpdateListener(
                // Note that if the text has a ColorStateList, this replaces it with a single color
                // for all states. The original ColorStateList is restored when the animation ends
                // (see below).
                (valueAnimator) -> view.setTextColor((Integer) valueAnimator.getAnimatedValue()));
        mAnimator.setRepeatMode(ValueAnimator.REVERSE);
        mAnimator.setRepeatCount(1);
        mAnimator.setDuration(mAnimationDurationMillis);
        final ColorStateList originalColors = view.getTextColors();
        WeakReference<TextView> viewRef = new WeakReference<>(view);
        mAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                TextView view = viewRef.get();
                if (view != null) {
                    view.setTextColor(originalColors);
                }
                mAnimator = null;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                changeTextRunnable.run();
            }
        });
        mAnimator.start();
    }

    private ValueAnimator mAnimator;

    /**
     * Returns {@code color} with alpha changed to {@code newAlpha}
     */
    private static int colorWithAlpha(int color, int newAlpha) {
        return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
    }
}
