/*
 * 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 androidx.navigation

import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.navigation.Navigator.Name

/**
 * Navigator defines a mechanism for navigating within an app.
 *
 * Each Navigator sets the policy for a specific type of navigation, e.g.
 * [ActivityNavigator] knows how to launch into [destinations][NavDestination]
 * backed by activities using [startActivity][Context.startActivity].
 *
 * Navigators should be able to manage their own back stack when navigating between two
 * destinations that belong to that navigator. The [NavController] manages a back stack of
 * navigators representing the current navigation stack across all navigators.
 *
 * Each Navigator should add the [Navigator.Name annotation][Name] to their class. Any
 * custom attributes used by the associated [destination][NavDestination] subclass should
 * have a name corresponding with the name of the Navigator, e.g., [ActivityNavigator] uses
 * `<declare-styleable name="ActivityNavigator">`
 *
 * @param D the subclass of [NavDestination] used with this Navigator which can be used
 * to hold any special data that will be needed to navigate to that destination.
 * Examples include information about an intent to navigate to other activities,
 * or a fragment class name to instantiate and swap to a new fragment.
 */
public abstract class Navigator<D : NavDestination> {
    /**
     * This annotation should be added to each Navigator subclass to denote the default name used
     * to register the Navigator with a [NavigatorProvider].
     *
     * @see NavigatorProvider.addNavigator
     * @see NavigatorProvider.getNavigator
     */
    @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
    @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
    public annotation class Name(val value: String)

    private var _state: NavigatorState? = null

    /**
     * The state of the Navigator is the communication conduit between the Navigator
     * and the [NavController] that has called [onAttach].
     *
     * It is the responsibility of the Navigator to call [NavigatorState.push]
     * and [NavigatorState.pop] to in order to update the [NavigatorState.backStack] at
     * the appropriate times.
     *
     * @throws IllegalStateException if [isAttached] is `false`
     */
    protected val state: NavigatorState
        get() = checkNotNull(_state) {
            "You cannot access the Navigator's state until the Navigator is attached"
        }

    /**
     * Whether this Navigator is actively being used by a [NavController].
     *
     * This is set to `true` when [onAttach] is called.
     */
    public var isAttached: Boolean = false
        private set

    /**
     * Indicator that this Navigator is actively being used by a [NavController]. This
     * is called when the NavController's state is ready to be restored.
     */
    @CallSuper
    public open fun onAttach(state: NavigatorState) {
        _state = state
        isAttached = true
    }

    /**
     * Construct a new NavDestination associated with this Navigator.
     *
     * Any initialization of the destination should be done in the destination's constructor as
     * it is not guaranteed that every destination will be created through this method.
     * @return a new NavDestination
     */
    public abstract fun createDestination(): D

    /**
     * Navigate to a destination.
     *
     * Requests navigation to a given destination associated with this navigator in
     * the navigation graph. This method generally should not be called directly;
     * [NavController] will delegate to it when appropriate.
     *
     * @param entries destination(s) to navigate to
     * @param navOptions additional options for navigation
     * @param navigatorExtras extras unique to your Navigator.
     */
    @Suppress("UNCHECKED_CAST")
    public open fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ) {
        entries.asSequence().map { backStackEntry ->
            val destination = backStackEntry.destination as? D ?: return@map null
            val navigatedToDestination = navigate(
                destination, backStackEntry.arguments, navOptions, navigatorExtras
            )
            when (navigatedToDestination) {
                null -> null
                destination -> backStackEntry
                else -> {
                    state.createBackStackEntry(
                        navigatedToDestination,
                        navigatedToDestination.addInDefaultArgs(backStackEntry.arguments)
                    )
                }
            }
        }.filterNotNull().forEach { backStackEntry ->
            state.push(backStackEntry)
        }
    }

    /**
     * Informational callback indicating that the given [backStackEntry] has been
     * affected by a [NavOptions.shouldLaunchSingleTop] operation. The entry provided is a new
     * [NavBackStackEntry] instance with all the previous state of the old entry and possibly
     * new arguments.
     */
    @Suppress("UNCHECKED_CAST")
    public open fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) {
        val destination = backStackEntry.destination as? D ?: return
        navigate(destination, null, navOptions { launchSingleTop = true }, null)
        state.onLaunchSingleTop(backStackEntry)
    }

    /**
     * Navigate to a destination.
     *
     * Requests navigation to a given destination associated with this navigator in
     * the navigation graph. This method generally should not be called directly;
     * [NavController] will delegate to it when appropriate.
     *
     * @param destination destination node to navigate to
     * @param args arguments to use for navigation
     * @param navOptions additional options for navigation
     * @param navigatorExtras extras unique to your Navigator.
     * @return The NavDestination that should be added to the back stack or null if
     * no change was made to the back stack (i.e., in cases of single top operations
     * where the destination is already on top of the back stack).
     */
    // TODO Deprecate this method once all call sites are removed
    @Suppress("UNUSED_PARAMETER", "RedundantNullableReturnType")
    public open fun navigate(
        destination: D,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ): NavDestination? = destination

    /**
     * Attempt to pop this navigator's back stack, performing the appropriate navigation.
     *
     * All destinations back to [popUpTo] should be popped off the back stack.
     *
     * @param popUpTo the entry that should be popped off the [NavigatorState.backStack]
     * along with all entries above this entry.
     * @param savedState whether any Navigator specific state associated with [popUpTo] should
     * be saved to later be restored by a call to [navigate] with [NavOptions.shouldRestoreState].
     */
    @Suppress("UNUSED_PARAMETER")
    public open fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
        val backStack = state.backStack.value
        check(backStack.contains(popUpTo)) {
            "popBackStack was called with $popUpTo which does not exist in back stack $backStack"
        }
        val iterator = backStack.listIterator(backStack.size)
        var lastPoppedEntry: NavBackStackEntry? = null
        do {
            if (!popBackStack()) {
                // Quit early if popBackStack() returned false
                break
            }
            lastPoppedEntry = iterator.previous()
        } while (lastPoppedEntry != popUpTo)
        if (lastPoppedEntry != null) {
            state.pop(lastPoppedEntry, savedState)
        }
    }

    /**
     * Attempt to pop this navigator's back stack, performing the appropriate navigation.
     *
     * Implementations should return `true` if navigation
     * was successful. Implementations should return `false` if navigation could not
     * be performed, for example if the navigator's back stack was empty.
     *
     * @return `true` if pop was successful
     */
    // TODO Deprecate this method once all call sites are removed
    public open fun popBackStack(): Boolean = true

    /**
     * Called to ask for a [Bundle] representing the Navigator's state. This will be
     * restored in [onRestoreState].
     */
    public open fun onSaveState(): Bundle? {
        return null
    }

    /**
     * Restore any state previously saved in [onSaveState]. This will be called before
     * any calls to [navigate] or
     * [popBackStack].
     *
     * Calls to [createDestination] should not be dependent on any state restored here as
     * [createDestination] can be called before the state is restored.
     *
     * @param savedState The state previously saved
     */
    public open fun onRestoreState(savedState: Bundle) {}

    /**
     * Interface indicating that this class should be passed to its respective
     * [Navigator] to enable Navigator specific behavior.
     */
    public interface Extras
}
