/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.messaging.util;

import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import android.text.Html;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.view.Gravity;
import android.view.Surface;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.Interpolator;
import android.view.animation.ScaleAnimation;
import android.widget.RemoteViews;
import android.widget.Toast;

import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBar.Placement;
import com.android.messaging.ui.conversationlist.ConversationListActivity;
import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.ui.SnackBarManager;
import com.android.messaging.ui.UIIntents;

import java.lang.reflect.Field;
import java.util.List;

public class UiUtils {
    /** MediaPicker transition duration in ms */
    public static final int MEDIAPICKER_TRANSITION_DURATION =
            getApplicationContext().getResources().getInteger(
                    R.integer.mediapicker_transition_duration);
    /** Short transition duration in ms */
    public static final int ASYNCIMAGE_TRANSITION_DURATION =
            getApplicationContext().getResources().getInteger(
                    R.integer.asyncimage_transition_duration);
    /** Compose transition duration in ms */
    public static final int COMPOSE_TRANSITION_DURATION =
            getApplicationContext().getResources().getInteger(
                    R.integer.compose_transition_duration);
    /** Generic duration for revealing/hiding a view */
    public static final int REVEAL_ANIMATION_DURATION =
            getApplicationContext().getResources().getInteger(
                    R.integer.reveal_view_animation_duration);

    public static final Interpolator DEFAULT_INTERPOLATOR = new CubicBezierInterpolator(
            0.4f, 0.0f, 0.2f, 1.0f);

    public static final Interpolator EASE_IN_INTERPOLATOR = new CubicBezierInterpolator(
            0.4f, 0.0f, 0.8f, 0.5f);

    public static final Interpolator EASE_OUT_INTERPOLATOR = new CubicBezierInterpolator(
            0.0f, 0.0f, 0.2f, 1f);

    /** Show a simple toast at the bottom */
    public static void showToastAtBottom(final int messageId) {
        UiUtils.showToastAtBottom(getApplicationContext().getString(messageId));
    }

    /** Show a simple toast at the bottom */
    public static void showToastAtBottom(final String message) {
        final Toast toast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG);
        toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
        toast.show();
    }

    /** Show a simple toast at the default position */
    public static void showToast(final int messageId) {
        final Toast toast = Toast.makeText(getApplicationContext(),
                getApplicationContext().getString(messageId), Toast.LENGTH_LONG);
        toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
        toast.show();
    }

    /** Show a simple toast at the default position */
    public static void showToast(final int pluralsMessageId, final int count) {
        final Toast toast = Toast.makeText(getApplicationContext(),
                getApplicationContext().getResources().getQuantityString(pluralsMessageId, count),
                Toast.LENGTH_LONG);
        toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
        toast.show();
    }

    public static void showSnackBar(final Context context, @NonNull final View parentView,
            final String message, @Nullable final Runnable runnable, final int runnableLabel,
            @Nullable final List<SnackBarInteraction> interactions) {
        Assert.notNull(context);
        SnackBar.Action action = null;
        switch (runnableLabel) {
            case SnackBar.Action.SNACK_BAR_UNDO:
                action = SnackBar.Action.createUndoAction(runnable);
                break;
            case SnackBar.Action.SNACK_BAR_RETRY:
                action =  SnackBar.Action.createRetryAction(runnable);
                break;
            default :
                break;
        }

        showSnackBarWithCustomAction(context, parentView, message, action, interactions,
                                        null /* placement */);
    }

    public static void showSnackBarWithCustomAction(final Context context,
            @NonNull final View parentView,
            @NonNull final String message,
            @NonNull final SnackBar.Action action,
            @Nullable final List<SnackBarInteraction> interactions,
            @Nullable final Placement placement) {
        Assert.notNull(context);
        Assert.isTrue(!TextUtils.isEmpty(message));
        Assert.notNull(action);
        SnackBarManager.get()
            .newBuilder(parentView)
            .setText(message)
            .setAction(action)
            .withInteractions(interactions)
            .withPlacement(placement)
            .show();
    }

    /**
     * Run the given runnable once after the next layout pass of the view.
     */
    public static void doOnceAfterLayoutChange(final View view, final Runnable runnable) {
        final OnLayoutChangeListener listener = new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(final View v, final int left, final int top, final int right,
                    final int bottom, final int oldLeft, final int oldTop, final int oldRight,
                    final int oldBottom) {
                // Call the runnable outside the layout pass because very few actions are allowed in
                // the layout pass
                ThreadUtil.getMainThreadHandler().post(runnable);
                view.removeOnLayoutChangeListener(this);
            }
        };
        view.addOnLayoutChangeListener(listener);
    }

    public static boolean isLandscapeMode() {
        return Factory.get().getApplicationContext().getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE;
    }

    private static Context getApplicationContext() {
        return Factory.get().getApplicationContext();
    }

    public static CharSequence commaEllipsize(
            final String text,
            final TextPaint paint,
            final int width,
            final String oneMore,
            final String more) {
        CharSequence ellipsized = TextUtils.commaEllipsize(
                text,
                paint,
                width,
                oneMore,
                more);
        if (TextUtils.isEmpty(ellipsized)) {
            ellipsized = text;
        }
        return ellipsized;
    }

    /**
     * Reveals/Hides a view with a scale animation from view center.
     * @param view the view to animate
     * @param desiredVisibility desired visibility (e.g. View.GONE) for the animated view.
     * @param onFinishRunnable an optional runnable called at the end of the animation
     */
    public static void revealOrHideViewWithAnimation(final View view, final int desiredVisibility,
            @Nullable final Runnable onFinishRunnable) {
        final boolean needAnimation = view.getVisibility() != desiredVisibility;
        if (needAnimation) {
            final float fromScale = desiredVisibility == View.VISIBLE ? 0F : 1F;
            final float toScale = desiredVisibility == View.VISIBLE ? 1F : 0F;
            final ScaleAnimation showHideAnimation =
                    new ScaleAnimation(fromScale, toScale, fromScale, toScale,
                            ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
                            ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
            showHideAnimation.setDuration(REVEAL_ANIMATION_DURATION);
            showHideAnimation.setInterpolator(DEFAULT_INTERPOLATOR);
            showHideAnimation.setAnimationListener(new AnimationListener() {
                @Override
                public void onAnimationStart(final Animation animation) {
                }

                @Override
                public void onAnimationRepeat(final Animation animation) {
                }

                @Override
                public void onAnimationEnd(final Animation animation) {
                    if (onFinishRunnable != null) {
                        // Rather than running this immediately, we post it to happen next so that
                        // the animation will be completed so that the view can be detached from
                        // it's window.  Otherwise, we may leak memory.
                        ThreadUtil.getMainThreadHandler().post(onFinishRunnable);
                    }
                }
            });
            view.clearAnimation();
            view.startAnimation(showHideAnimation);
            // We are playing a view Animation; unlike view property animations, we can commit the
            // visibility immediately instead of waiting for animation end.
            view.setVisibility(desiredVisibility);
        } else if (onFinishRunnable != null) {
            // Make sure onFinishRunnable is always executed.
            ThreadUtil.getMainThreadHandler().post(onFinishRunnable);
        }
    }

    public static Rect getMeasuredBoundsOnScreen(final View view) {
        final int[] location = new int[2];
        view.getLocationOnScreen(location);
        return new Rect(location[0], location[1],
                location[0] + view.getMeasuredWidth(), location[1] + view.getMeasuredHeight());
    }

    public static void setStatusBarColor(final Activity activity, final int color) {
        if (OsUtil.isAtLeastL()) {
            // To achieve the appearance of an 80% opacity blend against a black background,
            // each color channel is reduced in value by 20%.
            final int blendedRed = (int) Math.floor(0.8 * Color.red(color));
            final int blendedGreen = (int) Math.floor(0.8 * Color.green(color));
            final int blendedBlue = (int) Math.floor(0.8 * Color.blue(color));

            activity.getWindow().setStatusBarColor(
                    Color.rgb(blendedRed, blendedGreen, blendedBlue));
        }
    }

    public static void lockOrientation(final Activity activity) {
        final int orientation = activity.getResources().getConfiguration().orientation;
        final int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();

        // rotation tracks the rotation of the device from its natural orientation
        // orientation tracks whether the screen is landscape or portrait.
        // It is possible to have a rotation of 0 (device in its natural orientation) in portrait
        // (phone), or in landscape (tablet), so we have to check both values to determine what to
        // pass to setRequestedOrientation.
        if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
            if (orientation == Configuration.ORIENTATION_PORTRAIT) {
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            }
        } else if (rotation == Surface.ROTATION_180 || rotation == Surface.ROTATION_270) {
            if (orientation == Configuration.ORIENTATION_PORTRAIT) {
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
            } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
            }
        }
    }

    public static void unlockOrientation(final Activity activity) {
        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
    }

    public static int getPaddingStart(final View view) {
        return OsUtil.isAtLeastJB_MR1() ? view.getPaddingStart() : view.getPaddingLeft();
    }

    public static int getPaddingEnd(final View view) {
        return OsUtil.isAtLeastJB_MR1() ? view.getPaddingEnd() : view.getPaddingRight();
    }

    public static boolean isRtlMode() {
        return OsUtil.isAtLeastJB_MR2() && Factory.get().getApplicationContext().getResources()
                .getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    }

    /**
     * Check if the activity needs to be redirected to permission check
     * @return true if {@link Activity#finish()} was called because redirection was performed
     */
    public static boolean redirectToPermissionCheckIfNeeded(final Activity activity) {
        if (!OsUtil.hasRequiredPermissions()) {
            UIIntents.get().launchPermissionCheckActivity(activity);
        } else {
            // No redirect performed
            return false;
        }

        // Redirect performed
        activity.finish();
        return true;
    }

    /**
     * Called to check if all conditions are nominal and a "go" for some action, such as deleting
     * a message, that requires this app to be the default app. This is also a precondition
     * required for sending a draft.
     * @return true if all conditions are nominal and we're ready to send a message
     */
    public static boolean isReadyForAction() {
        final PhoneUtils phoneUtils = PhoneUtils.getDefault();

        // Have all the conditions been met:
        // Supports SMS?
        // Has a preferred sim?
        // Is the default sms app?
        return phoneUtils.isSmsCapable() &&
                phoneUtils.getHasPreferredSmsSim() &&
                phoneUtils.isDefaultSmsApp();
    }

    /*
     * Removes all html markup from the text and replaces links with the the text and a text version
     * of the href.
     * @param htmlText HTML markup text
     * @return Sanitized string with link hrefs inlined
     */
    public static String stripHtml(final String htmlText) {
        final StringBuilder result = new StringBuilder();
        final Spanned markup = Html.fromHtml(htmlText);
        final String strippedText = markup.toString();

        final URLSpan[] links = markup.getSpans(0, markup.length() - 1, URLSpan.class);
        int currentIndex = 0;
        for (final URLSpan link : links) {
            final int spanStart = markup.getSpanStart(link);
            final int spanEnd = markup.getSpanEnd(link);
            if (spanStart > currentIndex) {
                result.append(strippedText, currentIndex, spanStart);
            }
            final String displayText = strippedText.substring(spanStart, spanEnd);
            final String linkText = link.getURL();
            result.append(getApplicationContext().getString(R.string.link_display_format,
                    displayText, linkText));
            currentIndex = spanEnd;
        }
        if (strippedText.length() > currentIndex) {
            result.append(strippedText, currentIndex, strippedText.length());
        }
        return result.toString();
    }

    public static void setActionBarShadowVisibility(final AppCompatActivity activity, final boolean visible) {
        final ActionBar actionBar = activity.getSupportActionBar();
        actionBar.setElevation(visible ?
                activity.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation) :
                0);
        final View actionBarView = activity.getWindow().getDecorView().findViewById(
                androidx.appcompat.R.id.decor_content_parent);
        if (actionBarView != null) {
            // AppCompatActionBar has one drawable Field, which is the shadow for the action bar
            // set the alpha on that drawable manually
            final Field[] fields = actionBarView.getClass().getDeclaredFields();
            try {
                for (final Field field : fields) {
                    if (field.getType().equals(Drawable.class)) {
                        field.setAccessible(true);
                        final Drawable shadowDrawable = (Drawable) field.get(actionBarView);
                        if (shadowDrawable != null) {
                            shadowDrawable.setAlpha(visible ? 255 : 0);
                            actionBarView.invalidate();
                            return;
                        }
                    }
                }
            } catch (final IllegalAccessException ex) {
                // Not expected, we should avoid this via field.setAccessible(true) above
                LogUtil.e(LogUtil.BUGLE_TAG, "Error setting shadow visibility", ex);
            }
        }
    }

    /**
     * Get the activity that's hosting the view, typically casting view.getContext() as an Activity
     * is sufficient, but sometimes the context is a context wrapper, in which case we need to case
     * the base context
     */
    public static Activity getActivity(final View view) {
        if (view == null) {
            return null;
        }
        return getActivity(view.getContext());
    }

    /**
     * Get the activity for the supplied context, typically casting context as an Activity
     * is sufficient, but sometimes the context is a context wrapper, in which case we need to case
     * the base context
     */
    public static Activity getActivity(final Context context) {
        if (context == null) {
            return null;
        }
        if (context instanceof Activity) {
            return (Activity) context;
        }
        if (context instanceof ContextWrapper) {
            return getActivity(((ContextWrapper) context).getBaseContext());
        }

        // We've hit a non-activity context such as an app-context
        return null;
    }

    public static RemoteViews getWidgetMissingPermissionView(final Context context) {
        return new RemoteViews(context.getPackageName(), R.layout.widget_missing_permission);
    }
}
