/**
 * @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'
    );
  }
}
