/*
 * Copyright 2016 - 2020 Michael Rapp
 *
 * 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 de.mrapp.android.tabswitcher.gesture;

import android.content.res.Resources;
import android.graphics.RectF;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.mrapp.android.tabswitcher.Layout;
import de.mrapp.android.tabswitcher.R;
import de.mrapp.android.tabswitcher.TabSwitcher;

/**
 * An event handler, which allows to handle swipe gestures, which can be used to switch between
 * tabs.
 *
 * @author Michael Rapp
 * @since 1.0.0
 */
public class SwipeGestureEventHandler extends AbstractDragGestureEventHandler {

    /**
     * Defines the interface, a class, which should be notified about the events of a {@link
     * SwipeGestureEventHandler}, must implement.
     */
    public interface Callback {

        /**
         * The method, which is invoked, when switching between neighboring tabs.
         *
         * @param selectedTabIndex
         *         The index of the currently selected tab as an {@link Integer} value
         * @param distance
         *         The distance, the currently selected tab is swiped by, in pixels as a {@link
         *         Float} value
         */
        void onSwitchingBetweenTabs(int selectedTabIndex, float distance);

        /**
         * The method, which is invoked, when switching between neighboring tabs ended.
         *
         * @param selectedTabIndex
         *         The index of the tab, which should become selected, as an {@link Integer} value
         * @param previousSelectedTabIndex
         *         The index of the previously selected tab as an {@link Integer} value
         * @param selectionChanged
         *         True, if the selection has changed, false otherwise
         * @param velocity
         *         The velocity of the swipe gesture in pixels per second as a {@link Float} value
         * @param animationDuration
         *         The duration of the swipe animation in milliseconds as a {@link Long} value
         */
        void onSwitchingBetweenTabsEnded(int selectedTabIndex, int previousSelectedTabIndex,
                                         boolean selectionChanged, float velocity,
                                         long animationDuration);

    }

    /**
     * The velocity, which may be reached by a drag gesture at maximum to start a fling animation.
     */
    private final float maxFlingVelocity;

    /**
     * The velocity, which must be reached by a drag gesture in order to start a swipe animation.
     */
    private final float minSwipeVelocity;

    /**
     * The duration of the swipe animation in milliseconds.
     */
    private final long swipeAnimationDuration;

    /**
     * The distance between two neighboring tabs when being swiped in pixels.
     */
    private final int swipedTabDistance;

    /**
     * The index of the currently selected tab.
     */
    private int selectedTabIndex;

    /**
     * The callback, which is notified about the event handler's events.
     */
    private Callback callback;

    /**
     * Returns, whether the threshold of a swiped tab item, which causes the previous or next tab to
     * be selected, has been reached, or not.
     *
     * @return True, if the threshold has been reached, false otherwise
     */
    private boolean isSwipeThresholdReached() {
        return Math.abs(getDragHelper().getDragDistance()) > 4 * swipedTabDistance;
    }

    /**
     * Notifies the callback about a swipe gesture, which is used to switch between neighboring
     * tabs.
     *
     * @param selectedTabIndex
     *         The index of the currently selected tab as an {@link Integer} value
     * @param distance
     *         The distance, the currently selected tab is swiped by, in pixels as a {@link Float}
     *         value
     */
    private void notifyOnSwitchingBetweenTabs(final int selectedTabIndex, final float distance) {
        if (callback != null) {
            callback.onSwitchingBetweenTabs(selectedTabIndex, distance);
        }
    }

    /**
     * Notifies the callback, that a swipe gesture, which was used to switch between neighboring
     * tabs, has ended.
     *
     * @param selectedTabIndex
     *         The index of the tab, which should become selected, as an {@link Integer} value
     * @param previousSelectedTabIndex
     *         The index of the previously selected tab as an {@link Integer} value
     * @param selectionChanged
     *         True, if the selection has changed, false otherwise
     * @param velocity
     *         The velocity of the swipe gesture in pixels per second as a {@link Float} value
     */
    private void notifyOnSwitchingBetweenTabsEnded(final int selectedTabIndex,
                                                   final int previousSelectedTabIndex,
                                                   final boolean selectionChanged,
                                                   final float velocity) {
        if (callback != null) {
            callback.onSwitchingBetweenTabsEnded(selectedTabIndex, previousSelectedTabIndex,
                    selectionChanged, velocity, swipeAnimationDuration);
        }
    }

    /**
     * Creates a new event handler, which allows to handle swipe gestures, which can be used to
     * switch between tabs.
     *
     * @param tabSwitcher
     *         The tab switcher, the event handler belongs to, as an instance of the class {@link
     *         TabSwitcher}. The tab switcher may not be null
     * @param dragThreshold
     *         The drag threshold in pixels as an {@link Integer} value. The drag threshold must be
     *         at least 0
     * @param touchableArea
     *         The bounds of the onscreen area, the handler should take into consideration for
     *         handling touch events, as an instance of the class {@link RectF} or null, if the are
     *         should not be restricted
     * @param animationDuration
     *         The duration of the swipe animation in milliseconds as a {@link Long} value or -1, if
     *         the default duration should be used
     */
    public SwipeGestureEventHandler(@NonNull final TabSwitcher tabSwitcher, final int dragThreshold,
                                    @Nullable final RectF touchableArea,
                                    final long animationDuration) {
        super(tabSwitcher, dragThreshold, touchableArea);
        ViewConfiguration configuration = ViewConfiguration.get(tabSwitcher.getContext());
        this.maxFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        Resources resources = tabSwitcher.getResources();
        this.minSwipeVelocity = resources.getDimensionPixelSize(R.dimen.min_swipe_velocity);
        this.swipeAnimationDuration = animationDuration != -1 ? animationDuration :
                resources.getInteger(R.integer.swipe_animation_duration);
        this.swipedTabDistance = resources.getDimensionPixelSize(R.dimen.swiped_tab_distance);
        this.callback = null;
        this.selectedTabIndex = -1;
    }

    /**
     * Sets the callback, which should be notified about the event handler's events.
     *
     * @param callback
     *         The callback, which should be set, as an instance of the type {@link Callback} or
     *         null, if no callback should be notified
     */
    public final void setCallback(@Nullable final Callback callback) {
        this.callback = callback;
    }

    @Override
    protected final boolean isDraggingAllowed() {
        return (getTabSwitcher().getLayout() == Layout.TABLET ||
                !getTabSwitcher().isSwitcherShown()) && getTabSwitcher().getSelectedTab() != null;
    }

    @Override
    protected final void onTouchEvent() {

    }

    @Override
    protected final void onDown(@NonNull final MotionEvent event) {

    }

    @Override
    protected final void onDrag(@NonNull final MotionEvent event) {
        getDragHelper().update(event.getX());

        if (getDragHelper().hasThresholdBeenReached()) {
            if (selectedTabIndex == -1) {
                selectedTabIndex = getTabSwitcher().getSelectedTabIndex();
            }

            notifyOnSwitchingBetweenTabs(selectedTabIndex, getDragHelper().getDragDistance());
        }
    }

    @Override
    protected final void onUp(@Nullable final MotionEvent event) {
        if (selectedTabIndex != -1) {
            float swipeVelocity = 0;

            if (event != null && getVelocityTracker() != null) {
                int pointerId = event.getPointerId(0);
                getVelocityTracker().computeCurrentVelocity(1000, maxFlingVelocity);
                swipeVelocity = Math.abs(getVelocityTracker().getXVelocity(pointerId));
            }

            int index = selectedTabIndex;
            boolean selectionChanged = false;

            if (swipeVelocity >= minSwipeVelocity || isSwipeThresholdReached()) {
                index = getDragHelper().getDragDistance() > 0 ? selectedTabIndex + 1 :
                        selectedTabIndex - 1;
                selectionChanged = true;
                index = Math.max(Math.min(index, getTabSwitcher().getCount() - 1), 0);
            }

            notifyOnSwitchingBetweenTabsEnded(index, selectedTabIndex, selectionChanged,
                    swipeVelocity >= minSwipeVelocity ? swipeVelocity : 0);
        }

        selectedTabIndex = -1;
        reset();
    }

}