text: /**
   * @flow
   * UploadTask representation wrapper
   */
  import { statics as StorageStatics } from '.';
  import { isFunction } from '../../utils';
  import type Storage from '.';
  import type 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: Storage;
  
    path: string;
  
    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._storage;
      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);
        // $FlowExpectedError
        _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'
      );
    }
  }
  