/**
 * Copyright 2016 Keepsafe Software, Inc.
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 ui.apptour.taptargetview;

import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.DynamicLayout;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewManager;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Button;
import android.widget.RelativeLayout;
import java.util.Locale;

/**
 * TapTargetView implements a feature discovery paradigm following Google's Material Design
 * guidelines.
 * <p>
 * This class should not be instantiated directly. Instead, please use the
 * {@link #showFor(Activity, TapTarget, Listener)} static factory method instead.
 * <p>
 * More information can be found here:
 * https://material.google.com/growth-communications/feature-discovery.html#feature-discovery-design
 */
@SuppressLint("ViewConstructor")
public class TapTargetView extends View {
    private boolean isDismissed = false;
    private boolean isDismissing = false;
    private boolean isInteractable = true;

    final int TARGET_PADDING;
    final int TARGET_RADIUS;
    final int TARGET_PULSE_RADIUS;
    final int TEXT_PADDING;
    final int TEXT_SPACING;
    final int TEXT_MAX_WIDTH;
    final int TEXT_POSITIONING_BIAS;
    final int CIRCLE_PADDING;
    final int GUTTER_DIM;
    final int SHADOW_DIM;
    final int SHADOW_JITTER_DIM;

    @Nullable
    final ViewGroup boundingParent;
    final ViewManager parent;
    final TapTarget target;
    final Rect targetBounds;

    final TextPaint titlePaint;
    final TextPaint descriptionPaint;
    final TextPaint buttonTextPaint;
    final Paint buttonBGPaint;
    final Paint outerCirclePaint;
    final Paint outerCircleShadowPaint;
    final Paint targetCirclePaint;
    final Paint targetCirclePulsePaint;

    CharSequence title;
    @Nullable
    StaticLayout titleLayout;
    @Nullable
    CharSequence description;
    @Nullable
    StaticLayout descriptionLayout;
    @Nullable
    CharSequence buttonText;
    @Nullable
    StaticLayout buttonLayout;

    boolean buttonVisible;
    boolean isDark;
    boolean debug;
    boolean shouldTintTarget;
    boolean shouldDrawShadow;
    boolean cancelable;
    boolean visible;
    int cornersRadius;

    // Debug related variables
    @Nullable
    SpannableStringBuilder debugStringBuilder;
    @Nullable
    DynamicLayout debugLayout;
    @Nullable
    TextPaint debugTextPaint;
    @Nullable
    Paint debugPaint;

    // Drawing properties
    Rect drawingBounds;
    Rect textBounds;
    Rect textBoundsButton;

    Path outerCirclePath;
    float outerCircleRadius;
    int calculatedOuterCircleRadius;
    int[] outerCircleCenter;
    int outerCircleAlpha;

    float targetCirclePulseRadius;
    int targetCirclePulseAlpha;

    float targetCircleRadius;
    int targetCircleAlpha;

    int textAlpha;
    int dimColor;

    float lastTouchX;
    float lastTouchY;

    int topBoundary;
    int bottomBoundary;

    Bitmap tintedTarget;

    Listener listener;

    @Nullable
    ViewOutlineProvider outlineProvider;

    public static TapTargetView showFor(Activity activity, TapTarget target) {
        return showFor(activity, target, null);
    }

    public static TapTargetView showFor(Activity activity, TapTarget target, Listener listener) {
        if (activity == null) throw new IllegalArgumentException("Activity is null");

        final ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
        final ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        final ViewGroup content = (ViewGroup) decor.findViewById(android.R.id.content);
        final TapTargetView tapTargetView = new TapTargetView(activity, decor, content, target, listener);
        decor.addView(tapTargetView, layoutParams);

        return tapTargetView;
    }

    public static TapTargetView showFor(Dialog dialog, TapTarget target) {
        return showFor(dialog, target, null);
    }

    public static TapTargetView showFor(Dialog dialog, TapTarget target, Listener listener) {
        if (dialog == null) throw new IllegalArgumentException("Dialog is null");

        final Context context = dialog.getContext();
        final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
        params.format = PixelFormat.RGBA_8888;
        params.flags = 0;
        params.gravity = Gravity.START | Gravity.TOP;
        params.x = 0;
        params.y = 0;
        params.width = WindowManager.LayoutParams.MATCH_PARENT;
        params.height = WindowManager.LayoutParams.MATCH_PARENT;

        final TapTargetView tapTargetView = new TapTargetView(context, windowManager, null, target, listener);
        windowManager.addView(tapTargetView, params);

        return tapTargetView;
    }

    public static class Listener {
        /**
         * Signals that the user has clicked inside of the target
         **/
        public void onTargetClick(TapTargetView view) {
            view.dismiss(true);
        }

        /**
         * Signals that the user has long clicked inside of the target
         **/
        public void onTargetLongClick(TapTargetView view) {
            onTargetClick(view);
        }

        /**
         * If cancelable, signals that the user has clicked outside of the outer circle
         **/
        public void onTargetCancel(TapTargetView view) {
            view.dismiss(false);
        }

        /**
         * Signals that the user clicked on the outer circle portion of the tap target
         **/
        public void onOuterCircleClick(TapTargetView view) {
            // no-op as default
        }

        public void onButtonClick(TapTargetView view) {
            view.dismiss(true);
        }

        /**
         * Signals that the tap target has been dismissed
         *
         * @param userInitiated Whether the user caused this action
         */
        public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
        }
    }

    final FloatValueAnimatorBuilder.UpdateListener expandContractUpdateListener = new FloatValueAnimatorBuilder.UpdateListener() {
        @Override
        public void onUpdate(float lerpTime) {
            final float newOuterCircleRadius = calculatedOuterCircleRadius * lerpTime;
            final boolean expanding = newOuterCircleRadius > outerCircleRadius;
            if (!expanding) {
                // When contracting we need to invalidate the old drawing bounds. Otherwise
                // you will see artifacts as the circle gets smaller
                calculateDrawingBounds();
            }

            final float targetAlpha = target.outerCircleAlpha * 255;
            outerCircleRadius = newOuterCircleRadius;
            outerCircleAlpha = (int) Math.min(targetAlpha, (lerpTime * 1.5f * targetAlpha));
            outerCirclePath.reset();
            outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);

            targetCircleAlpha = (int) Math.min(255.0f, (lerpTime * 1.5f * 255.0f));

            if (expanding) {
                targetCircleRadius = TARGET_RADIUS * Math.min(1.0f, lerpTime * 1.5f);
            } else {
                targetCircleRadius = TARGET_RADIUS * lerpTime;
                targetCirclePulseRadius *= lerpTime;
            }

            textAlpha = (int) (delayedLerp(lerpTime, 0.7f) * 255);

            if (expanding) {
                calculateDrawingBounds();
            }

            invalidateViewAndOutline(drawingBounds);
        }
    };

    final ValueAnimator expandAnimation = new FloatValueAnimatorBuilder()
            .duration(250)
            .delayBy(250)
            .interpolator(new AccelerateDecelerateInterpolator())
            .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
                @Override
                public void onUpdate(float lerpTime) {
                    expandContractUpdateListener.onUpdate(lerpTime);
                }
            })
            .onEnd(new FloatValueAnimatorBuilder.EndListener() {
                @Override
                public void onEnd() {
                    pulseAnimation.start();
                    isInteractable = true;
                }
            })
            .build();

    final ValueAnimator pulseAnimation = new FloatValueAnimatorBuilder()
            .duration(1000)
            .repeat(ValueAnimator.INFINITE)
            .interpolator(new AccelerateDecelerateInterpolator())
            .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
                @Override
                public void onUpdate(float lerpTime) {
                    final float pulseLerp = delayedLerp(lerpTime, 0.5f);
                    targetCirclePulseRadius = (1.0f + pulseLerp) * TARGET_RADIUS;
                    targetCirclePulseAlpha = (int) ((1.0f - pulseLerp) * 255);
                    targetCircleRadius = TARGET_RADIUS + halfwayLerp(lerpTime) * TARGET_PULSE_RADIUS;

                    if (outerCircleRadius != calculatedOuterCircleRadius) {
                        outerCircleRadius = calculatedOuterCircleRadius;
                    }

                    calculateDrawingBounds();
                    invalidateViewAndOutline(drawingBounds);
                }
            })
            .build();

    final ValueAnimator dismissAnimation = new FloatValueAnimatorBuilder(true)
            .duration(250)
            .interpolator(new AccelerateDecelerateInterpolator())
            .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
                @Override
                public void onUpdate(float lerpTime) {
                    expandContractUpdateListener.onUpdate(lerpTime);
                }
            })
            .onEnd(new FloatValueAnimatorBuilder.EndListener() {
                @Override
                public void onEnd() {
                    finishDismiss(true);
                }
            })
            .build();

    private final ValueAnimator dismissConfirmAnimation = new FloatValueAnimatorBuilder()
            .duration(250)
            .interpolator(new AccelerateDecelerateInterpolator())
            .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
                @Override
                public void onUpdate(float lerpTime) {
                    final float spedUpLerp = Math.min(1.0f, lerpTime * 2.0f);
                    outerCircleRadius = calculatedOuterCircleRadius * (1.0f + (spedUpLerp * 0.2f));
                    outerCircleAlpha = (int) ((1.0f - spedUpLerp) * target.outerCircleAlpha * 255.0f);
                    outerCirclePath.reset();
                    outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);
                    targetCircleRadius = (1.0f - lerpTime) * TARGET_RADIUS;
                    targetCircleAlpha = (int) ((1.0f - lerpTime) * 255.0f);
                    targetCirclePulseRadius = (1.0f + lerpTime) * TARGET_RADIUS;
                    targetCirclePulseAlpha = (int) ((1.0f - lerpTime) * targetCirclePulseAlpha);
                    textAlpha = (int) ((1.0f - spedUpLerp) * 255.0f);
                    calculateDrawingBounds();
                    invalidateViewAndOutline(drawingBounds);
                }
            })
            .onEnd(new FloatValueAnimatorBuilder.EndListener() {
                @Override
                public void onEnd() {
                    finishDismiss(true);
                }
            })
            .build();

    private ValueAnimator[] animators = new ValueAnimator[]
            {expandAnimation, pulseAnimation, dismissConfirmAnimation, dismissAnimation};

    private final ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;

    /**
     * This constructor should only be used directly for very specific use cases not covered by
     * the static factory methods.
     *
     * @param context        The host context
     * @param parent         The parent that this TapTargetView will become a child of. This parent should
     *                       allow the largest possible area for this view to utilize
     * @param boundingParent Optional. Will be used to calculate boundaries if needed. For example,
     *                       if your view is added to the decor view of your Window, then you want
     *                       to adjust for system ui like the navigation bar or status bar, and so
     *                       you would pass in the content view (which doesn't include system ui)
     *                       here.
     * @param target         The {@link TapTarget} to target
     * @param userListener   Optional. The {@link Listener} instance for this view
     */
    public TapTargetView(final Context context,
                         final ViewManager parent,
                         @Nullable final ViewGroup boundingParent,
                         final TapTarget target,
                         @Nullable final Listener userListener) {
        super(context);
        if (target == null) throw new IllegalArgumentException("Target cannot be null");

        this.target = target;
        this.parent = parent;
        this.boundingParent = boundingParent;
        this.listener = userListener != null ? userListener : new Listener();
        this.title = target.title;
        this.description = target.description;
        if (target.buttonText != null) {
            this.buttonText =  "  " + target.buttonText + "  ";
        } else {
            this.buttonText = "  OK  ";
        }
        this.buttonVisible = target.buttonVisible;
        this.cornersRadius = target.cornersRadius;

        TARGET_PADDING = UiUtil.dp(context, 20);
        CIRCLE_PADDING = UiUtil.dp(context, 40);
        TARGET_RADIUS = UiUtil.dp(context, target.targetRadius);
        TEXT_PADDING = UiUtil.dp(context, 40);
        TEXT_SPACING = UiUtil.dp(context, 8);
        TEXT_MAX_WIDTH = UiUtil.dp(context, 360);
        TEXT_POSITIONING_BIAS = UiUtil.dp(context, 20);
        GUTTER_DIM = UiUtil.dp(context, 88);
        SHADOW_DIM = UiUtil.dp(context, 8);
        SHADOW_JITTER_DIM = UiUtil.dp(context, 1);
        TARGET_PULSE_RADIUS = (int) (0.1f * TARGET_RADIUS);

        outerCirclePath = new Path();
        targetBounds = new Rect();
        drawingBounds = new Rect();

        AssetManager am = context.getApplicationContext().getAssets();

        titlePaint = new TextPaint();
        titlePaint.setTextSize(target.titleTextSizePx(context));
        titlePaint.setTypeface(Typeface.createFromAsset(am,String.format(Locale.US, "fonts/%s", "SukhumvitSet-Bold.ttf")));
        titlePaint.setAntiAlias(true);

        descriptionPaint = new TextPaint();
        descriptionPaint.setTextSize(target.descriptionTextSizePx(context));
        descriptionPaint.setTypeface(Typeface.createFromAsset(am,String.format(Locale.US, "fonts/%s", "SukhumvitSet-Medium.ttf")));
        descriptionPaint.setAntiAlias(true);
        descriptionPaint.setAlpha((int) (0.54f * 255.0f));

        buttonBGPaint = new Paint();
        buttonBGPaint.setAntiAlias(true);
        buttonBGPaint.setStrokeWidth(10);
        buttonBGPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        buttonTextPaint = new TextPaint();
        buttonTextPaint.setTextSize(target.buttonTextSizePx(context));
        buttonTextPaint.setTypeface(Typeface.createFromAsset(am,String.format(Locale.US, "fonts/%s", "SukhumvitSet-Bold.ttf")));
        buttonTextPaint.setAntiAlias(true);

        outerCirclePaint = new Paint();
        outerCirclePaint.setAntiAlias(true);
        outerCirclePaint.setAlpha((int) (target.outerCircleAlpha * 255.0f));

        outerCircleShadowPaint = new Paint();
        outerCircleShadowPaint.setAntiAlias(true);
        outerCircleShadowPaint.setAlpha(50);
        outerCircleShadowPaint.setStyle(Paint.Style.STROKE);
        outerCircleShadowPaint.setStrokeWidth(SHADOW_JITTER_DIM);
        outerCircleShadowPaint.setColor(Color.BLACK);

        targetCirclePaint = new Paint();
        targetCirclePaint.setAntiAlias(true);

        targetCirclePulsePaint = new Paint();
        targetCirclePulsePaint.setAntiAlias(true);

        applyTargetOptions(context);

        globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (isDismissing) {
                    return;
                }
                updateTextLayouts();
                target.onReady(new Runnable() {
                    @Override
                    public void run() {
                        final int[] offset = new int[2];

                        targetBounds.set(target.bounds());

                        getLocationOnScreen(offset);
                        targetBounds.offset(-offset[0], -offset[1]);

                        if (boundingParent != null) {
                            final WindowManager windowManager
                                    = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
                            final DisplayMetrics displayMetrics = new DisplayMetrics();
                            windowManager.getDefaultDisplay().getMetrics(displayMetrics);

                            final Rect rect = new Rect();
                            boundingParent.getWindowVisibleDisplayFrame(rect);

                            // We bound the boundaries to be within the screen's coordinates to
                            // handle the case where the layout bounds do not match
                            // (like when FLAG_LAYOUT_NO_LIMITS is specified)
                            topBoundary = Math.max(0, rect.top);
                            bottomBoundary = Math.min(rect.bottom, displayMetrics.heightPixels);
                        }

                        drawTintedTarget();
                        requestFocus();
                        calculateDimensions();

                        startExpandAnimation();
                    }
                });
            }
        };

        getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);

        setFocusableInTouchMode(true);
        setClickable(true);
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (listener == null || outerCircleCenter == null || !isInteractable) return;

                final boolean clickedInTarget =
                        distance(targetBounds.centerX(), targetBounds.centerY(), (int) lastTouchX, (int) lastTouchY) <= targetCircleRadius;

                final boolean clickButton = (lastTouchX <= textBoundsButton.right &&
                        lastTouchX >= textBoundsButton.left &&
                        lastTouchY >= textBoundsButton.top &&
                        lastTouchY <= textBoundsButton.bottom);


                final double distanceToOuterCircleCenter = distance(outerCircleCenter[0], outerCircleCenter[1],
                        (int) lastTouchX, (int) lastTouchY);
                final boolean clickedInsideOfOuterCircle = distanceToOuterCircleCenter <= outerCircleRadius;

                if (clickedInTarget || clickButton) {
                    isInteractable = false;
                    listener.onTargetClick(TapTargetView.this);
                } else if (clickedInsideOfOuterCircle && !clickButton) {
                    listener.onOuterCircleClick(TapTargetView.this);
                } else if (cancelable) {
                    isInteractable = false;
                    listener.onTargetCancel(TapTargetView.this);
                }
            }
        });

        setOnLongClickListener(new OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (listener == null) return false;

                if (targetBounds.contains((int) lastTouchX, (int) lastTouchY)) {
                    listener.onTargetLongClick(TapTargetView.this);
                    return true;
                }

                return false;
            }
        });
    }

    private void startExpandAnimation() {
        if (!visible) {
            isInteractable = false;
            expandAnimation.start();
            visible = true;
        }
    }

    protected void applyTargetOptions(Context context) {
        shouldTintTarget = target.tintTarget;
        shouldDrawShadow = target.drawShadow;
        cancelable = target.cancelable;

        // We can't clip out portions of a view outline, so if the user specified a transparent
        // target, we need to fallback to drawing a jittered shadow approximation
        if (shouldDrawShadow && Build.VERSION.SDK_INT >= 21 && !target.transparentTarget) {
            outlineProvider = new ViewOutlineProvider() {
                @TargetApi(Build.VERSION_CODES.LOLLIPOP)
                @Override
                public void getOutline(View view, Outline outline) {
                    if (outerCircleCenter == null) return;
                    outline.setOval(
                            (int) (outerCircleCenter[0] - outerCircleRadius), (int) (outerCircleCenter[1] - outerCircleRadius),
                            (int) (outerCircleCenter[0] + outerCircleRadius), (int) (outerCircleCenter[1] + outerCircleRadius));
                    outline.setAlpha(outerCircleAlpha / 255.0f);
                    if (Build.VERSION.SDK_INT >= 22) {
                        outline.offset(0, SHADOW_DIM);
                    }
                }
            };

            setOutlineProvider(outlineProvider);
            setElevation(SHADOW_DIM);
        }

        if (shouldDrawShadow && outlineProvider == null && Build.VERSION.SDK_INT < 18) {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        } else {
            setLayerType(LAYER_TYPE_HARDWARE, null);
        }

        final Resources.Theme theme = context.getTheme();
        isDark = UiUtil.themeIntAttr(context, "isLightTheme") == 0;

        final Integer outerCircleColor = target.outerCircleColorInt(context);
        if (outerCircleColor != null) {
            outerCirclePaint.setColor(outerCircleColor);
        } else if (theme != null) {
            outerCirclePaint.setColor(UiUtil.themeIntAttr(context, "colorPrimary"));
        } else {
            outerCirclePaint.setColor(Color.WHITE);
        }

        final Integer targetCircleColor = target.targetCircleColorInt(context);
        if (targetCircleColor != null) {
            targetCirclePaint.setColor(targetCircleColor);
        } else {
            targetCirclePaint.setColor(isDark ? Color.BLACK : Color.WHITE);
        }

        if (target.transparentTarget) {
            targetCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        }

        targetCirclePulsePaint.setColor(targetCirclePaint.getColor());

        final Integer targetDimColor = target.dimColorInt(context);
        if (targetDimColor != null) {
            dimColor = UiUtil.setAlpha(targetDimColor, 0.3f);
        } else {
            dimColor = -1;
        }

        final Integer titleTextColor = target.titleTextColorInt(context);
        if (titleTextColor != null) {
            titlePaint.setColor(titleTextColor);
        } else {
            titlePaint.setColor(isDark ? Color.BLACK : Color.WHITE);
        }

        final Integer descriptionTextColor = target.descriptionTextColorInt(context);
        if (descriptionTextColor != null) {
            descriptionPaint.setColor(descriptionTextColor);
        } else {
            descriptionPaint.setColor(titlePaint.getColor());
        }

        final Integer buttonTextColor = target.buttonTextColorInt(context);
        if (buttonTextColor != null) {
            buttonTextPaint.setColor(buttonTextColor);
        } else {
            buttonTextPaint.setColor(Color.WHITE);
        }

        final Integer buttonBGTextColor = target.buttonBGColorInt(context);
        if (buttonBGTextColor != null) {
            buttonBGPaint.setColor(buttonBGTextColor);
        } else {
            buttonBGPaint.setColor(Color.RED);
        }

        if (target.titleTypeface != null) {
            titlePaint.setTypeface(target.titleTypeface);
        }

        if (target.descriptionTypeface != null) {
            descriptionPaint.setTypeface(target.descriptionTypeface);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        onDismiss(false);
    }

    void onDismiss(boolean userInitiated) {
        if (isDismissed) return;

        isDismissing = false;
        isDismissed = true;

        for (final ValueAnimator animator : animators) {
            animator.cancel();
            animator.removeAllUpdateListeners();
        }

        ViewUtil.removeOnGlobalLayoutListener(getViewTreeObserver(), globalLayoutListener);
        visible = false;

        if (listener != null) {
            listener.onTargetDismissed(this, userInitiated);
        }
    }

    @Override
    protected void onDraw(Canvas c) {
        if (isDismissed || outerCircleCenter == null) return;

        if (topBoundary > 0 && bottomBoundary > 0) {
            c.clipRect(0, topBoundary, getWidth(), bottomBoundary);
        }

        if (dimColor != -1) {
            c.drawColor(dimColor);
        }

        int saveCount;
        outerCirclePaint.setAlpha(outerCircleAlpha);
        if (shouldDrawShadow && outlineProvider == null) {
            saveCount = c.save();
            {
                c.clipPath(outerCirclePath, Region.Op.DIFFERENCE);
                drawJitteredShadow(c);
            }
            c.restoreToCount(saveCount);
        }
        c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, outerCirclePaint);

        targetCirclePaint.setAlpha(targetCircleAlpha);
        if (targetCirclePulseAlpha > 0) {
            targetCirclePulsePaint.setAlpha(targetCirclePulseAlpha);
            c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
                    targetCirclePulseRadius, targetCirclePulsePaint);
        }
        c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
                targetCircleRadius, targetCirclePaint);

        saveCount = c.save();
        {
            c.translate(textBounds.left, textBounds.top);
            titlePaint.setAlpha(textAlpha);
            if (titleLayout != null) {
                titleLayout.draw(c);
            }

            if (descriptionLayout != null && titleLayout != null) {
                c.translate(0, titleLayout.getHeight() + TEXT_SPACING);
                descriptionPaint.setAlpha((int) (target.descriptionTextAlpha * textAlpha));
                descriptionLayout.draw(c);
            }

            if (buttonLayout != null && descriptionLayout != null && titleLayout != null && buttonVisible) {
                c.translate(0, descriptionLayout.getHeight() + TEXT_SPACING);
                buttonTextPaint.setAlpha(textAlpha);
                buttonBGPaint.setAlpha(textAlpha);
                Paint p = new Paint();
                p.setTextSize((float) target.buttonTextSizePx(this.getContext()));
                float w = p.measureText(buttonText, 0, buttonText.length());
                int width = (int) Math.ceil(w);
                if (Build.VERSION.SDK_INT >= 21) {
                    c.drawRoundRect(-8, -8, width + 16, buttonLayout.getHeight() + 8, cornersRadius, cornersRadius, buttonBGPaint);
                } else {
                    c.drawRect(-8, -8, width + 16, buttonLayout.getHeight() + 8, buttonBGPaint);
                }
                buttonLayout.draw(c);
            } else if (buttonLayout != null && titleLayout != null && descriptionLayout == null && buttonVisible) {
                c.translate(0, titleLayout.getHeight() + TEXT_SPACING);
                buttonTextPaint.setAlpha(textAlpha);
                buttonBGPaint.setAlpha(textAlpha);
                Paint p = new Paint();
                p.setTextSize((float) target.buttonTextSizePx(this.getContext()));
                float w = p.measureText(buttonText, 0, buttonText.length());
                int width = (int) Math.ceil(w);
                if (Build.VERSION.SDK_INT >= 21) {
                    c.drawRoundRect(-8, -8, width + 16, buttonLayout.getHeight() + 8, cornersRadius, cornersRadius, buttonBGPaint);
                } else {
                    c.drawRect(-8, -8, width + 16, buttonLayout.getHeight() + 8, buttonBGPaint);
                }
                buttonLayout.draw(c);
            } else {
                buttonTextPaint.setAlpha(0);
            }
        }
        c.restoreToCount(saveCount);

        saveCount = c.save();
        {
            if (tintedTarget != null) {
                c.translate(targetBounds.centerX() - tintedTarget.getWidth() / 2,
                        targetBounds.centerY() - tintedTarget.getHeight() / 2);
                c.drawBitmap(tintedTarget, 0, 0, targetCirclePaint);
            } else if (target.icon != null) {
                c.translate(targetBounds.centerX() - target.icon.getBounds().width() / 2,
                        targetBounds.centerY() - target.icon.getBounds().height() / 2);
                target.icon.setAlpha(targetCirclePaint.getAlpha());
                target.icon.draw(c);
            }
        }
        c.restoreToCount(saveCount);

        if (debug) {
            drawDebugInformation(c);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        lastTouchX = e.getX();
        lastTouchY = e.getY();
        return super.onTouchEvent(e);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (isVisible() && cancelable && keyCode == KeyEvent.KEYCODE_BACK) {
            event.startTracking();
            return true;
        }

        return false;
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (isVisible() && isInteractable && cancelable
                && keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
            isInteractable = false;

            if (listener != null) {
                listener.onTargetCancel(this);
            } else {
                new Listener().onTargetCancel(this);
            }

            return true;
        }

        return false;
    }

    /**
     * Dismiss this view
     *
     * @param tappedTarget If the user tapped the target or not
     *                     (results in different dismiss animations)
     */
    public void dismiss(boolean tappedTarget) {
        isDismissing = true;
        pulseAnimation.cancel();
        expandAnimation.cancel();
        if (!visible) {
            finishDismiss(tappedTarget);
            return;
        }
        if (tappedTarget) {
            dismissConfirmAnimation.start();
        } else {
            dismissAnimation.start();
        }
    }

    private void finishDismiss(boolean userInitiated) {
        onDismiss(userInitiated);
        ViewUtil.removeView(parent, TapTargetView.this);
    }

    /**
     * Specify whether to draw a wireframe around the view, useful for debugging
     **/
    public void setDrawDebug(boolean status) {
        if (debug != status) {
            debug = status;
            postInvalidate();
        }
    }

    /**
     * Returns whether this view is visible or not
     **/
    public boolean isVisible() {
        return !isDismissed && visible;
    }

    void drawJitteredShadow(Canvas c) {
        final float baseAlpha = 0.20f * outerCircleAlpha;
        outerCircleShadowPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        outerCircleShadowPaint.setAlpha((int) baseAlpha);
        c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM, outerCircleRadius, outerCircleShadowPaint);
        outerCircleShadowPaint.setStyle(Paint.Style.STROKE);
        final int numJitters = 7;
        for (int i = numJitters - 1; i > 0; --i) {
            outerCircleShadowPaint.setAlpha((int) ((i / (float) numJitters) * baseAlpha));
            c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM,
                    outerCircleRadius + (numJitters - i) * SHADOW_JITTER_DIM, outerCircleShadowPaint);
        }
    }

    void drawDebugInformation(Canvas c) {
        if (debugPaint == null) {
            debugPaint = new Paint();
            debugPaint.setARGB(255, 255, 0, 0);
            debugPaint.setStyle(Paint.Style.STROKE);
            debugPaint.setStrokeWidth(UiUtil.dp(getContext(), 1));
        }

        if (debugTextPaint == null) {
            debugTextPaint = new TextPaint();
            debugTextPaint.setColor(0xFFFF0000);
            debugTextPaint.setTextSize(UiUtil.sp(getContext(), 16));
        }

        // Draw wireframe
        debugPaint.setStyle(Paint.Style.STROKE);
        c.drawRect(textBounds, debugPaint);
        c.drawRect(targetBounds, debugPaint);
        c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], 10, debugPaint);
        c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], calculatedOuterCircleRadius - CIRCLE_PADDING, debugPaint);
        c.drawCircle(targetBounds.centerX(), targetBounds.centerY(), TARGET_RADIUS + TARGET_PADDING, debugPaint);

        // Draw positions and dimensions
        debugPaint.setStyle(Paint.Style.FILL);
        final String debugText =
                "Text bounds: " + textBounds.toShortString() + "\n" +
                        "Target bounds: " + targetBounds.toShortString() + "\n" +
                        "Center: " + outerCircleCenter[0] + " " + outerCircleCenter[1] + "\n" +
                        "View size: " + getWidth() + " " + getHeight() + "\n" +
                        "Target bounds: " + targetBounds.toShortString();

        if (debugStringBuilder == null) {
            debugStringBuilder = new SpannableStringBuilder(debugText);
        } else {
            debugStringBuilder.clear();
            debugStringBuilder.append(debugText);
        }

        if (debugLayout == null) {
            debugLayout = new DynamicLayout(debugText, debugTextPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        }

        final int saveCount = c.save();
        {
            debugPaint.setARGB(220, 0, 0, 0);
            c.translate(0.0f, topBoundary);
            c.drawRect(0.0f, 0.0f, debugLayout.getWidth(), debugLayout.getHeight(), debugPaint);
            debugPaint.setARGB(255, 255, 0, 0);
            debugLayout.draw(c);
        }
        c.restoreToCount(saveCount);
    }

    void drawTintedTarget() {
        final Drawable icon = target.icon;
        if (!shouldTintTarget || icon == null) {
            tintedTarget = null;
            return;
        }

        if (tintedTarget != null) return;

        tintedTarget = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(),
                Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(tintedTarget);
        icon.setColorFilter(new PorterDuffColorFilter(
                outerCirclePaint.getColor(), PorterDuff.Mode.SRC_ATOP));
        icon.draw(canvas);
        icon.setColorFilter(null);
    }

    void updateTextLayouts() {
        final int textWidth = Math.min(getWidth(), TEXT_MAX_WIDTH) - TEXT_PADDING * 2;
        if (textWidth <= 0) {
            return;
        }

        titleLayout = new StaticLayout(title, titlePaint, textWidth,
                Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);

        if (description != null) {
            descriptionLayout = new StaticLayout(description, descriptionPaint, textWidth,
                    Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        } else {
            descriptionLayout = null;
        }

        if (buttonText != null) {
            buttonLayout = new StaticLayout(buttonText, buttonTextPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        } else {
            buttonLayout = new StaticLayout("  OK  ", buttonTextPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        }
    }

    float halfwayLerp(float lerp) {
        if (lerp < 0.5f) {
            return lerp / 0.5f;
        }

        return (1.0f - lerp) / 0.5f;
    }

    float delayedLerp(float lerp, float threshold) {
        if (lerp < threshold) {
            return 0.0f;
        }

        return (lerp - threshold) / (1.0f - threshold);
    }

    void calculateDimensions() {
        textBounds = getTextBounds();
        textBoundsButton = getButtonBounds();
        outerCircleCenter = getOuterCircleCenterPoint();
        calculatedOuterCircleRadius = getOuterCircleRadius(outerCircleCenter[0], outerCircleCenter[1], textBounds, targetBounds);
    }

    void calculateDrawingBounds() {
        if (outerCircleCenter == null) {
            // Called dismiss before we got a chance to display the tap target
            // So we have no center -> cant determine the drawing bounds
            return;
        }
        drawingBounds.left = (int) Math.max(0, outerCircleCenter[0] - outerCircleRadius);
        drawingBounds.top = (int) Math.min(0, outerCircleCenter[1] - outerCircleRadius);
        drawingBounds.right = (int) Math.min(getWidth(),
                outerCircleCenter[0] + outerCircleRadius + CIRCLE_PADDING);
        drawingBounds.bottom = (int) Math.min(getHeight(),
                outerCircleCenter[1] + outerCircleRadius + CIRCLE_PADDING);
    }

    int getOuterCircleRadius(int centerX, int centerY, Rect textBounds, Rect targetBounds) {
        final int targetCenterX = targetBounds.centerX();
        final int targetCenterY = targetBounds.centerY();
        final int expandedRadius = (int) (1.1f * TARGET_RADIUS);
        final Rect expandedBounds = new Rect(targetCenterX, targetCenterY, targetCenterX, targetCenterY);
        expandedBounds.inset(-expandedRadius, -expandedRadius);

        final int textRadius = maxDistanceToPoints(centerX, centerY, textBounds);
        final int targetRadius = maxDistanceToPoints(centerX, centerY, expandedBounds);
        return Math.max(textRadius, targetRadius) + CIRCLE_PADDING;
    }

    Rect getTextBounds() {
        final int totalTextHeight = getTotalTextHeight();
        final int totalTextWidth = getTotalTextWidth();

        final int possibleTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight;
        final int top;
        if (possibleTop > topBoundary) {
            top = possibleTop;
        } else {
            top = targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING;
        }

        final int relativeCenterDistance = (getWidth() / 2) - targetBounds.centerX();
        final int bias = relativeCenterDistance < 0 ? -TEXT_POSITIONING_BIAS : TEXT_POSITIONING_BIAS;
        final int left = Math.max(TEXT_PADDING, targetBounds.centerX() - bias - totalTextWidth);
        final int right = Math.min(getWidth() - TEXT_PADDING, left + totalTextWidth);
        return new Rect(left, top, right, top + totalTextHeight);
    }

    Rect getButtonBounds() {
        final int totalTextHeight = getTotalTextHeight();
        final int totalTextWidth = getTotalTextWidth();

        final int totalButtonHeight = buttonLayout.getHeight();

        Paint p = new Paint();
        p.setTextSize((float) target.buttonTextSizePx(this.getContext()));
        float w = p.measureText(buttonText, 0, buttonText.length());
        final int totalButtonWidth = (int) Math.ceil(w);

        int aaa = targetBounds.centerY();

        final int possibleTop1 = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight;
        int tH = titleLayout.getHeight();
        int dH = 0;
        int possibleTopB = 0;

        final int possibleTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight;
        final int top;
        if (possibleTop > topBoundary) {
            if (descriptionLayout == null) {
                possibleTopB = possibleTop1 + tH + TEXT_SPACING;
            } else {
                dH = descriptionLayout.getHeight();
                possibleTopB = possibleTop1 + tH + TEXT_SPACING + dH + TEXT_SPACING;
            }
            top = possibleTopB;
        } else {
            if (descriptionLayout == null) {
                possibleTopB = targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING + tH + TEXT_SPACING;
            } else {
                dH = descriptionLayout.getHeight();
                possibleTopB = targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING + tH + TEXT_SPACING + dH + TEXT_SPACING;
            }
            top = possibleTopB;
        }

        final int relativeCenterDistance = (getWidth() / 2) - targetBounds.centerX();
        final int bias = relativeCenterDistance < 0 ? -TEXT_POSITIONING_BIAS : TEXT_POSITIONING_BIAS;
        final int left = Math.max(TEXT_PADDING, targetBounds.centerX() - bias - totalTextWidth);
        final int right = Math.min(getWidth() - TEXT_PADDING, left + totalButtonWidth);
        return new Rect(left - 8, top - 8, right + 16, top + totalButtonHeight + 16);
    }

    int[] getOuterCircleCenterPoint() {
        if (inGutter(targetBounds.centerY())) {
            return new int[]{targetBounds.centerX(), targetBounds.centerY()};
        }

        final int targetRadius = Math.max(targetBounds.width(), targetBounds.height()) / 2 + TARGET_PADDING;
        final int totalTextHeight = getTotalTextHeight();

        final boolean onTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight > 0;

        final int left = Math.min(textBounds.left, targetBounds.left - targetRadius);
        final int right = Math.max(textBounds.right, targetBounds.right + targetRadius);
        final int titleHeight = titleLayout == null ? 0 : titleLayout.getHeight();
        final int centerY = onTop ?
                targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight + titleHeight
                :
                targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING + titleHeight;

        return new int[]{(left + right) / 2, centerY};
    }

    int getTotalTextHeight() {
        if (titleLayout == null) {
            return 0;
        }
        if (descriptionLayout == null) {
          if (buttonLayout == null) {
            return titleLayout.getHeight() + TEXT_SPACING;
          } else {
            return titleLayout.getHeight() + TEXT_SPACING + buttonLayout.getHeight() + TEXT_SPACING;
          }
        } else {
          if (buttonLayout == null) {
            return titleLayout.getHeight() + TEXT_SPACING + descriptionLayout.getHeight() + TEXT_SPACING;
        } else {
          return titleLayout.getHeight() + TEXT_SPACING + descriptionLayout.getHeight() + TEXT_SPACING + buttonLayout.getHeight() + TEXT_SPACING;
          }
        }
    }

    int getTotalTextWidth() {
        if (titleLayout == null) {
            return 0;
        }

        if (descriptionLayout == null) {
            if (buttonLayout == null) {
                return titleLayout.getWidth();
            } else {
                return titleLayout.getWidth() + buttonLayout.getWidth();
            }

        }

        return Math.max(titleLayout.getWidth(), descriptionLayout.getWidth());
    }

    boolean inGutter(int y) {
        if (bottomBoundary > 0) {
            return y < GUTTER_DIM || y > bottomBoundary - GUTTER_DIM;
        } else {
            return y < GUTTER_DIM || y > getHeight() - GUTTER_DIM;
        }
    }

    int maxDistanceToPoints(int x1, int y1, Rect bounds) {
        final double tl = distance(x1, y1, bounds.left, bounds.top);
        final double tr = distance(x1, y1, bounds.right, bounds.top);
        final double bl = distance(x1, y1, bounds.left, bounds.bottom);
        final double br = distance(x1, y1, bounds.right, bounds.bottom);
        return (int) Math.max(tl, Math.max(tr, Math.max(bl, br)));
    }

    double distance(int x1, int y1, int x2, int y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    }

    void invalidateViewAndOutline(Rect bounds) {
        invalidate(bounds);
        if (outlineProvider != null && Build.VERSION.SDK_INT >= 21) {
            invalidateOutline();
        }
    }
}
