/* @flow */
import { statics as StorageStatics } from './';
import { isFunction } from './../../utils';
import StorageReference from './reference';

export const UPLOAD_TASK = 'upload';
export const DOWNLOAD_TASK = 'download';

declare type UploadTaskSnapshotType = {
  bytesTransferred: number,
  downloadURL: string|null,
  metadata: Object, // TODO flow type def for https://firebase.google.com/docs/reference/js/firebase.storage.FullMetadata.html
  ref: StorageReference,
  state: (
    typeof StorageStatics.TaskState.RUNNING
    | typeof StorageStatics.TaskState.PAUSED
    | typeof StorageStatics.TaskState.SUCCESS
    | typeof StorageStatics.TaskState.CANCELLED
    | typeof StorageStatics.TaskState.ERROR
  ),
  task: StorageTask,
  totalBytes: number,
};

declare type FuncSnapshotType = null|(snapshot: UploadTaskSnapshotType) => any;

declare type FuncErrorType = null|(error: Error) => any;

declare type NextOrObserverType = null |
  {
    next?: FuncSnapshotType,
    error?: FuncErrorType,
    complete?:FuncSnapshotType
  } |
  FuncSnapshotType;

/**
 * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask
 */
export default class StorageTask {
  type: typeof UPLOAD_TASK | typeof DOWNLOAD_TASK;
  ref: StorageReference;
  storage: StorageReference.storage;
  path: StorageReference.path;
  then: () => Promise<*>;
  catch: () => Promise<*>;

  constructor(type: typeof UPLOAD_TASK | typeof DOWNLOAD_TASK, promise: Promise<*>, storageRef: StorageReference) {
    this.type = type;
    this.ref = storageRef;
    this.storage = storageRef._module;
    this.path = storageRef.path;

    // 'proxy' original promise
    this.then = promise.then.bind(promise);
    this.catch = promise.catch.bind(promise);
  }

  /**
   * Intercepts a native snapshot result object attaches ref / task instances
   * and calls the original function
   * @returns {Promise.<T>}
   * @private
   */
  _interceptSnapshotEvent(f: ?Function): null | () => * {
    if (!isFunction(f)) return null;
    return (snapshot) => {
      const _snapshot = Object.assign({}, snapshot);
      _snapshot.task = this;
      _snapshot.ref = this.ref;
      return f && f(_snapshot);
    };
  }

  /**
   * Intercepts a error object form native and converts to a JS Error
   * @param f
   * @returns {*}
   * @private
   */
  _interceptErrorEvent(f: ?Function): null | (Error) => * {
    if (!isFunction(f)) return null;
    return (error) => {
      const _error = new Error(error.message);
      // $FlowFixMe
      _error.code = error.code;
      return f && f(_error);
    };
  }

  /**
   *
   * @param nextOrObserver
   * @param error
   * @param complete
   * @returns {function()}
   * @private
   */
  _subscribe(nextOrObserver: NextOrObserverType, error: FuncErrorType, complete: FuncSnapshotType): Function {
    let _error;
    let _next;
    let _complete;

    if (typeof nextOrObserver === 'function') {
      _error = this._interceptErrorEvent(error);
      _next = this._interceptSnapshotEvent(nextOrObserver);
      _complete = this._interceptSnapshotEvent(complete);
    } else if (nextOrObserver) {
      _error = this._interceptErrorEvent(nextOrObserver.error);
      _next = this._interceptSnapshotEvent(nextOrObserver.next);
      _complete = this._interceptSnapshotEvent(nextOrObserver.complete);
    }

    if (_next) {
      this.storage._addListener(
        this.path,
        StorageStatics.TaskEvent.STATE_CHANGED,
        _next,
      );
    }
    if (_error) {
      this.storage._addListener(
        this.path,
        `${this.type}_failure`,
        _error,
      );
    }
    if (_complete) {
      this.storage._addListener(
        this.path,
        `${this.type}_success`,
        _complete,
      );
    }

    return () => {
      if (_next) this.storage._removeListener(this.path, StorageStatics.TaskEvent.STATE_CHANGED, _next);
      if (_error) this.storage._removeListener(this.path, `${this.type}_failure`, _error);
      if (_complete) this.storage._removeListener(this.path, `${this.type}_success`, _complete);
    };
  }

  /**
   *
   * @param event
   * @param nextOrObserver
   * @param error
   * @param complete
   * @returns {function()}
   */
  on(event: string = StorageStatics.TaskEvent.STATE_CHANGED, nextOrObserver: NextOrObserverType, error: FuncErrorType, complete: FuncSnapshotType): Function {
    if (!event) {
      throw new Error('StorageTask.on listener is missing required string argument \'event\'.');
    }

    if (event !== StorageStatics.TaskEvent.STATE_CHANGED) {
      throw new Error(`StorageTask.on event argument must be a string with a value of '${StorageStatics.TaskEvent.STATE_CHANGED}'`);
    }

    // if only event provided return the subscriber function
    if (!nextOrObserver && !error && !complete) {
      return this._subscribe.bind(this);
    }

    return this._subscribe(nextOrObserver, error, complete);
  }

  pause() {
    throw new Error('.pause() is not currently supported by react-native-firebase');
  }

  resume() {
    // todo
    throw new Error('.resume() is not currently supported by react-native-firebase');
  }

  cancel() {
    // todo
    throw new Error('.cancel() is not currently supported by react-native-firebase');
  }
}
