/** @module resolve */ /** for typedoc */
import {IInjectable, find, filter, map, tail, defaults, extend, pick, omit} from "../common/common";
import {prop, propEq} from "../common/hof";
import {isString, isObject} from "../common/predicates";
import {trace} from "../common/trace";
import {services} from "../common/coreservices";
import {Resolvables, ResolvePolicy, IOptions1} from "./interface";

import {Node} from "../path/module";
import {Resolvable} from "./resolvable";
import {State} from "../state/module";
import {mergeR} from "../common/common";
import {PathFactory} from "../path/pathFactory";

// TODO: make this configurable
let defaultResolvePolicy = ResolvePolicy[ResolvePolicy.LAZY];

interface IPolicies { [key: string]: string; }
interface Promises { [key: string]: Promise<any>; }

export class ResolveContext {

  private _nodeFor: Function;
  private _pathTo: Function;

  constructor(private _path: Node[]) {
    extend(this, {
      _nodeFor(state: State): Node {
        return <Node> find(this._path, propEq('state', state));
      },
      _pathTo(state: State): Node[] {
        return PathFactory.subPath(this._path, state);
      }
    });
  }

  /**
   * Gets the available Resolvables for the last element of this path.
   *
   * @param state the State (within the ResolveContext's Path) for which to get resolvables
   * @param options
   *
   * options.omitOwnLocals: array of property names
   *   Omits those Resolvables which are found on the last element of the path.
   *
   *   This will hide a deepest-level resolvable (by name), potentially exposing a parent resolvable of
   *   the same name further up the state tree.
   *
   *   This is used by Resolvable.resolve() in order to provide the Resolvable access to all the other
   *   Resolvables at its own PathElement level, yet disallow that Resolvable access to its own injectable Resolvable.
   *
   *   This is also used to allow a state to override a parent state's resolve while also injecting
   *   that parent state's resolve:
   *
   *   state({ name: 'G', resolve: { _G: function() { return "G"; } } });
   *   state({ name: 'G.G2', resolve: { _G: function(_G) { return _G + "G2"; } } });
   *   where injecting _G into a controller will yield "GG2"
   */
  getResolvables(state?: State, options?: any): Resolvables {
    options = defaults(options, { omitOwnLocals: [] });

    const path = (state ?  this._pathTo(state) : this._path);
    const last = tail(path);

    return path.reduce((memo, node) => {
      let omitProps = (node === last) ? options.omitOwnLocals : [];
      let filteredResolvables = omit(node.resolves, omitProps);
      return extend(memo, filteredResolvables);
    }, <Resolvables> {});
  }

  /** Inspects a function `fn` for its dependencies.  Returns an object containing any matching Resolvables */
  getResolvablesForFn(fn: IInjectable): { [key: string]: Resolvable } {
    let deps = services.$injector.annotate(<Function> fn, services.$injector.strictDi);
    return <any> pick(this.getResolvables(), deps);
  }

  isolateRootTo(state: State): ResolveContext {
    return new ResolveContext(this._pathTo(state));
  }
  
  addResolvables(resolvables: Resolvables, state: State) {
    extend(this._nodeFor(state).resolves, resolvables);
  }
  
  /** Gets the resolvables declared on a particular state */
  getOwnResolvables(state: State): Resolvables {
    return extend({}, this._nodeFor(state).resolves);
  }
   
  // Returns a promise for an array of resolved path Element promises
  resolvePath(options: IOptions1 = {}): Promise<any> {
    trace.traceResolvePath(this._path, options);
    const promiseForNode = (node: Node) => this.resolvePathElement(node.state, options);
    return services.$q.all(<any> map(this._path, promiseForNode)).then(all => all.reduce(mergeR, {}));
  }

  // returns a promise for all the resolvables on this PathElement
  // options.resolvePolicy: only return promises for those Resolvables which are at 
  // the specified policy, or above.  i.e., options.resolvePolicy === 'lazy' will
  // resolve both 'lazy' and 'eager' resolves.
  resolvePathElement(state: State, options: IOptions1 = {}): Promise<any> {
    // The caller can request the path be resolved for a given policy and "below" 
    let policy: string = options && options.resolvePolicy;
    let policyOrdinal: number = ResolvePolicy[policy || defaultResolvePolicy];
    // Get path Resolvables available to this element
    let resolvables = this.getOwnResolvables(state);

    const matchesRequestedPolicy = resolvable => getPolicy(state.resolvePolicy, resolvable) >= policyOrdinal;
    let matchingResolves = filter(resolvables, matchesRequestedPolicy);

    const getResolvePromise = (resolvable: Resolvable) => resolvable.get(this.isolateRootTo(state), options);
    let resolvablePromises: Promises = <any> map(matchingResolves, getResolvePromise);

    trace.traceResolvePathElement(this, matchingResolves, options);

    return services.$q.all(resolvablePromises);
  } 
  
  
  /**
   * Injects a function given the Resolvables available in the path, from the first node
   * up to the node for the given state.
   *
   * First it resolves all the resolvable depencies.  When they are done resolving, it invokes
   * the function.
   *
   * @return a promise for the return value of the function.
   *
   * @param fn: the function to inject (i.e., onEnter, onExit, controller)
   * @param locals: are the angular $injector-style locals to inject
   * @param options: options (TODO: document)
   */
  invokeLater(fn: IInjectable, locals: any = {}, options: IOptions1 = {}): Promise<any> {
    let resolvables = this.getResolvablesForFn(fn);
    trace.tracePathElementInvoke(tail(this._path), fn, Object.keys(resolvables), extend({when: "Later"}, options));
    const getPromise = (resolvable: Resolvable) => resolvable.get(this, options);
    let promises: Promises = <any> map(resolvables, getPromise);
    
    return services.$q.all(promises).then(() => {
      try {
        return this.invokeNow(fn, locals, options);
      } catch (error) {
        return services.$q.reject(error);
      }
    });
  }

  /**
   * Immediately injects a function with the dependent Resolvables available in the path, from
   * the first node up to the node for the given state.
   *
   * If a Resolvable is not yet resolved, then null is injected in place of the resolvable.
   *
   * @return the return value of the function.
   *
   * @param fn: the function to inject (i.e., onEnter, onExit, controller)
   * @param locals: are the angular $injector-style locals to inject
   * @param options: options (TODO: document)
   */
  // Injects a function at this PathElement level with available Resolvables
  // Does not wait until all Resolvables have been resolved; you must call PathElement.resolve() (or manually resolve each dep) first
  invokeNow(fn: IInjectable, locals: any, options: any = {}) {
    let resolvables = this.getResolvablesForFn(fn);
    trace.tracePathElementInvoke(tail(this._path), fn, Object.keys(resolvables), extend({when: "Now  "}, options));
    let resolvedLocals = map(resolvables, prop("data"));
    return services.$injector.invoke(<Function> fn, options.bind || null, extend({}, locals, resolvedLocals));
  }
}

/**
 * Given a state's resolvePolicy attribute and a resolvable from that state, returns the policy ordinal for the Resolvable
 * Use the policy declared for the Resolve. If undefined, use the policy declared for the State.  If
 * undefined, use the system defaultResolvePolicy.
 * 
 * @param stateResolvePolicyConf The raw resolvePolicy declaration on the state object; may be a String or Object
 * @param resolvable The resolvable to compute the policy for
 */
function getPolicy(stateResolvePolicyConf, resolvable: Resolvable): number {
  // Normalize the configuration on the state to either state-level (a string) or resolve-level (a Map of string:string)
  let stateLevelPolicy: string = <string> (isString(stateResolvePolicyConf) ? stateResolvePolicyConf : null);
  let resolveLevelPolicies: IPolicies = <any> (isObject(stateResolvePolicyConf) ? stateResolvePolicyConf : {});
  let policyName = resolveLevelPolicies[resolvable.name] || stateLevelPolicy || defaultResolvePolicy;
  return ResolvePolicy[policyName];  
}