import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {ListWrapper} from 'angular2/src/facade/collection';
import {ChangeDetectorRef} from '../change_detector_ref';
import {Provider, SkipSelfMetadata, OptionalMetadata, Injectable} from 'angular2/src/core/di';

/**
 * A strategy for tracking changes over time to an iterable. Used for {@link NgFor} to
 * respond to changes in an iterable by effecting equivalent changes in the DOM.
 */
export interface IterableDiffer {
  diff(object: any): any;
  onDestroy();
}

/**
  * An optional function passed into {@link NgFor} that defines how to track
  * items in an iterable (e.g. by index or id)
 */
export interface TrackByFn { (index: number, item: any): any; }


/**
 * Provides a factory for {@link IterableDiffer}.
 */
export interface IterableDifferFactory {
  supports(objects: any): boolean;
  create(cdRef: ChangeDetectorRef, trackByFn?: TrackByFn): IterableDiffer;
}

/**
 * A repository of different iterable diffing strategies used by NgFor, NgClass, and others.
 */
@Injectable()
@CONST()
export class IterableDiffers {
  constructor(public factories: IterableDifferFactory[]) {}

  static create(factories: IterableDifferFactory[], parent?: IterableDiffers): IterableDiffers {
    if (isPresent(parent)) {
      var copied = ListWrapper.clone(parent.factories);
      factories = factories.concat(copied);
      return new IterableDiffers(factories);
    } else {
      return new IterableDiffers(factories);
    }
  }

  /**
   * Takes an array of {@link IterableDifferFactory} and returns a provider used to extend the
   * inherited {@link IterableDiffers} instance with the provided factories and return a new
   * {@link IterableDiffers} instance.
   *
   * The following example shows how to extend an existing list of factories,
         * which will only be applied to the injector for this component and its children.
         * This step is all that's required to make a new {@link IterableDiffer} available.
   *
   * ### Example
   *
   * ```
   * @Component({
   *   viewProviders: [
   *     IterableDiffers.extend([new ImmutableListDiffer()])
   *   ]
   * })
   * ```
   */
  static extend(factories: IterableDifferFactory[]): Provider {
    return new Provider(IterableDiffers, {
      useFactory: (parent: IterableDiffers) => {
        if (isBlank(parent)) {
          // Typically would occur when calling IterableDiffers.extend inside of dependencies passed
          // to
          // bootstrap(), which would override default pipes instead of extending them.
          throw new BaseException('Cannot extend IterableDiffers without a parent injector');
        }
        return IterableDiffers.create(factories, parent);
      },
      // Dependency technically isn't optional, but we can provide a better error message this way.
      deps: [[IterableDiffers, new SkipSelfMetadata(), new OptionalMetadata()]]
    });
  }

  find(iterable: any): IterableDifferFactory {
    var factory = this.factories.find(f => f.supports(iterable));
    if (isPresent(factory)) {
      return factory;
    } else {
      throw new BaseException(`Cannot find a differ supporting object '${iterable}'`);
    }
  }
}
