'use strict';

import {Observable} from 'rxjs';
import {Query, QueryRaw, QueryWhere} from 'blow-query';
import * as uuid from 'node-uuid';
import * as _ from './helpers';
import {CollectionOptions} from './interfaces';

export class Collection<T> {

  protected _data: T[];
  protected _idKey: string;
  protected _idGenerator: () => string | number;

  constructor(options?: CollectionOptions) {
    options = options || {};
    this._idKey = options.idKey || '_id';
    this._idGenerator = options.idGenerator || (() => uuid.v4());
    this._data = [];
  }

  get size(): number {
    return this._data.length;
  }

  protected get _data$(): Observable<T> {
    return Observable.from(this._data);
  }

  protected _getId(data: any): any {
    return _.get(data, this._idKey);
  }

  protected _setId(data: any, id?: any): any {
    return _.set(data, this._idKey, id || this._idGenerator());
  }

  protected _hasId(data: any): boolean {
    return _.has(data, this._idKey);
  }

  create(data: any): Observable<T> {
    data = _.dropReference(data);
    if (!this._hasId(data)) {
      data = this._setId(data);
    }
    this._data.push(data);
    return Observable.of(_.dropReference(data));
  }

  update(where: QueryWhere, data: any): Observable<number> {
    return _.where(this._data$, where)
      .map(row => Object.assign(row, data))
      .count();
  }

  updateOrCreate(data: any): Observable<T> {
    if (this._hasId(data)) {
      return this.findById(this._getId(data))
        .defaultIfEmpty()
        .mergeMap(current => {
          if (!current) {
            return this.create(data);
          } else {
            Object.assign(current, data);
            return Observable.of(_.dropReference(current));
          }
        });
    } else {
      return this.create(data);
    }
  }

  count(where?: QueryWhere): Observable<number> {
    where = where || {};
    return _.where(this._data$, where).count();
  }

  destroy(where?: QueryWhere): Observable<number> {
    where = where || {};
    return _.where(this._data$, where, true)
      .toArray()
      .map(rows => {
        const count = this._data.length - rows.length;
        this._data = rows;
        return count;
      });
  }

  destroyById(id: any): Observable<boolean> {
    const where = this._setId({}, id);
    return this.destroy(where).map(num => !!num);
  }

  find(query?: QueryRaw | Query): Observable<T> {
    query = _.prepareQuery(query);
    return _.filter(this._data$, query).map(row => Object.assign({}, row));
  }

  findOne(query?: QueryRaw | Query): Observable<T> {
    query = _.prepareQuery(query);
    return this.find(Object.assign(query, { limit: 1 }));
  }

  findById(id: any): Observable<T> {
    const where = this._setId({}, id);
    return _.filter(this._data$, { where }).map(row => Object.assign({}, row));
  }

  findOrCreate(where: QueryWhere, data: any): Observable<T> {
    return this.findOne({ where }).defaultIfEmpty().mergeMap(current => {
      if (!current) {
        return this.create(data);
      } else {
        return Observable.of(_.dropReference(current));
      }
    });
  }

  exists(id: any): Observable<boolean> {
    return this.findById(id).map(row => !!row).defaultIfEmpty(false);
  }

}
