/*
 * Copyright (C) 2013 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.transitionseverywhere;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.graphics.Color;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;

import java.util.Map;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.transition.Transition;
import androidx.transition.TransitionListenerAdapter;
import androidx.transition.TransitionValues;

/**
 * This transition tracks changes to the text in TextView targets. If the text
 * changes between the start and end scenes, the transition ensures that the
 * starting text stays until the transition ends, at which point it changes
 * to the end text.  This is useful in situations where you want to resize a
 * text view to its new size before displaying the text that goes there.
 */
public class ChangeText extends Transition {

    private static final String PROPNAME_TEXT = "android:textchange:text";
    private static final String PROPNAME_TEXT_SELECTION_START =
            "android:textchange:textSelectionStart";
    private static final String PROPNAME_TEXT_SELECTION_END =
            "android:textchange:textSelectionEnd";
    private static final String PROPNAME_TEXT_COLOR = "android:textchange:textColor";

    private int mChangeBehavior = CHANGE_BEHAVIOR_KEEP;

    /**
     * Flag specifying that the text in affected/changing TextView targets will keep
     * their original text during the transition, setting it to the final text when
     * the transition ends. This is the default behavior.
     *
     * @see #setChangeBehavior(int)
     */
    public static final int CHANGE_BEHAVIOR_KEEP = 0;
    /**
     * Flag specifying that the text changing animation should first fade
     * out the original text completely. The new text is set on the target
     * view at the end of the fade-out animation. This transition is typically
     * used with a later {@link #CHANGE_BEHAVIOR_IN} transition, allowing more
     * flexibility than the {@link #CHANGE_BEHAVIOR_OUT_IN} by allowing other
     * transitions to be run sequentially or in parallel with these fades.
     *
     * @see #setChangeBehavior(int)
     */
    public static final int CHANGE_BEHAVIOR_OUT = 1;
    /**
     * Flag specifying that the text changing animation should fade in the
     * end text into the affected target view(s). This transition is typically
     * used in conjunction with an earlier {@link #CHANGE_BEHAVIOR_OUT}
     * transition, possibly with other transitions running as well, such as
     * a sequence to fade out, then resize the view, then fade in.
     *
     * @see #setChangeBehavior(int)
     */
    public static final int CHANGE_BEHAVIOR_IN = 2;
    /**
     * Flag specifying that the text changing animation should first fade
     * out the original text completely and then fade in the
     * new text.
     *
     * @see #setChangeBehavior(int)
     */
    public static final int CHANGE_BEHAVIOR_OUT_IN = 3;

    private static final String[] sTransitionProperties = {
            PROPNAME_TEXT,
            PROPNAME_TEXT_SELECTION_START,
            PROPNAME_TEXT_SELECTION_END
    };

    /**
     * Sets the type of changing animation that will be run, one of
     * {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT},
     * {@link #CHANGE_BEHAVIOR_IN}, and {@link #CHANGE_BEHAVIOR_OUT_IN}.
     *
     * @param changeBehavior The type of fading animation to use when this
     *                       transition is run.
     * @return this textChange object.
     */
    @NonNull
    public ChangeText setChangeBehavior(int changeBehavior) {
        if (changeBehavior >= CHANGE_BEHAVIOR_KEEP && changeBehavior <= CHANGE_BEHAVIOR_OUT_IN) {
            mChangeBehavior = changeBehavior;
        }
        return this;
    }

    @Nullable
    @Override
    public String[] getTransitionProperties() {
        return sTransitionProperties;
    }

    /**
     * Returns the type of changing animation that will be run.
     *
     * @return either {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT},
     * {@link #CHANGE_BEHAVIOR_IN}, or {@link #CHANGE_BEHAVIOR_OUT_IN}.
     */
    public int getChangeBehavior() {
        return mChangeBehavior;
    }

    private void captureValues(TransitionValues transitionValues) {
        if (transitionValues.view instanceof TextView) {
            TextView textview = (TextView) transitionValues.view;
            transitionValues.values.put(PROPNAME_TEXT, textview.getText());
            if (textview instanceof EditText) {
                transitionValues.values.put(PROPNAME_TEXT_SELECTION_START,
                        textview.getSelectionStart());
                transitionValues.values.put(PROPNAME_TEXT_SELECTION_END,
                        textview.getSelectionEnd());
            }
            if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
                transitionValues.values.put(PROPNAME_TEXT_COLOR, textview.getCurrentTextColor());
            }
        }
    }

    @Override
    public void captureStartValues(@NonNull TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(@NonNull TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Nullable
    @Override
    public Animator createAnimator(@NonNull ViewGroup sceneRoot, @Nullable TransitionValues startValues,
                                   @Nullable TransitionValues endValues) {
        if (startValues == null || endValues == null ||
                !(startValues.view instanceof TextView) || !(endValues.view instanceof TextView)) {
            return null;
        }
        final TextView view = (TextView) endValues.view;
        Map<String, Object> startVals = startValues.values;
        Map<String, Object> endVals = endValues.values;
        final CharSequence startText = startVals.get(PROPNAME_TEXT) != null ?
                (CharSequence) startVals.get(PROPNAME_TEXT) : "";
        final CharSequence endText = endVals.get(PROPNAME_TEXT) != null ?
                (CharSequence) endVals.get(PROPNAME_TEXT) : "";
        final int startSelectionStart, startSelectionEnd, endSelectionStart, endSelectionEnd;
        if (view instanceof EditText) {
            startSelectionStart = startVals.get(PROPNAME_TEXT_SELECTION_START) != null ?
                    (Integer) startVals.get(PROPNAME_TEXT_SELECTION_START) : -1;
            startSelectionEnd = startVals.get(PROPNAME_TEXT_SELECTION_END) != null ?
                    (Integer) startVals.get(PROPNAME_TEXT_SELECTION_END) : startSelectionStart;
            endSelectionStart = endVals.get(PROPNAME_TEXT_SELECTION_START) != null ?
                    (Integer) endVals.get(PROPNAME_TEXT_SELECTION_START) : -1;
            endSelectionEnd = endVals.get(PROPNAME_TEXT_SELECTION_END) != null ?
                    (Integer) endVals.get(PROPNAME_TEXT_SELECTION_END) : endSelectionStart;
        } else {
            startSelectionStart = startSelectionEnd = endSelectionStart = endSelectionEnd = -1;
        }
        if (!startText.equals(endText)) {
            final int startColor;
            final int endColor;
            if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
                view.setText(startText);
                if (view instanceof EditText) {
                    setSelection(((EditText) view), startSelectionStart, startSelectionEnd);
                }
            }
            Animator anim;
            if (mChangeBehavior == CHANGE_BEHAVIOR_KEEP) {
                startColor = endColor = 0;
                anim = ValueAnimator.ofFloat(0, 1);
                anim.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (startText.equals(view.getText())) {
                            // Only set if it hasn't been changed since anim started
                            view.setText(endText);
                            if (view instanceof EditText) {
                                setSelection(((EditText) view), endSelectionStart, endSelectionEnd);
                            }
                        }
                    }
                });
            } else {
                startColor = (Integer) startVals.get(PROPNAME_TEXT_COLOR);
                endColor = (Integer) endVals.get(PROPNAME_TEXT_COLOR);
                // Fade out start text
                ValueAnimator outAnim = null, inAnim = null;
                if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN ||
                        mChangeBehavior == CHANGE_BEHAVIOR_OUT) {
                    outAnim = ValueAnimator.ofInt(Color.alpha(startColor), 0);
                    outAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                            int currAlpha = (Integer) animation.getAnimatedValue();
                            view.setTextColor(currAlpha << 24 | startColor & 0xff0000 |
                                    startColor & 0xff00 | startColor & 0xff);
                        }
                    });
                    outAnim.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            if (startText.equals(view.getText())) {
                                // Only set if it hasn't been changed since anim started
                                view.setText(endText);
                                if (view instanceof EditText) {
                                    setSelection(((EditText) view), endSelectionStart,
                                            endSelectionEnd);
                                }
                            }
                            // restore opaque alpha and correct end color
                            view.setTextColor(endColor);
                        }
                    });
                }
                if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN ||
                        mChangeBehavior == CHANGE_BEHAVIOR_IN) {
                    inAnim = ValueAnimator.ofInt(0, Color.alpha(endColor));
                    inAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                            int currAlpha = (Integer) animation.getAnimatedValue();
                            view.setTextColor(currAlpha << 24 | Color.red(endColor) << 16 |
                                    Color.green(endColor) << 8 | Color.blue(endColor));
                        }
                    });
                    inAnim.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationCancel(Animator animation) {
                            // restore opaque alpha and correct end color
                            view.setTextColor(endColor);
                        }
                    });
                }
                if (outAnim != null && inAnim != null) {
                    anim = new AnimatorSet();
                    ((AnimatorSet) anim).playSequentially(outAnim, inAnim);
                } else if (outAnim != null) {
                    anim = outAnim;
                } else {
                    // Must be an in-only animation
                    anim = inAnim;
                }
            }
            TransitionListener transitionListener = new TransitionListenerAdapter() {
                int mPausedColor = 0;

                @Override
                public void onTransitionPause(@NonNull Transition transition) {
                    if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
                        view.setText(endText);
                        if (view instanceof EditText) {
                            setSelection(((EditText) view), endSelectionStart, endSelectionEnd);
                        }
                    }
                    if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
                        mPausedColor = view.getCurrentTextColor();
                        view.setTextColor(endColor);
                    }
                }

                @Override
                public void onTransitionResume(@NonNull Transition transition) {
                    if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
                        view.setText(startText);
                        if (view instanceof EditText) {
                            setSelection(((EditText) view), startSelectionStart, startSelectionEnd);
                        }
                    }
                    if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
                        view.setTextColor(mPausedColor);
                    }
                }

                @Override
                public void onTransitionEnd(@NonNull Transition transition) {
                    transition.removeListener(this);
                }
            };
            addListener(transitionListener);
            return anim;
        }
        return null;
    }

    private void setSelection(@NonNull EditText editText, int start, int end) {
        if (start >= 0 && end >= 0) {
            editText.setSelection(start, end);
        }
    }
}
