/*
 * Copyright (C) 2017 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.setupwizardlib.template;

import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.android.setupwizardlib.TemplateLayout;
import com.android.setupwizardlib.view.NavigationBar;

/**
 * A mixin to require the a scrollable container (BottomScrollView, RecyclerView or ListView) to be
 * scrolled to bottom, making sure that the user sees all content above and below the fold.
 */
public class RequireScrollMixin implements Mixin {

  /* static section */

  /**
   * Listener for when the require-scroll state changes. Note that this only requires the user to
   * scroll to the bottom once - if the user scrolled to the bottom and back-up, scrolling to bottom
   * is not required again.
   */
  public interface OnRequireScrollStateChangedListener {

    /**
     * Called when require-scroll state changed.
     *
     * @param scrollNeeded True if the user should be required to scroll to bottom.
     */
    void onRequireScrollStateChanged(boolean scrollNeeded);
  }

  /**
   * A delegate to detect scrollability changes and to scroll the page. This provides a layer of
   * abstraction for BottomScrollView, RecyclerView and ListView. The delegate should call {@link
   * #notifyScrollabilityChange(boolean)} when the view scrollability is changed.
   */
  interface ScrollHandlingDelegate {

    /** Starts listening to scrollability changes at the target scrollable container. */
    void startListening();

    /** Scroll the page content down by one page. */
    void pageScrollDown();
  }

  /* non-static section */

  private final Handler handler = new Handler(Looper.getMainLooper());

  private boolean requiringScrollToBottom = false;

  // Whether the user have seen the more button yet.
  private boolean everScrolledToBottom = false;

  private ScrollHandlingDelegate delegate;

  @Nullable private OnRequireScrollStateChangedListener listener;

  /** @param templateLayout The template containing this mixin */
  public RequireScrollMixin(@NonNull TemplateLayout templateLayout) {
  }

  /**
   * Sets the delegate to handle scrolling. The type of delegate should depend on whether the
   * scrolling view is a BottomScrollView, RecyclerView or ListView.
   */
  public void setScrollHandlingDelegate(@NonNull ScrollHandlingDelegate delegate) {
    this.delegate = delegate;
  }

  /**
   * Listen to require scroll state changes. When scroll is required, {@link
   * OnRequireScrollStateChangedListener#onRequireScrollStateChanged(boolean)} is called with {@code
   * true}, and vice versa.
   */
  public void setOnRequireScrollStateChangedListener(
      @Nullable OnRequireScrollStateChangedListener listener) {
    this.listener = listener;
  }

  /** @return The scroll state listener previously set, or {@code null} if none is registered. */
  public OnRequireScrollStateChangedListener getOnRequireScrollStateChangedListener() {
    return listener;
  }

  /**
   * Creates an {@link OnClickListener} which if scrolling is required, will scroll the page down,
   * and if scrolling is not required, delegates to the wrapped {@code listener}. Note that you
   * should call {@link #requireScroll()} as well in order to start requiring scrolling.
   *
   * @param listener The listener to be invoked when scrolling is not needed and the user taps on
   *     the button. If {@code null}, the click listener will be a no-op when scroll is not
   *     required.
   * @return A new {@link OnClickListener} which will scroll the page down or delegate to the given
   *     listener depending on the current require-scroll state.
   */
  public OnClickListener createOnClickListener(@Nullable final OnClickListener listener) {
    return new OnClickListener() {
      @Override
      public void onClick(View view) {
        if (requiringScrollToBottom) {
          delegate.pageScrollDown();
        } else if (listener != null) {
          listener.onClick(view);
        }
      }
    };
  }

  /**
   * Coordinate with the given navigation bar to require scrolling on the page. The more button will
   * be shown instead of the next button while scrolling is required.
   */
  public void requireScrollWithNavigationBar(@NonNull final NavigationBar navigationBar) {
    setOnRequireScrollStateChangedListener(
        new OnRequireScrollStateChangedListener() {
          @Override
          public void onRequireScrollStateChanged(boolean scrollNeeded) {
            navigationBar.getMoreButton().setVisibility(scrollNeeded ? View.VISIBLE : View.GONE);
            navigationBar.getNextButton().setVisibility(scrollNeeded ? View.GONE : View.VISIBLE);
          }
        });
    navigationBar.getMoreButton().setOnClickListener(createOnClickListener(null));
    requireScroll();
  }

  /** @see #requireScrollWithButton(Button, CharSequence, OnClickListener) */
  public void requireScrollWithButton(
      @NonNull Button button, @StringRes int moreText, @Nullable OnClickListener onClickListener) {
    requireScrollWithButton(button, button.getContext().getText(moreText), onClickListener);
  }

  /**
   * Use the given {@code button} to require scrolling. When scrolling is required, the button label
   * will change to {@code moreText}, and tapping the button will cause the page to scroll down.
   *
   * <p>Note: Calling {@link View#setOnClickListener} on the button after this method will remove
   * its link to the require-scroll mechanism. If you need to do that, obtain the click listener
   * from {@link #createOnClickListener(OnClickListener)}.
   *
   * <p>Note: The normal button label is taken from the button's text at the time of calling this
   * method. Calling {@link android.widget.TextView#setText} after calling this method causes
   * undefined behavior.
   *
   * @param button The button to use for require scroll. The button's "normal" label is taken from
   *     the text at the time of calling this method, and the click listener of it will be replaced.
   * @param moreText The button label when scroll is required.
   * @param onClickListener The listener for clicks when scrolling is not required.
   */
  public void requireScrollWithButton(
      @NonNull final Button button,
      final CharSequence moreText,
      @Nullable OnClickListener onClickListener) {
    final CharSequence nextText = button.getText();
    button.setOnClickListener(createOnClickListener(onClickListener));
    setOnRequireScrollStateChangedListener(
        new OnRequireScrollStateChangedListener() {
          @Override
          public void onRequireScrollStateChanged(boolean scrollNeeded) {
            button.setText(scrollNeeded ? moreText : nextText);
          }
        });
    requireScroll();
  }

  /**
   * @return True if scrolling is required. Note that this mixin only requires the user to scroll to
   *     the bottom once - if the user scrolled to the bottom and back-up, scrolling to bottom is
   *     not required again.
   */
  public boolean isScrollingRequired() {
    return requiringScrollToBottom;
  }

  /**
   * Start requiring scrolling on the layout. After calling this method, this mixin will start
   * listening to scroll events from the scrolling container, and call {@link
   * OnRequireScrollStateChangedListener} when the scroll state changes.
   */
  public void requireScroll() {
    delegate.startListening();
  }

  /**
   * {@link ScrollHandlingDelegate} should call this method when the scrollability of the scrolling
   * container changed, so this mixin can recompute whether scrolling should be required.
   *
   * @param canScrollDown True if the view can scroll down further.
   */
  void notifyScrollabilityChange(boolean canScrollDown) {
    if (canScrollDown == requiringScrollToBottom) {
      // Already at the desired require-scroll state
      return;
    }
    if (canScrollDown) {
      if (!everScrolledToBottom) {
        postScrollStateChange(true);
        requiringScrollToBottom = true;
      }
    } else {
      postScrollStateChange(false);
      requiringScrollToBottom = false;
      everScrolledToBottom = true;
    }
  }

  private void postScrollStateChange(final boolean scrollNeeded) {
    handler.post(
        new Runnable() {
          @Override
          public void run() {
            if (listener != null) {
              listener.onRequireScrollStateChanged(scrollNeeded);
            }
          }
        });
  }
}
