import Ember from 'ember';
import layout from '../templates/components/animated-each';
import { task } from 'ember-concurrency';
import { afterRender } from '../concurrency-helpers';
import Move from '../motions/move';
import parallel from '../parallel';

export default Ember.Component.extend({
  layout,
  tagName: '',
  motionService: Ember.inject.service('-ea-motion'),
  duration: 2000,

  init() {
    this._enteringComponents = [];
    this._currentComponents = [];
    this._leavingComponents = [];
    this._prevItems = [];
    this._firstTime = true;
    this.get('motionService').register(this);
    this._super();
  },

  isAnimating: Ember.computed.or('animate.isRunning', 'runThenRemove.isRunning'),

  willDestroyElement() {
    let removedSprites = flatMap(this._currentComponents, component => component.sprites());
    removedSprites.forEach(sprite => sprite.measureInitialBounds());
    this.get('motionService.farMatch').perform([], removedSprites);
    this.get('motionService').unregister(this);
  },

  didReceiveAttrs() {
    let prevItems = this._prevItems;
    let items = this.get('items') || [];
    this._prevItems = items.slice();

    let firstTime = this._firstTime;
    this._firstTime = false;

    if (!firstTime) {
      this._notifyContainer('lock');
    }

    let currentSprites = flatMap(this._currentComponents, component => component.sprites());
    currentSprites.forEach(sprite => sprite.measureInitialBounds());
    currentSprites.forEach(sprite => sprite.lock());
    this.get('animate').perform(prevItems, items, currentSprites, firstTime);
  },

  animate: task(function * (prevItems, items, currentSprites, firstTime) {
    yield afterRender();

    let [keptSprites, removedSprites] = partition(
      currentSprites,
      sprite => this._leavingComponents.indexOf(sprite.component) < 0
    );

    // Briefly unlock everybody
    keptSprites.forEach(sprite => sprite.unlock());
    // so we can measure the final static layout
    let insertedSprites = flatMap(this._enteringComponents, component => component.sprites());
    insertedSprites.forEach(sprite => sprite.measureFinalBounds());
    keptSprites.forEach(sprite => sprite.measureFinalBounds());
    this._notifyContainer('measure', { duration: this.get('duration') });

    // Update our permanent state so that if we're interrupted after
    // this point we are already consistent. AFAIK, we can't be
    // interrupted before this point because Ember won't fire
    // `didReceiveAttrs` multiple times before `afterRender` happens.
    this._updateComponentLists();

    // Then lock everything down
    keptSprites.forEach(sprite => sprite.lock());
    insertedSprites.forEach(sprite => sprite.lock());

    let farMatches = yield this.get('motionService.farMatch').perform(insertedSprites, removedSprites);

    // any removed sprites that matched elsewhere will get handled elsewhere
    removedSprites = removedSprites.filter(sprite => !farMatches.get(sprite))

    // Removal motions have different lifetimes than the kept or
    // inserted motions because an interrupting animation doesn't cancel them.
    this.get('runThenRemove').perform(createRemovalMotions(removedSprites, this.get('duration')), removedSprites);

    yield * parallel(createMotions(firstTime, insertedSprites, keptSprites, farMatches, this.get('duration')), onError);
    keptSprites.forEach(sprite => sprite.unlock());
    insertedSprites.forEach(sprite => sprite.unlock());
    this._notifyContainer('unlock');
  }).restartable(),

  runThenRemove: task(function * (generators, sprites) {
    try {
      yield * parallel(generators);
    } finally {
      sprites.forEach(sprite => sprite.remove());
    }
  }),

  _updateComponentLists() {
    this._currentComponents = this._currentComponents.concat(this._enteringComponents)
      .filter(c => this._leavingComponents.indexOf(c) === -1);
    this._enteringComponents = [];
    this._leavingComponents = [];
  },

  _notifyContainer(method, opts) {
    var target = this.get('notify');
    if (target && target[method]) {
      return target[method](opts);
    }
  },

  actions: {
    childEntering(component) {
      this._enteringComponents.push(component);
    },
    childLeaving(component) {
      this._leavingComponents.push(component);
    }
  }

}).reopenClass({
  positionalParams: ['items']
});


function partition(list, pred) {
  let matched = [];
  let unmatched = [];
  list.forEach(entry => {
    if (pred(entry)) {
      matched.push(entry);
    } else {
      unmatched.push(entry);
    }
  });
  return [matched, unmatched];
}

function flatMap(list, fn) {
  let results = [];
  for (let i = 0; i < list.length; i++) {
    results.push(fn(list[i]));
  }
  return [].concat(...results);
}

function onError(reason) {
  if (reason.name !== 'TaskCancelation') {
    setTimeout(function() {
      throw reason;
    }, 0);
  }
}

function createMotions(firstTime, insertedSprites, keptSprites, farMatches, duration) {
  let generators = [];

  insertedSprites.forEach(sprite => {
    let oldSprite = farMatches.get(sprite);
    if (oldSprite) {
      sprite.startAt(oldSprite);
      sprite.reveal();
      // fixme: mutation
      keptSprites.push(sprite);
    } else {
      if (!firstTime) {
        sprite.startTranslatedBy(1000, 0);
        let move = new Move(sprite, { duration });
        generators.push(move.run());
      }
      sprite.reveal();
    }
  });

  keptSprites.forEach(sprite => {
    let move = new Move(sprite, { duration });
    generators.push(move.run());
  });

  return generators;
}

function createRemovalMotions(removedSprites, duration) {
  let removalGenerators = [];
  removedSprites.forEach(sprite => {
    sprite.append();
    sprite.lock();
    sprite.endTranslatedBy(1000, 0);
    let move = new Move(sprite, { duration });
    removalGenerators.push(move.run());
  });
  return removalGenerators;
}

@@ -5144,79 +5144,83 @@
     
-sprite.reveal();%0A      // fixme: mut
+let move = new Move(sprite, %7B dur
 ation
+ %7D);
 %0A      
-keptSprites.push(sprite
+generators.push(move.run()
 );%0A 
@@ -5229,24 +5229,16 @@
  %7D else 
-%7B%0A      
 if (!fir
@@ -5243,26 +5243,24 @@
 irstTime) %7B%0A
-  
       sprite
@@ -5290,26 +5290,24 @@
 , 0);%0A      
- 
- 
 let move = n
@@ -5339,26 +5339,24 @@
 n %7D);%0A      
-  
 generators.p
@@ -5376,22 +5376,18 @@
 ));%0A    
-  %7D%0A  
+%7D%0A
     spri
@@ -5399,22 +5399,16 @@
 veal();%0A
-    %7D%0A
   %7D);%0A%0A 

refactor toward clearer transitions api