package com.vpaliy.loginconcept;


/*
 * Copyright (C) 2016 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.
 */


import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;

import com.transitionseverywhere.*;

import android.os.Build;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

/**
 * Transitions a TextView from one font size to another. This does not
 * do any animation of TextView content and if the text changes, this
 * transition will not run.
 * <p>
 * The animation works by capturing a bitmap of the text at the start
 * and end states. It then scales the start bitmap until it reaches
 * a threshold and switches to the scaled end bitmap for the remainder
 * of the animation. This keeps the jump in bitmaps in the middle of
 * the animation, where it is less noticeable than at the beginning
 * or end of the animation. This transition does not work well with
 * cropped text. TextResize also does not work with changes in
 * TextView gravity.
 */
public class TextSizeTransition extends Transition {
  private static final String FONT_SIZE = "TextResize:fontSize";
  private static final String DATA = "TextResize:data";

  private static final String[] PROPERTIES = {
          // We only care about FONT_SIZE. If anything else changes, we don't
          // want this transition to be called to create an Animator.
          FONT_SIZE,
  };

  public TextSizeTransition() {
    addTarget(TextView.class);
  }

  /**
   * Constructor used from XML.
   */
  public TextSizeTransition(Context context, AttributeSet attrs) {
    super(context, attrs);
    addTarget(TextView.class);
  }

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

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

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

  private void captureValues(TransitionValues transitionValues) {
    if (!(transitionValues.view instanceof TextView)) {
      return;
    }
    final TextView view = (TextView) transitionValues.view;
    final float fontSize = view.getTextSize();
    transitionValues.values.put(FONT_SIZE, fontSize);
    final TextResizeData data = new TextResizeData(view);
    transitionValues.values.put(DATA, data);
  }

  @Override
  public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
                                 TransitionValues endValues) {
    if (startValues == null || endValues == null) {
      return null;
    }

    final TextResizeData startData = (TextResizeData) startValues.values.get(DATA);
    final TextResizeData endData = (TextResizeData) endValues.values.get(DATA);
    if (startData.gravity != endData.gravity) {
      return null; // Can't deal with changes in gravity
    }

    final TextView textView = (TextView) endValues.view;
    float startFontSize = (Float) startValues.values.get(FONT_SIZE);
    // Capture the start bitmap -- we need to set the values to the start values first
    setTextViewData(textView, startData, startFontSize);
    final float startWidth = textView.getPaint().measureText(textView.getText().toString());

    final Bitmap startBitmap = captureTextBitmap(textView);

    if (startBitmap == null) {
      startFontSize = 0;
    }

    float endFontSize = (Float) endValues.values.get(FONT_SIZE);

    // Set the values to the end values
    setTextViewData(textView, endData, endFontSize);

    final float endWidth = textView.getPaint().measureText(textView.getText().toString());

    // Capture the end bitmap
    final Bitmap endBitmap = captureTextBitmap(textView);
    if (endBitmap == null) {
      endFontSize = 0;
    }

    if (startFontSize == 0 && endFontSize == 0) {
      return null; // Can't animate null bitmaps
    }

    // Set the colors of the TextView so that nothing is drawn.
    // Only draw the bitmaps in the overlay.
    final ColorStateList textColors = textView.getTextColors();
    final ColorStateList hintColors = textView.getHintTextColors();
    final int highlightColor = textView.getHighlightColor();
    final ColorStateList linkColors = textView.getLinkTextColors();
    textView.setTextColor(Color.TRANSPARENT);
    textView.setHintTextColor(Color.TRANSPARENT);
    textView.setHighlightColor(Color.TRANSPARENT);
    textView.setLinkTextColor(Color.TRANSPARENT);

    // Create the drawable that will be animated in the TextView's overlay.
    // Ensure that it is showing the start state now.
    final SwitchBitmapDrawable drawable = new SwitchBitmapDrawable(textView, startData.gravity,
            startBitmap, startFontSize, startWidth, endBitmap, endFontSize, endWidth);
    textView.getOverlay().add(drawable);

    // Properties: left, top, font size, text color
    final PropertyValuesHolder leftProp =
            PropertyValuesHolder.ofFloat("left", startData.paddingLeft, endData.paddingLeft);
    final PropertyValuesHolder topProp =
            PropertyValuesHolder.ofFloat("top", startData.paddingTop, endData.paddingTop);
    final PropertyValuesHolder rightProp = PropertyValuesHolder.ofFloat("right",
            startData.width - startData.paddingRight, endData.width - endData.paddingRight);
    final PropertyValuesHolder bottomProp = PropertyValuesHolder.ofFloat("bottom",
            startData.height - startData.paddingBottom, endData.height - endData.paddingBottom);
    final PropertyValuesHolder fontSizeProp =
            PropertyValuesHolder.ofFloat("fontSize", startFontSize, endFontSize);
    final ObjectAnimator animator;
    if (startData.textColor != endData.textColor) {
      final PropertyValuesHolder textColorProp = PropertyValuesHolder.ofObject("textColor",
              new ArgbEvaluator(), startData.textColor, endData.textColor);
      animator = ObjectAnimator.ofPropertyValuesHolder(drawable,
              leftProp, topProp, rightProp, bottomProp, fontSizeProp, textColorProp);
    } else {
      animator = ObjectAnimator.ofPropertyValuesHolder(drawable,
              leftProp, topProp, rightProp, bottomProp, fontSizeProp);
    }

    final float finalFontSize = endFontSize;
    AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
      @Override
      public void onAnimationEnd(Animator animation) {
        textView.getOverlay().remove(drawable);
        textView.setTextColor(textColors);
        textView.setHintTextColor(hintColors);
        textView.setHighlightColor(highlightColor);
        textView.setLinkTextColor(linkColors);
      }

      @Override
      public void onAnimationPause(Animator animation) {
        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawable.getFontSize());
        final int paddingLeft = Math.round(drawable.getLeft());
        final int paddingTop = Math.round(drawable.getTop());
        final float fraction = animator.getAnimatedFraction();
        final int paddingRight = Math.round(interpolate(startData.paddingRight,
                endData.paddingRight, fraction));
        final int paddingBottom = Math.round(interpolate(startData.paddingBottom,
                endData.paddingBottom, fraction));
        textView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
        textView.setTextColor(drawable.getTextColor());
      }

      @Override
      public void onAnimationResume(Animator animation) {
        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, finalFontSize);
        textView.setPadding(endData.paddingLeft, endData.paddingTop,
                endData.paddingRight, endData.paddingBottom);
        textView.setTextColor(endData.textColor);
      }
    };
    animator.addListener(listener);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) animator.addPauseListener(listener);
    return animator;
  }

  private static void setTextViewData(TextView view, TextResizeData data, float fontSize) {
    view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
    view.setPadding(data.paddingLeft, data.paddingTop, data.paddingRight, data.paddingBottom);
    view.setRight(view.getLeft() + data.width);
    view.setBottom(view.getTop() + data.height);
    view.setTextColor(data.textColor);
    int widthSpec = View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY);
    int heightSpec = View.MeasureSpec.makeMeasureSpec(view.getHeight(), View.MeasureSpec.EXACTLY);
    view.measure(widthSpec, heightSpec);
    view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
  }

  private static Bitmap captureTextBitmap(TextView textView) {
    Drawable background = textView.getBackground();
    textView.setBackground(null);
    int width = textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight();
    int height = textView.getHeight() - textView.getPaddingTop() - textView.getPaddingBottom();
    if (width == 0 || height == 0) {
      return null;
    }
    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.translate(-textView.getPaddingLeft(), -textView.getPaddingTop());
    textView.draw(canvas);
    textView.setBackground(background);
    return bitmap;
  }

  private static float interpolate(float start, float end, float fraction) {
    return start + (fraction * (end - start));
  }

  /**
   * This Drawable is used to scale the start and end bitmaps and switch between them
   * at the appropriate progress.
   */
  private static class SwitchBitmapDrawable extends Drawable {
    private final TextView view;
    private final int horizontalGravity;
    private final int verticalGravity;
    private final Bitmap startBitmap;
    private final Bitmap endBitmap;
    private final Paint paint = new Paint();
    private final float startFontSize;
    private final float endFontSize;
    private final float startWidth;
    private final float endWidth;
    private float fontSize;
    private float left;
    private float top;
    private float right;
    private float bottom;
    private int textColor;

    public SwitchBitmapDrawable(TextView view, int gravity,
                                Bitmap startBitmap, float startFontSize, float startWidth,
                                Bitmap endBitmap, float endFontSize, float endWidth) {
      this.view = view;
      this.horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
      this.verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
      this.startBitmap = startBitmap;
      this.endBitmap = endBitmap;
      this.startFontSize = startFontSize;
      this.endFontSize = endFontSize;
      this.startWidth = startWidth;
      this.endWidth = endWidth;
    }

    @Override
    public void invalidateSelf() {
      super.invalidateSelf();
      view.invalidate();
    }

    /**
     * Sets the font size that the text should be displayed at.
     *
     * @param fontSize The font size in pixels of the scaled bitmap text.
     */
    public void setFontSize(float fontSize) {
      this.fontSize = fontSize;
      invalidateSelf();
    }

    /**
     * Sets the color of the text to be displayed.
     *
     * @param textColor The color of the text to be displayed.
     */
    public void setTextColor(int textColor) {
      this.textColor = textColor;
      setColorFilter(textColor, PorterDuff.Mode.SRC_IN);
      invalidateSelf();
    }

    /**
     * Sets the left side of the text. This should be the same as the left padding.
     *
     * @param left The left side of the text in pixels.
     */
    public void setLeft(float left) {
      this.left = left;
      invalidateSelf();
    }

    /**
     * Sets the top of the text. This should be the same as the top padding.
     *
     * @param top The top of the text in pixels.
     */
    public void setTop(float top) {
      this.top = top;
      invalidateSelf();
    }

    /**
     * Sets the right of the drawable.
     *
     * @param right The right pixel of the drawn area.
     */
    public void setRight(float right) {
      this.right = right;
      invalidateSelf();
    }

    /**
     * Sets the bottom of the drawable.
     *
     * @param bottom The bottom pixel of the drawn area.
     */
    public void setBottom(float bottom) {
      this.bottom = bottom;
      invalidateSelf();
    }

    /**
     * @return The left side of the text.
     */
    public float getLeft() {
      return left;
    }

    /**
     * @return The top of the text.
     */
    public float getTop() {
      return top;
    }

    /**
     * @return The right side of the text.
     */
    public float getRight() {
      return right;
    }

    /**
     * @return The bottom of the text.
     */
    public float getBottom() {
      return bottom;
    }

    /**
     * @return The font size of the text in the displayed bitmap.
     */
    public float getFontSize() {
      return fontSize;
    }

    /**
     * @return The color of the text being displayed.
     */
    public int getTextColor() {
      return textColor;
    }

    @Override
    public void draw(Canvas canvas) {
      int saveCount = canvas.save();
      // The threshold changes depending on the target font sizes. Because scaled-up
      // fonts look bad, we want to switch when closer to the smaller font size. This
      // algorithm ensures that null bitmaps (font size = 0) are never used.
      final float threshold = startFontSize / (startFontSize + endFontSize);
      final float fontSize = getFontSize();
      final float progress = (fontSize - startFontSize) / (endFontSize - startFontSize);

      // The drawn text width is a more accurate scale than font size. This avoids
      // jump when switching bitmaps.
      final float expectedWidth = interpolate(startWidth, endWidth, progress);
      if (progress < threshold) {
        // draw start bitmap
        final float scale = expectedWidth / startWidth;
        float tx = getTranslationPoint(horizontalGravity, left, right,
                startBitmap.getWidth(), scale);
        float ty = getTranslationPoint(verticalGravity, top, bottom,
                startBitmap.getHeight(), scale);
        canvas.translate(tx, ty);
        canvas.scale(scale, scale);
        canvas.drawBitmap(startBitmap, 0, 0, paint);
      } else {
        // draw end bitmap
        final float scale = expectedWidth / endWidth;
        float tx = getTranslationPoint(horizontalGravity, left, right,
                endBitmap.getWidth(), scale);
        float ty = getTranslationPoint(verticalGravity, top, bottom,
                endBitmap.getHeight(), scale);
        canvas.translate(tx, ty);
        canvas.scale(scale, scale);
        canvas.drawBitmap(endBitmap, 0, 0, paint);
      }
      canvas.restoreToCount(saveCount);
    }

    @Override
    public void setAlpha(int alpha) {
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
      paint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
      return PixelFormat.TRANSLUCENT;
    }

    private float getTranslationPoint(int gravity, float start, float end, float dim,
                                      float scale) {
      switch (gravity) {
        case Gravity.CENTER_HORIZONTAL:
        case Gravity.CENTER_VERTICAL:
          return ((start + end) - (dim * scale)) / 2f;
        case Gravity.RIGHT:
        case Gravity.BOTTOM:
          return end - (dim * scale);
        case Gravity.LEFT:
        case Gravity.TOP:
        default:
          return start;
      }
    }
  }

  /**
   * Contains all the non-font-size data used by the TextResize transition.
   * None of these values should trigger the transition, so they are not listed
   * in PROPERTIES. These are captured together to avoid boxing of all the
   * primitives while adding to TransitionValues.
   */
  static class TextResizeData {
    public final int paddingLeft;
    public final int paddingTop;
    public final int paddingRight;
    public final int paddingBottom;
    public final int width;
    public final int height;
    public final int gravity;
    public final int textColor;

    public TextResizeData(TextView textView) {
      this.paddingLeft = textView.getPaddingLeft();
      this.paddingTop = textView.getPaddingTop();
      this.paddingRight = textView.getPaddingRight();
      this.paddingBottom = textView.getPaddingBottom();
      this.width = textView.getWidth();
      this.height = textView.getHeight();
      this.gravity = textView.getGravity();
      this.textColor = textView.getCurrentTextColor();
    }
  }
}