/*
 * Copyright (C) 2022 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.google.android.material.textfield;

import com.google.android.material.R;

import static com.google.android.material.textfield.IconHelper.applyIconTint;
import static com.google.android.material.textfield.IconHelper.convertScaleType;
import static com.google.android.material.textfield.IconHelper.refreshIconDrawableState;
import static com.google.android.material.textfield.IconHelper.setCompatRippleBackgroundIfNeeded;
import static com.google.android.material.textfield.IconHelper.setIconMinSize;
import static com.google.android.material.textfield.IconHelper.setIconOnClickListener;
import static com.google.android.material.textfield.IconHelper.setIconOnLongClickListener;
import static com.google.android.material.textfield.IconHelper.setIconScaleType;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_CLEAR_TEXT;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_DROPDOWN_MENU;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_NONE;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.appcompat.widget.TintTypedArray;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.MarginLayoutParamsCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityManagerCompat;
import androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
import androidx.core.widget.TextViewCompat;
import com.google.android.material.internal.CheckableImageButton;
import com.google.android.material.internal.TextWatcherAdapter;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.textfield.TextInputLayout.EndIconMode;
import com.google.android.material.textfield.TextInputLayout.OnEditTextAttachedListener;
import com.google.android.material.textfield.TextInputLayout.OnEndIconChangedListener;
import java.util.LinkedHashSet;

/**
 * A compound layout that includes views that will be shown at the end of {@link TextInputLayout}
 * and their relevant rendering and presenting logic.
 */
@SuppressLint("ViewConstructor")
class EndCompoundLayout extends LinearLayout {
  final TextInputLayout textInputLayout;

  @NonNull private final FrameLayout endIconFrame;

  @NonNull private final CheckableImageButton errorIconView;
  private ColorStateList errorIconTintList;
  private PorterDuff.Mode errorIconTintMode;
  private OnLongClickListener errorIconOnLongClickListener;

  @NonNull private final CheckableImageButton endIconView;
  private final EndIconDelegates endIconDelegates;
  @EndIconMode private int endIconMode = END_ICON_NONE;
  private final LinkedHashSet<OnEndIconChangedListener> endIconChangedListeners =
      new LinkedHashSet<>();
  private ColorStateList endIconTintList;
  private PorterDuff.Mode endIconTintMode;
  private int endIconMinSize;
  @NonNull private ScaleType endIconScaleType;
  private OnLongClickListener endIconOnLongClickListener;

  @Nullable private CharSequence suffixText;
  @NonNull private final TextView suffixTextView;

  private boolean hintExpanded;

  private EditText editText;
  @Nullable private final AccessibilityManager accessibilityManager;
  @Nullable private TouchExplorationStateChangeListener touchExplorationStateChangeListener;

  private final TextWatcher editTextWatcher =
      new TextWatcherAdapter() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
          getEndIconDelegate().beforeEditTextChanged(s, start, count, after);
        }

        @Override
        public void afterTextChanged(Editable s) {
          getEndIconDelegate().afterEditTextChanged(s);
        }
      };

  private final OnEditTextAttachedListener onEditTextAttachedListener =
      new OnEditTextAttachedListener() {
        @Override
        public void onEditTextAttached(@NonNull TextInputLayout textInputLayout) {
          if (editText == textInputLayout.getEditText()) {
            return;
          }
          if (editText != null) {
            editText.removeTextChangedListener(editTextWatcher);
            if (editText.getOnFocusChangeListener()
                == getEndIconDelegate().getOnEditTextFocusChangeListener()) {
              editText.setOnFocusChangeListener(null);
            }
          }
          editText = textInputLayout.getEditText();
          if (editText != null) {
            editText.addTextChangedListener(editTextWatcher);
          }
          getEndIconDelegate().onEditTextAttached(editText);
          setOnFocusChangeListenersIfNeeded(getEndIconDelegate());
        }
      };

  EndCompoundLayout(TextInputLayout textInputLayout, TintTypedArray a) {
    super(textInputLayout.getContext());

    accessibilityManager =
        (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);

    this.textInputLayout = textInputLayout;

    setVisibility(GONE);
    setOrientation(HORIZONTAL);
    setLayoutParams(
        new FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.MATCH_PARENT,
            Gravity.END | Gravity.RIGHT));

    endIconFrame = new FrameLayout(getContext());
    endIconFrame.setVisibility(GONE);
    endIconFrame.setLayoutParams(
        new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));

    LayoutInflater layoutInflater = LayoutInflater.from(getContext());
    errorIconView = createIconView(this, layoutInflater, R.id.text_input_error_icon);
    endIconView = createIconView(endIconFrame, layoutInflater, R.id.text_input_end_icon);
    endIconDelegates = new EndIconDelegates(this, a);

    suffixTextView = new AppCompatTextView(getContext());

    initErrorIconView(a);
    initEndIconView(a);
    initSuffixTextView(a);

    endIconFrame.addView(endIconView);

    addView(suffixTextView);
    addView(endIconFrame);
    addView(errorIconView);

    textInputLayout.addOnEditTextAttachedListener(onEditTextAttachedListener);
    addOnAttachStateChangeListener(
        new OnAttachStateChangeListener() {
          @Override
          public void onViewAttachedToWindow(View ignored) {
            addTouchExplorationStateChangeListenerIfNeeded();
          }

          @Override
          public void onViewDetachedFromWindow(View ignored) {
            removeTouchExplorationStateChangeListenerIfNeeded();
          }
        });
  }

  private CheckableImageButton createIconView(
      ViewGroup root, LayoutInflater inflater, @IdRes int id) {
    CheckableImageButton iconView =
        (CheckableImageButton) inflater.inflate(
            R.layout.design_text_input_end_icon, root, false);
    iconView.setId(id);
    setCompatRippleBackgroundIfNeeded(iconView);
    if (MaterialResources.isFontScaleAtLeast1_3(getContext())) {
      ViewGroup.MarginLayoutParams lp =
          (ViewGroup.MarginLayoutParams) iconView.getLayoutParams();
      MarginLayoutParamsCompat.setMarginStart(lp, 0);
    }
    return iconView;
  }

  private void initErrorIconView(TintTypedArray a) {
    if (a.hasValue(R.styleable.TextInputLayout_errorIconTint)) {
      errorIconTintList =
          MaterialResources.getColorStateList(
              getContext(), a, R.styleable.TextInputLayout_errorIconTint);
    }
    if (a.hasValue(R.styleable.TextInputLayout_errorIconTintMode)) {
      errorIconTintMode =
          ViewUtils.parseTintMode(
              a.getInt(R.styleable.TextInputLayout_errorIconTintMode, -1), null);
    }
    if (a.hasValue(R.styleable.TextInputLayout_errorIconDrawable)) {
      setErrorIconDrawable(a.getDrawable(R.styleable.TextInputLayout_errorIconDrawable));
    }
    errorIconView.setContentDescription(
        getResources().getText(R.string.error_icon_content_description));
    ViewCompat.setImportantForAccessibility(
        errorIconView, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
    errorIconView.setClickable(false);
    errorIconView.setPressable(false);
    errorIconView.setFocusable(false);
  }

  private void initEndIconView(TintTypedArray a) {
    // Set up the end icon if any.
    if (!a.hasValue(R.styleable.TextInputLayout_passwordToggleEnabled)) {
      // Default tint for any end icon or value specified by user
      if (a.hasValue(R.styleable.TextInputLayout_endIconTint)) {
        endIconTintList =
            MaterialResources.getColorStateList(
                getContext(), a, R.styleable.TextInputLayout_endIconTint);
      }
      // Default tint mode for any end icon or value specified by user
      if (a.hasValue(R.styleable.TextInputLayout_endIconTintMode)) {
        endIconTintMode =
            ViewUtils.parseTintMode(
                a.getInt(R.styleable.TextInputLayout_endIconTintMode, -1), null);
      }
    }
    if (a.hasValue(R.styleable.TextInputLayout_endIconMode)) {
      // Specific defaults depending on which end icon mode is set
      setEndIconMode(a.getInt(R.styleable.TextInputLayout_endIconMode, END_ICON_NONE));
      if (a.hasValue(R.styleable.TextInputLayout_endIconContentDescription)) {
        setEndIconContentDescription(
            a.getText(R.styleable.TextInputLayout_endIconContentDescription));
      }
      setEndIconCheckable(a.getBoolean(R.styleable.TextInputLayout_endIconCheckable, true));
    } else if (a.hasValue(R.styleable.TextInputLayout_passwordToggleEnabled)) {
      // Support for deprecated attributes related to the password toggle end icon
      if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTint)) {
        endIconTintList =
            MaterialResources.getColorStateList(
                getContext(), a, R.styleable.TextInputLayout_passwordToggleTint);
      }
      if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTintMode)) {
        endIconTintMode =
            ViewUtils.parseTintMode(
                a.getInt(R.styleable.TextInputLayout_passwordToggleTintMode, -1), null);
      }
      boolean passwordToggleEnabled =
          a.getBoolean(R.styleable.TextInputLayout_passwordToggleEnabled, false);
      setEndIconMode(passwordToggleEnabled ? END_ICON_PASSWORD_TOGGLE : END_ICON_NONE);
      setEndIconContentDescription(
          a.getText(R.styleable.TextInputLayout_passwordToggleContentDescription));
    }
    setEndIconMinSize(
        a.getDimensionPixelSize(
            R.styleable.TextInputLayout_endIconMinSize,
            getResources().getDimensionPixelSize(R.dimen.mtrl_min_touch_target_size)));
    if (a.hasValue(R.styleable.TextInputLayout_endIconScaleType)) {
      setEndIconScaleType(
          convertScaleType(a.getInt(R.styleable.TextInputLayout_endIconScaleType, -1)));
    }
  }

  private void initSuffixTextView(TintTypedArray a) {
    // Set up suffix view.
    suffixTextView.setVisibility(GONE);
    suffixTextView.setId(R.id.textinput_suffix_text);
    suffixTextView.setLayoutParams(
        new LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT,
            Gravity.BOTTOM));
    ViewCompat.setAccessibilityLiveRegion(
        suffixTextView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);

    setSuffixTextAppearance(
        a.getResourceId(R.styleable.TextInputLayout_suffixTextAppearance, 0));
    if (a.hasValue(R.styleable.TextInputLayout_suffixTextColor)) {
      setSuffixTextColor(a.getColorStateList(R.styleable.TextInputLayout_suffixTextColor));
    }
    setSuffixText(a.getText(R.styleable.TextInputLayout_suffixText));
  }

  void setErrorIconDrawable(@DrawableRes int resId) {
    setErrorIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
    refreshErrorIconDrawableState();
  }

  void setErrorIconDrawable(@Nullable Drawable errorIconDrawable) {
    errorIconView.setImageDrawable(errorIconDrawable);
    updateErrorIconVisibility();
    applyIconTint(textInputLayout, errorIconView, errorIconTintList, errorIconTintMode);
  }

  Drawable getErrorIconDrawable() {
    return errorIconView.getDrawable();
  }

  void setErrorIconTintList(@Nullable ColorStateList errorIconTintList) {
    if (this.errorIconTintList != errorIconTintList) {
      this.errorIconTintList = errorIconTintList;
      applyIconTint(textInputLayout, errorIconView, errorIconTintList, errorIconTintMode);
    }
  }

  void setErrorIconTintMode(@Nullable PorterDuff.Mode errorIconTintMode) {
    if (this.errorIconTintMode != errorIconTintMode) {
      this.errorIconTintMode = errorIconTintMode;
      applyIconTint(textInputLayout, errorIconView, errorIconTintList, this.errorIconTintMode);
    }
  }

  void setErrorIconOnClickListener(@Nullable OnClickListener errorIconOnClickListener) {
    setIconOnClickListener(errorIconView, errorIconOnClickListener, errorIconOnLongClickListener);
  }

  CheckableImageButton getEndIconView() {
    return endIconView;
  }

  EndIconDelegate getEndIconDelegate() {
    return endIconDelegates.get(endIconMode);
  }

  @EndIconMode
  int getEndIconMode() {
    return endIconMode;
  }

  void setEndIconMode(@EndIconMode int endIconMode) {
    if (this.endIconMode == endIconMode) {
      return;
    }
    tearDownDelegate(getEndIconDelegate());
    int previousEndIconMode = this.endIconMode;
    this.endIconMode = endIconMode;
    dispatchOnEndIconChanged(previousEndIconMode);
    setEndIconVisible(endIconMode != END_ICON_NONE);
    EndIconDelegate delegate = getEndIconDelegate();
    setEndIconDrawable(getIconResId(delegate));
    setEndIconContentDescription(delegate.getIconContentDescriptionResId());
    setEndIconCheckable(delegate.isIconCheckable());
    if (delegate.isBoxBackgroundModeSupported(textInputLayout.getBoxBackgroundMode())) {
      setUpDelegate(delegate);
    } else {
      throw new IllegalStateException(
          "The current box background mode "
              + textInputLayout.getBoxBackgroundMode()
              + " is not supported by the end icon mode "
              + endIconMode);
    }
    setEndIconOnClickListener(delegate.getOnIconClickListener());
    if (editText != null) {
      delegate.onEditTextAttached(editText);
      setOnFocusChangeListenersIfNeeded(delegate);
    }
    applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
    refreshIconState(/* force= */ true);
  }

  void refreshIconState(boolean force) {
    boolean stateChanged = false;
    EndIconDelegate delegate = getEndIconDelegate();
    if (delegate.isIconCheckable()) {
      boolean wasChecked = endIconView.isChecked();
      if (wasChecked != delegate.isIconChecked()) {
        endIconView.setChecked(!wasChecked);
        stateChanged = true;
      }
    }
    if (delegate.isIconActivable()) {
      boolean wasActivated = endIconView.isActivated();
      if (wasActivated != delegate.isIconActivated()) {
        setEndIconActivated(!wasActivated);
        stateChanged = true;
      }
    }
    if (force || stateChanged) {
      refreshEndIconDrawableState();
    }
  }

  private void setUpDelegate(@NonNull EndIconDelegate delegate) {
    delegate.setUp();

    touchExplorationStateChangeListener = delegate.getTouchExplorationStateChangeListener();
    addTouchExplorationStateChangeListenerIfNeeded();
  }

  private void tearDownDelegate(@NonNull EndIconDelegate delegate) {
    removeTouchExplorationStateChangeListenerIfNeeded();
    touchExplorationStateChangeListener = null;
    delegate.tearDown();
  }

  private void addTouchExplorationStateChangeListenerIfNeeded() {
    if (touchExplorationStateChangeListener != null
        && accessibilityManager != null
        && ViewCompat.isAttachedToWindow(this)) {
      AccessibilityManagerCompat.addTouchExplorationStateChangeListener(
          accessibilityManager, touchExplorationStateChangeListener);
    }
  }

  private void removeTouchExplorationStateChangeListenerIfNeeded() {
    if (touchExplorationStateChangeListener != null && accessibilityManager != null) {
      AccessibilityManagerCompat.removeTouchExplorationStateChangeListener(
          accessibilityManager, touchExplorationStateChangeListener);
    }
  }

  private int getIconResId(EndIconDelegate delegate) {
    int customIconResId = endIconDelegates.customEndIconDrawableId;
    return customIconResId == 0 ? delegate.getIconDrawableResId() : customIconResId;
  }

  void setEndIconOnClickListener(@Nullable OnClickListener endIconOnClickListener) {
    setIconOnClickListener(endIconView, endIconOnClickListener, endIconOnLongClickListener);
  }

  void setEndIconOnLongClickListener(
      @Nullable OnLongClickListener endIconOnLongClickListener) {
    this.endIconOnLongClickListener = endIconOnLongClickListener;
    setIconOnLongClickListener(endIconView, endIconOnLongClickListener);
  }

  void setErrorIconOnLongClickListener(
      @Nullable OnLongClickListener errorIconOnLongClickListener) {
    this.errorIconOnLongClickListener = errorIconOnLongClickListener;
    setIconOnLongClickListener(errorIconView, errorIconOnLongClickListener);
  }

  private void setOnFocusChangeListenersIfNeeded(EndIconDelegate delegate) {
    if (editText == null) {
      return;
    }
    if (delegate.getOnEditTextFocusChangeListener() != null) {
      editText.setOnFocusChangeListener(delegate.getOnEditTextFocusChangeListener());
    }
    if (delegate.getOnIconViewFocusChangeListener() != null) {
      endIconView.setOnFocusChangeListener(delegate.getOnIconViewFocusChangeListener());
    }
  }

  void refreshErrorIconDrawableState() {
    refreshIconDrawableState(textInputLayout, errorIconView, errorIconTintList);
  }

  void setEndIconVisible(boolean visible) {
    if (isEndIconVisible() != visible) {
      endIconView.setVisibility(visible ? View.VISIBLE : View.GONE);
      updateEndLayoutVisibility();
      updateSuffixTextViewPadding();
      textInputLayout.updateDummyDrawables();
    }
  }

  boolean isEndIconVisible() {
    return endIconFrame.getVisibility() == VISIBLE && endIconView.getVisibility() == VISIBLE;
  }

  void setEndIconActivated(boolean endIconActivated) {
    endIconView.setActivated(endIconActivated);
  }

  void refreshEndIconDrawableState() {
    refreshIconDrawableState(textInputLayout, endIconView, endIconTintList);
  }

  void setEndIconCheckable(boolean endIconCheckable) {
    endIconView.setCheckable(endIconCheckable);
  }

  boolean isEndIconCheckable() {
    return endIconView.isCheckable();
  }

  boolean isEndIconChecked() {
    return hasEndIcon() && endIconView.isChecked();
  }

  void checkEndIcon() {
    endIconView.performClick();
    // Skip animation
    endIconView.jumpDrawablesToCurrentState();
  }

  void setEndIconDrawable(@DrawableRes int resId) {
    setEndIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
  }

  void setEndIconDrawable(@Nullable Drawable endIconDrawable) {
    endIconView.setImageDrawable(endIconDrawable);
    if (endIconDrawable != null) {
      applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
      refreshEndIconDrawableState();
    }
  }

  @Nullable
  Drawable getEndIconDrawable() {
    return endIconView.getDrawable();
  }

  void setEndIconContentDescription(@StringRes int resId) {
    setEndIconContentDescription(resId != 0 ? getResources().getText(resId) : null);
  }

  void setEndIconContentDescription(@Nullable CharSequence endIconContentDescription) {
    if (getEndIconContentDescription() != endIconContentDescription) {
      endIconView.setContentDescription(endIconContentDescription);
    }
  }

  @Nullable
  CharSequence getEndIconContentDescription() {
    return endIconView.getContentDescription();
  }

  void setEndIconTintList(@Nullable ColorStateList endIconTintList) {
    if (this.endIconTintList != endIconTintList) {
      this.endIconTintList = endIconTintList;
      applyIconTint(textInputLayout, endIconView, this.endIconTintList, endIconTintMode);
    }
  }

  void setEndIconTintMode(@Nullable PorterDuff.Mode endIconTintMode) {
    if (this.endIconTintMode != endIconTintMode) {
      this.endIconTintMode = endIconTintMode;
      applyIconTint(textInputLayout, endIconView, endIconTintList, this.endIconTintMode);
    }
  }

  void setEndIconMinSize(@Px int iconSize) {
    if (iconSize < 0) {
      throw new IllegalArgumentException("endIconSize cannot be less than 0");
    }
    if (iconSize != endIconMinSize) {
      endIconMinSize = iconSize;
      setIconMinSize(endIconView, iconSize);
      setIconMinSize(errorIconView, iconSize);
    }
  }

  int getEndIconMinSize() {
    return endIconMinSize;
  }

  void setEndIconScaleType(@NonNull ScaleType endIconScaleType) {
    this.endIconScaleType = endIconScaleType;
    setIconScaleType(endIconView, endIconScaleType);
    setIconScaleType(errorIconView, endIconScaleType);
  }

  @NonNull ScaleType getEndIconScaleType() {
    return endIconScaleType;
  }

  void addOnEndIconChangedListener(@NonNull OnEndIconChangedListener listener) {
    endIconChangedListeners.add(listener);
  }

  void removeOnEndIconChangedListener(@NonNull OnEndIconChangedListener listener) {
    endIconChangedListeners.remove(listener);
  }

  void clearOnEndIconChangedListeners() {
    endIconChangedListeners.clear();
  }

  boolean hasEndIcon() {
    return endIconMode != END_ICON_NONE;
  }

  TextView getSuffixTextView() {
    return suffixTextView;
  }

  void setSuffixText(@Nullable CharSequence suffixText) {
    this.suffixText = TextUtils.isEmpty(suffixText) ? null : suffixText;
    suffixTextView.setText(suffixText);
    updateSuffixTextVisibility();
  }

  @Nullable
  CharSequence getSuffixText() {
    return suffixText;
  }

  void setSuffixTextAppearance(@StyleRes int suffixTextAppearance) {
    TextViewCompat.setTextAppearance(suffixTextView, suffixTextAppearance);
  }

  void setSuffixTextColor(@NonNull ColorStateList suffixTextColor) {
    suffixTextView.setTextColor(suffixTextColor);
  }

  @Nullable
  ColorStateList getSuffixTextColor() {
    return suffixTextView.getTextColors();
  }

  void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) {
    setPasswordVisibilityToggleDrawable(
        resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
  }

  void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) {
    endIconView.setImageDrawable(icon);
  }

  void setPasswordVisibilityToggleContentDescription(@StringRes int resId) {
    setPasswordVisibilityToggleContentDescription(
        resId != 0 ? getResources().getText(resId) : null);
  }

  void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) {
    endIconView.setContentDescription(description);
  }

  @Nullable
  Drawable getPasswordVisibilityToggleDrawable() {
    return endIconView.getDrawable();
  }

  @Nullable
  CharSequence getPasswordVisibilityToggleContentDescription() {
    return endIconView.getContentDescription();
  }

  boolean isPasswordVisibilityToggleEnabled() {
    return endIconMode == END_ICON_PASSWORD_TOGGLE;
  }

  void setPasswordVisibilityToggleEnabled(boolean enabled) {
    if (enabled && endIconMode != END_ICON_PASSWORD_TOGGLE) {
      // Set password toggle end icon if it's not already set
      setEndIconMode(END_ICON_PASSWORD_TOGGLE);
    } else if (!enabled) {
      // Set end icon to null
      setEndIconMode(END_ICON_NONE);
    }
  }

  void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) {
    endIconTintList = tintList;
    applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
  }

  void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) {
    endIconTintMode = mode;
    applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
  }

  void togglePasswordVisibilityToggle(boolean shouldSkipAnimations) {
    if (endIconMode == END_ICON_PASSWORD_TOGGLE) {
      endIconView.performClick();
      if (shouldSkipAnimations) {
        endIconView.jumpDrawablesToCurrentState();
      }
    }
  }

  void onHintStateChanged(boolean hintExpanded) {
    this.hintExpanded = hintExpanded;
    updateSuffixTextVisibility();
  }

  void onTextInputBoxStateUpdated() {
    updateErrorIconVisibility();

    // Update icons tints
    refreshErrorIconDrawableState();
    refreshEndIconDrawableState();

    if (getEndIconDelegate().shouldTintIconOnError()) {
      tintEndIconOnError(textInputLayout.shouldShowError());
    }
  }

  private void updateSuffixTextVisibility() {
    int oldVisibility = suffixTextView.getVisibility();
    int newVisibility = (suffixText != null && !hintExpanded) ? VISIBLE : GONE;
    if (oldVisibility != newVisibility) {
      getEndIconDelegate().onSuffixVisibilityChanged(/* visible= */ newVisibility == VISIBLE);
    }
    updateEndLayoutVisibility();
    // Set visibility after updating end layout's visibility so screen readers correctly announce
    // when suffix text appears.
    suffixTextView.setVisibility(newVisibility);
    textInputLayout.updateDummyDrawables();
  }

  void updateSuffixTextViewPadding() {
    if (textInputLayout.editText == null) {
      return;
    }
    int endPadding =
        (isEndIconVisible() || isErrorIconVisible())
            ? 0 : ViewCompat.getPaddingEnd(textInputLayout.editText);
    ViewCompat.setPaddingRelative(
        suffixTextView,
        getContext()
            .getResources()
            .getDimensionPixelSize(R.dimen.material_input_text_to_prefix_suffix_padding),
        textInputLayout.editText.getPaddingTop(),
        endPadding,
        textInputLayout.editText.getPaddingBottom());
  }

  int getSuffixTextEndOffset() {
    int endIconOffset;
    if (isEndIconVisible() || isErrorIconVisible()) {
      endIconOffset =
          endIconView.getMeasuredWidth()
              + MarginLayoutParamsCompat.getMarginStart(
              (MarginLayoutParams) endIconView.getLayoutParams());
    } else {
      endIconOffset = 0;
    }
    return ViewCompat.getPaddingEnd(this)
        + ViewCompat.getPaddingEnd(suffixTextView)
        + endIconOffset;
  }

  @Nullable
  CheckableImageButton getCurrentEndIconView() {
    if (isErrorIconVisible()) {
      return errorIconView;
    } else if (hasEndIcon() && isEndIconVisible()) {
      return endIconView;
    } else {
      return null;
    }
  }

  boolean isErrorIconVisible() {
    return errorIconView.getVisibility() == VISIBLE;
  }

  private void updateErrorIconVisibility() {
    boolean visible =
        getErrorIconDrawable() != null
            && textInputLayout.isErrorEnabled()
            && textInputLayout.shouldShowError();
    errorIconView.setVisibility(visible ? VISIBLE : GONE);
    updateEndLayoutVisibility();
    updateSuffixTextViewPadding();
    if (!hasEndIcon()) {
      textInputLayout.updateDummyDrawables();
    }
  }

  private void updateEndLayoutVisibility() {
    // Sync endIconFrame's visibility with the endIconView's.
    endIconFrame.setVisibility(
        (endIconView.getVisibility() == VISIBLE && !isErrorIconVisible()) ? VISIBLE : GONE);

    int suffixTextVisibility = (suffixText != null && !hintExpanded) ? VISIBLE : GONE;
    boolean shouldBeVisible =
        isEndIconVisible() || isErrorIconVisible() || suffixTextVisibility == VISIBLE;
    setVisibility(shouldBeVisible ? VISIBLE : GONE);
  }

  private void dispatchOnEndIconChanged(@EndIconMode int previousIcon) {
    for (OnEndIconChangedListener listener : endIconChangedListeners) {
      listener.onEndIconChanged(textInputLayout, previousIcon);
    }
  }

  private void tintEndIconOnError(boolean tintEndIconOnError) {
    if (tintEndIconOnError && getEndIconDrawable() != null) {
      // Setting the tint here instead of calling setEndIconTintList() in order to preserve and
      // restore the icon's original tint.
      Drawable endIconDrawable = DrawableCompat.wrap(getEndIconDrawable()).mutate();
      DrawableCompat.setTint(
          endIconDrawable, textInputLayout.getErrorCurrentTextColors());
      endIconView.setImageDrawable(endIconDrawable);
    } else {
      applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
    }
  }

  private static class EndIconDelegates {
    private final SparseArray<EndIconDelegate> delegates = new SparseArray<>();

    private final EndCompoundLayout endLayout;
    private final int customEndIconDrawableId;
    private final int passwordIconDrawableId;

    EndIconDelegates(EndCompoundLayout endLayout, TintTypedArray a) {
      this.endLayout = endLayout;
      customEndIconDrawableId = a.getResourceId(R.styleable.TextInputLayout_endIconDrawable, 0);
      passwordIconDrawableId =
          a.getResourceId(R.styleable.TextInputLayout_passwordToggleDrawable, 0);
    }

    EndIconDelegate get(@EndIconMode int endIconMode) {
      EndIconDelegate delegate = delegates.get(endIconMode);
      if (delegate == null) {
        delegate = create(endIconMode);
        delegates.append(endIconMode, delegate);
      }
      return delegate;
    }

    private EndIconDelegate create(@EndIconMode int endIconMode) {
      switch (endIconMode) {
        case END_ICON_PASSWORD_TOGGLE:
          return new PasswordToggleEndIconDelegate(endLayout, passwordIconDrawableId);
        case END_ICON_CLEAR_TEXT:
          return new ClearTextEndIconDelegate(endLayout);
        case END_ICON_DROPDOWN_MENU:
          return new DropdownMenuEndIconDelegate(endLayout);
        case END_ICON_CUSTOM:
          return new CustomEndIconDelegate(endLayout);
        case END_ICON_NONE:
          return new NoEndIconDelegate(endLayout);
        default:
          throw new IllegalArgumentException("Invalid end icon mode: " + endIconMode);
      }
    }
  }
}
