text: /* @flow */
  
  import invariant from 'fbjs/lib/invariant';
  
  import type { NavigationRoute, NavigationState } from './TypeDefinition';
  
  /**
   * Utilities to perform atomic operation with navigate state and routes.
   *
   * ```javascript
   * const state1 = {key: 'screen 1'};
   * const state2 = NavigationStateUtils.push(state1, {key: 'screen 2'});
   * ```
   */
  const StateUtils = {
    /**
     * Gets a route by key. If the route isn't found, returns `null`.
     */
    get(state: NavigationState, key: string): ?NavigationRoute {
      return state.routes.find((route: *) => route.key === key) || null;
    },
  
    /**
     * Returns the first index at which a given route's key can be found in the
     * routes of the navigation state, or -1 if it is not present.
     */
    indexOf(state: NavigationState, key: string): number {
      return state.routes.map((route: *) => route.key).indexOf(key);
    },
  
    /**
     * Returns `true` at which a given route's key can be found in the
     * routes of the navigation state.
     */
    has(state: NavigationState, key: string): boolean {
      return !!state.routes.some((route: *) => route.key === key);
    },
  
    /**
     * Pushes a new route into the navigation state.
     * Note that this moves the index to the positon to where the last route in the
     * stack is at.
     */
    push(state: NavigationState, route: NavigationRoute): NavigationState {
      invariant(
        StateUtils.indexOf(state, route.key) === -1,
        'should not push route with duplicated key %s',
        route.key,
      );
  
      const routes = state.routes.slice();
      routes.push(route);
  
      return {
        ...state,
        index: routes.length - 1,
        routes,
      };
    },
  
    /**
     * Pops out a route from the navigation state.
     * Note that this moves the index to the positon to where the last route in the
     * stack is at.
     */
    pop(state: NavigationState): NavigationState {
      if (state.index <= 0) {
        // [Note]: Over-popping does not throw error. Instead, it will be no-op.
        return state;
      }
      const routes = state.routes.slice(0, -1);
      return {
        ...state,
        index: routes.length - 1,
        routes,
      };
    },
  
    /**
     * Sets the focused route of the navigation state by index.
     */
    jumpToIndex(state: NavigationState, index: number): NavigationState {
      if (index === state.index) {
        return state;
      }
  
      invariant(!!state.routes[index], 'invalid index %s to jump to', index);
  
      return {
        ...state,
        index,
      };
    },
  
    /**
     * Sets the focused route of the navigation state by key.
     */
    jumpTo(state: NavigationState, key: string): NavigationState {
      const index = StateUtils.indexOf(state, key);
      return StateUtils.jumpToIndex(state, index);
    },
  
    /**
     * Sets the focused route to the previous route.
     */
    back(state: NavigationState): NavigationState {
      const index = state.index - 1;
      const route = state.routes[index];
      return route ? StateUtils.jumpToIndex(state, index) : state;
    },
  
    /**
     * Sets the focused route to the next route.
     */
    forward(state: NavigationState): NavigationState {
      const index = state.index + 1;
      const route = state.routes[index];
      return route ? StateUtils.jumpToIndex(state, index) : state;
    },
  
    /**
     * Replace a route by a key.
     * Note that this moves the index to the positon to where the new route in the
     * stack is at.
     */
    replaceAt(
      state: NavigationState,
      key: string,
      route: NavigationRoute,
    ): NavigationState {
      const index = StateUtils.indexOf(state, key);
      return StateUtils.replaceAtIndex(state, index, route);
    },
  
    /**
     * Replace a route by a index.
     * Note that this moves the index to the positon to where the new route in the
     * stack is at.
     */
    replaceAtIndex(
      state: NavigationState,
      index: number,
      route: NavigationRoute,
    ): NavigationState {
      invariant(
        !!state.routes[index],
        'invalid index %s for replacing route %s',
        index,
        route.key,
      );
  
      if (state.routes[index] === route) {
        return state;
      }
  
      const routes = state.routes.slice();
      routes[index] = route;
  
      return {
        ...state,
        index,
        routes,
      };
    },
  
    /**
     * Resets all routes.
     * Note that this moves the index to the positon to where the last route in the
     * stack is at if the param `index` isn't provided.
     */
    reset(
      state: NavigationState,
      routes: Array<NavigationRoute>,
      index?: number,
    ): NavigationState {
      invariant(
        routes.length && Array.isArray(routes),
        'invalid routes to replace',
      );
  
      const nextIndex: number = index === undefined ? routes.length - 1 : index;
  
      if (state.routes.length === routes.length && state.index === nextIndex) {
        const compare = (route: *, ii: *) => routes[ii] === route;
        if (state.routes.every(compare)) {
          return state;
        }
      }
  
      invariant(!!routes[nextIndex], 'invalid index %s to reset', nextIndex);
  
      return {
        ...state,
        index: nextIndex,
        routes,
      };
    },
  };
  
  export default StateUtils;
  