/**
 * Copyright 2004-present Facebook. All Rights Reserved.
 *
 * @providesModule NavigationLegacyNavigatorRouteStack
 * @flow
 */
'use strict';

const invariant = require('fbjs/lib/invariant');

import type {
  NavigationState,
  NavigationParentState,
} from 'NavigationTypeDefinition';

type IterationCallback = (route: any, index: number, key: string) => void;

function isRouteEmpty(route: any): boolean {
  return (route === undefined || route === null || route === '') || false;
}

function areRouteNodesEqual(
  one: Array<RouteNode>,
  two: Array<RouteNode>,
): boolean {
  if (one === two) {
    return true;
  }

  if (one.length !== two.length) {
    return false;
  }
  for (let ii = 0, jj = one.length; ii < jj; ii++) {
    if (one[ii] !== two[ii]) {
      return false;
    }
  }
  return true;
}

let _nextRouteNodeID = 0;

/**
 * Private struct class that holds the key for a route.
 */
class RouteNode {
  key: string;
  route: any;
  constructor(route: any) {
    // Key value gets bigger incrementally. Developer can compare the
    // keys of two routes then know which route is added to the stack
    // earlier.
    const key = String(_nextRouteNodeID++);
    if (__DEV__ ) {
      // Ensure the immutability of the node.
      Object.defineProperty(this, 'key' , {
        enumerable: true,
        configurable: false,
        writable: false,
        value: key,
      });
      Object.defineProperty(this, 'route' , {
        enumerable: true,
        configurable: false,
        writable: false,
        value: route,
      });
    } else {
      this.key = key;
      this.route = route;
    }
  }

  toNavigationState(): NavigationState {
    const state: NavigationState = this;
    return state;
  }
}

let _nextRouteStackID = 0;

/**
 * The data structure that holds a list of routes and the focused index
 * of the routes. This data structure is implemented as immutable data
 * and mutation (e.g. push, pop...etc) will yields a new instance.
 */
class RouteStack  {
  _index: number;
  _key: string;
  _routeNodes: Array<RouteNode>;

  constructor(index: number, routes: Array<any>) {
    invariant(
      routes.length > 0,
      'routes must not be an empty array'
    );

    invariant(
      index > -1 && index <= routes.length - 1,
      'index out of bound'
    );


    let routeNodes;
    if (routes[0] instanceof RouteNode) {
      // The array is already an array of <RouteNode>.
      routeNodes = routes;
    } else {
      // Wrap the route with <RouteNode>.
      routeNodes = routes.map((route) => {
        invariant(!isRouteEmpty(route), 'route must not be mepty');
        return new RouteNode(route);
      });
    }

    this._routeNodes = routeNodes;
    this._index = index;
    this._key = String(_nextRouteStackID++);
  }

  /* $FlowFixMe - get/set properties not yet supported */
  get size(): number {
    return this._routeNodes.length;
  }

  /* $FlowFixMe - get/set properties not yet supported */
  get index(): number {
    return this._index;
  }

  // Export as...
  toArray(): Array<any> {
    return this._routeNodes.map(node => node.route);
  }

  toNavigationState(): NavigationParentState {
    return {
      index: this._index,
      key: this._key,
      children: this._routeNodes.map(node => node.toNavigationState()),
    };
  }

  get(index: number): any {
    if (index < 0 || index > this._routeNodes.length - 1) {
      return null;
    }
    return this._routeNodes[index].route;
  }

  /**
   * Returns the key associated with the route.
   * When a route is added to a stack, the stack creates a key for this route.
   * The key will persist until the initial stack and its derived stack
   * no longer contains this route.
   */
  keyOf(route: any): ?string {
    if (isRouteEmpty(route)) {
      return null;
    }
    const index = this.indexOf(route);
    return index > -1 ?
      this._routeNodes[index].key :
      null;
  }

  indexOf(route: any): number {
    if (isRouteEmpty(route)) {
      return -1;
    }

    for (let ii = 0, jj = this._routeNodes.length; ii < jj; ii++) {
      let node = this._routeNodes[ii];
      if (node.route === route) {
        return ii;
      }
    }

    return -1;
  }

  slice(begin: ?number, end: ?number): RouteStack {
    // check `begin` and `end` first to keep @flow happy.
    const routeNodes = (end === undefined || end === null) ?
      this._routeNodes.slice(begin || 0) :
      this._routeNodes.slice(begin || 0, end || 0);

    const index = Math.min(this._index, routeNodes.length - 1);
    return this._update(index, routeNodes);
  }

  /**
   * Returns a new stack with the provided route appended,
   * starting at this stack size.
   */
  push(route: any): RouteStack {

    invariant(
      !isRouteEmpty(route),
      'Must supply route to push'
    );

    invariant(this.indexOf(route) === -1, 'route must be unique');

    // When pushing, removes the rest of the routes past the current index.
    const routeNodes = this._routeNodes.slice(0, this._index + 1);
    routeNodes.push(new RouteNode(route));
    return this._update(routeNodes.length - 1, routeNodes);
  }

  /**
   * Returns a new stack a size ones less than this stack,
   * excluding the last index in this stack.
   */
  pop(): RouteStack {
    invariant(
      this._routeNodes.length > 1,
      'should not pop routeNodes stack to empty'
    );

    // When popping, removes the rest of the routes past the current index.
    const routeNodes = this._routeNodes.slice(0, this._index);
    return this._update(routeNodes.length - 1, routeNodes);
  }

  jumpToIndex(index: number): RouteStack {
    invariant(
      index > -1 && index < this._routeNodes.length,
      'jumpToIndex: index out of bound'
    );

    return this._update(index, this._routeNodes);
  }

  /**
   * Replace a route in the navigation stack.
   *
   * `index` specifies the route in the stack that should be replaced.
   * If it's negative, it counts from the back.
   */
  replaceAtIndex(index: number, route: any): RouteStack {
    invariant(
      !isRouteEmpty(route),
      'Must supply route to replace'
    );

    if (this.get(index) === route) {
      return this;
    }

    invariant(this.indexOf(route) === -1, 'route must be unique');

    if (index < 0) {
      index += this._routeNodes.length;
    }

    invariant(
      index > -1 && index < this._routeNodes.length,
      'replaceAtIndex: index out of bound'
    );

    const routeNodes = this._routeNodes.slice(0);
    routeNodes[index] = new RouteNode(route);
    return this._update(index, routeNodes);
  }

  // Iterations
  forEach(callback: IterationCallback, context: ?Object): void {
    this._routeNodes.forEach((node, index) => {
      callback.call(context, node.route, index, node.key);
    });
  }

  mapToArray(callback: IterationCallback, context: ?Object): Array<any> {
    return this._routeNodes.map((node, index) => {
      return callback.call(context, node.route, index, node.key);
    });
  }

  _update(index: number, routeNodes: Array<RouteNode>): RouteStack {
    if (
      this._index === index &&
      areRouteNodesEqual(this._routeNodes, routeNodes)
    ) {
      return this;
    }

    return new RouteStack(index, routeNodes);
  }
}

module.exports = RouteStack;
