import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import {EventEmitter, Handler, Subscription} from './emitter';
import {ArrayEmitter as IArrayEmitter} from './types';

const ArrayProto = Array.prototype;

export class ArrayEmitter<T> extends Array<T> implements IArrayEmitter<T> {

  public $emitter: EventEmitter;
  public $subject: BehaviorSubject<ArrayEmitter<T>>;
  public on: {(name: string, handler: Handler): void};
  public off: {(name: string, handler: Handler): void};
  public listen: {(name: string, handler: Handler): Subscription};
  public once: {(name: string, handler: Handler): void};
  public addListener: {(name: string, handler: Handler): void};
  public removeListener: {(name: string, handler: Handler): void};

  constructor(...args: any[]) {
    super(...args);
    let arr = this;

    // Non enumerable so that we can send the array over postMessage
    Object.defineProperty(arr, '$emitter', {
      enumerable: false,
      value: new EventEmitter(),
    });

    Object.defineProperty(arr, '$subject', {
      enumerable: false,
      value: new BehaviorSubject(this),
    });

    this.$emitter.on('update', () => {
      this.$subject.next(this);
    });

    // Generate our Array Emitter methods
    let ArrayEmitterMethods: any = {};
    ['on', 'off', 'listen', 'once', 'addListener', 'removeListener'].forEach(key => {
      ArrayEmitterMethods[key] = {
        enumerable: false,
        value: (...methodArgs: any[]) => {
          return this.$emitter[key](...methodArgs);
        },
      };
    });

    // These methods create new arrays
    ['reverse', 'sort', 'splice'].forEach(key => {
      ArrayEmitterMethods[key] = {
        enumerable: false,
        value: (...methodArgs: any[]) => {
          let emitter = this.$emitter;
          emitter.on('update', () => {
            ArrayProto[key].apply(this, methodArgs);
          });

          ArrayProto[key].apply(this, methodArgs);
          return this;
        },
      };
    });

    ['filter', 'map', 'slice'].forEach(key => {
      ArrayEmitterMethods[key] = {
        enumerable: false,
        value: (...methodArgs: any[]) => {
          let newArr: any = new ArrayEmitter();
          let emitter = this.$emitter;
          emitter.on('update', () => {
            updateNewArr(ArrayProto[key].apply(this, methodArgs));
            newArr.$emitter.emit('update', newArr);
          });

          updateNewArr(ArrayProto[key].apply(this, methodArgs));

          function updateNewArr(updatedArr: any[]) {
            newArr.length = 0;
            for (let i = 0; i < updatedArr.length; i++) { // tslint:disable-line prefer-for-of
              newArr.push(updatedArr[i]);
            }
          }

          return newArr;
        },
      };
    });

    ArrayEmitterMethods.concat = {
      enumerable: false,
      value: (...methodArgs: any[]) => {
        let newArr = new ArrayEmitter();
        let allArrs = [this, ...methodArgs];
        let qries = allArrs.filter((obj: any) => (obj.$emitter && Array.isArray(obj)));
        qries.forEach((qry: ArrayEmitter<any>) => {
          let emitter = qry.$emitter;
          emitter.on('update', () => {
            updateConcat();
            newArr.$emitter.emit('update', newArr);
          });
        });

        updateConcat();

        return newArr;

        function updateConcat() {
          let concatted = ArrayProto.concat.call(this, ...methodArgs);
          newArr.length = 0;
          newArr.push(...concatted);
        }
      },
    };

    Object.defineProperties(arr, ArrayEmitterMethods);
  }
}
