text: /**
   * @flow
   */
  
  import Query from './query.js';
  import Snapshot from './snapshot';
  import Disconnect from './disconnect';
  import ReferenceBase from './../../utils/ReferenceBase';
  
  import {
    promiseOrCallback,
    isFunction,
    isObject,
    isString,
    tryJSONParse,
    tryJSONStringify,
    generatePushID,
  } from './../../utils';
  
  import INTERNALS from './../../internals';
  
  // track all event registrations by path
  let listeners = 0;
  
  /**
   * Enum for event types
   * @readonly
   * @enum {String}
   */
  const ReferenceEventTypes = {
    value: 'value',
    child_added: 'child_added',
    child_removed: 'child_removed',
    child_changed: 'child_changed',
    child_moved: 'child_moved',
  };
  
  /**
   * @typedef {String} ReferenceLocation - Path to location in the database, relative
   * to the root reference. Consists of a path where segments are separated by a
   * forward slash (/) and ends in a ReferenceKey - except the root location, which
   * has no ReferenceKey.
   *
   * @example
   * // root reference location: '/'
   * // non-root reference: '/path/to/referenceKey'
   */
  
  /**
   * @typedef {String} ReferenceKey - Identifier for each location that is unique to that
   * location, within the scope of its parent. The last part of a ReferenceLocation.
   */
  
  /**
   * Represents a specific location in your Database that can be used for
   * reading or writing data.
   *
   * You can reference the root using firebase.database().ref() or a child location
   * by calling firebase.database().ref("child/path").
   *
   * @link https://firebase.google.com/docs/reference/js/firebase.database.Reference
   * @class Reference
   * @extends ReferenceBase
   */
  export default class Reference extends ReferenceBase {
  
    _refListeners: { [listenerId: number]: DatabaseListener };
    _database: Object;
    _query: Query;
  
    constructor(database: Object, path: string, existingModifiers?: Array<DatabaseModifier>) {
      super(path, database);
      this._promise = null;
      this._refListeners = {};
      this._database = database;
      this._query = new Query(this, path, existingModifiers);
      this.log = this._database.log;
      this.log.debug('Created new Reference', this._getRefKey());
    }
  
    /**
     * By calling `keepSynced(true)` on a location, the data for that location will
     * automatically be downloaded and kept in sync, even when no listeners are
     * attached for that location. Additionally, while a location is kept synced,
     *  it will not be evicted from the persistent disk cache.
     *
     * @link https://firebase.google.com/docs/reference/android/com/google/firebase/database/Query.html#keepSynced(boolean)
     * @param bool
     * @returns {*}
     */
    keepSynced(bool: boolean) {
      return this._database._native.keepSynced(this._getRefKey(), this.path, this._query.getModifiers(), bool);
    }
  
    /**
     * Writes data to this Database location.
     *
     * @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#set
     * @param value
     * @param onComplete
     * @returns {Promise}
     */
    set(value: any, onComplete?: Function): Promise {
      return promiseOrCallback(
        this._database._native.set(this.path, this._serializeAnyType(value)),
        onComplete,
      );
    }
  
    /**
     * Sets a priority for the data at this Database location.
     *
     * @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#setPriority
     * @param priority
     * @param onComplete
     * @returns {Promise}
     */
    setPriority(priority: string | number | null, onComplete?: Function): Promise {
      const _priority = this._serializeAnyType(priority);
  
      return promiseOrCallback(
        this._database._native.setPriority(this.path, _priority),
        onComplete,
      );
    }
  
    /**
     * Writes data the Database location. Like set() but also specifies the priority for that data.
     *
     * @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#setWithPriority
     * @param value
     * @param priority
     * @param onComplete
     * @returns {Promise}
     */
    setWithPriority(value: any, priority: string | number | null, onComplete?: Function): Promise {
      const _value = this._serializeAnyType(value);
      const _priority = this._serializeAnyType(priority);
  
      return promiseOrCallback(
        this._database._native.setWithPriority(this.path, _value, _priority),
        onComplete,
      );
    }
  
    /**
     * Writes multiple values to the Database at once.
     *
     * @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#update
     * @param val
     * @param onComplete
     * @returns {Promise}
     */
    update(val: Object, onComplete?: Function): Promise {
      const value = this._serializeObject(val);
  
      return promiseOrCallback(
        this._database._native.update(this.path, value),
        onComplete,
      );
    }
  
    /**
     * Removes the data at this Database location.
     *
     * @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#remove
     * @param onComplete
     * @return {Promise}
     */
    remove(onComplete?: Function): Promise {
      return promiseOrCallback(
        this._database._native.remove(this.path),
        onComplete,
      );
    }
  
    /**
     * Atomically modifies the data at this location.
     *
     * @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
     * @param transactionUpdate
     * @param onComplete
     * @param applyLocally
     */
    transaction(
      transactionUpdate: Function,
      onComplete: (error: ?Error, committed: boolean, snapshot: ?Snapshot) => *,
      applyLocally: boolean = false,
    ) {
      if (!isFunction(transactionUpdate)) {
        return Promise.reject(
          new Error('Missing transactionUpdate function argument.'),
        );
      }
  
      return new Promise((resolve, reject) => {
        const onCompleteWrapper = (error, committed, snapshotData) => {
          if (isFunction(onComplete)) {
            if (error) return onComplete(error, committed, null);
            return onComplete(null, committed, new Snapshot(this, snapshotData));
          }
  
          if (error) return reject(error);
          return resolve({ committed, snapshot: new Snapshot(this, snapshotData) });
        };
  
        // start the transaction natively
        this._database._transactionHandler.add(this, transactionUpdate, onCompleteWrapper, applyLocally);
      });
    }
  
  
    /**
     *
     * @param eventName
     * @param successCallback
     * @param cancelOrContext
     * @param context
     * @returns {Promise.<any>}
     */
    once(
      eventName: string = 'value',
      successCallback: (snapshot: Object) => void,
      cancelOrContext: (error: FirebaseError) => void,
      context?: Object,
    ) {
      return this._database._native.once(this._getRefKey(), this.path, this._query.getModifiers(), eventName)
        .then(({ snapshot }) => {
          const _snapshot = new Snapshot(this, snapshot);
  
          if (isFunction(successCallback)) {
            if (isObject(cancelOrContext)) successCallback.bind(cancelOrContext)(_snapshot);
            if (context && isObject(context)) successCallback.bind(context)(_snapshot);
            successCallback(_snapshot);
          }
  
          return _snapshot;
        })
        .catch((error) => {
          if (isFunction(cancelOrContext)) return cancelOrContext(error);
          return error;
        });
    }
  
    /**
     *
     * @param value
     * @param onComplete
     * @returns {*}
     */
    push(value: any, onComplete?: Function) {
      if (value === null || value === undefined) {
        return new Reference(this._database, `${this.path}/${generatePushID(this._database.serverTimeOffset)}`);
      }
  
      const newRef = new Reference(this._database, `${this.path}/${generatePushID(this._database.serverTimeOffset)}`);
      const promise = newRef.set(value);
  
      // if callback provided then internally call the set promise with value
      if (isFunction(onComplete)) {
        return promise
          .then(() => onComplete(null, newRef))
          .catch(error => onComplete(error, null));
      }
  
      // otherwise attach promise to 'thenable' reference and return the
      // new reference
      newRef._setThenable(promise);
      return newRef;
    }
  
    /**
     * MODIFIERS
     */
  
    /**
     *
     * @returns {Reference}
     */
    orderByKey(): Reference {
      return this.orderBy('orderByKey');
    }
  
    /**
     *
     * @returns {Reference}
     */
    orderByPriority(): Reference {
      return this.orderBy('orderByPriority');
    }
  
    /**
     *
     * @returns {Reference}
     */
    orderByValue(): Reference {
      return this.orderBy('orderByValue');
    }
  
    /**
     *
     * @param key
     * @returns {Reference}
     */
    orderByChild(key: string): Reference {
      return this.orderBy('orderByChild', key);
    }
  
    /**
     *
     * @param name
     * @param key
     * @returns {Reference}
     */
    orderBy(name: string, key?: string): Reference {
      const newRef = new Reference(this._database, this.path, this._query.getModifiers());
      newRef._query.orderBy(name, key);
      return newRef;
    }
  
    /**
     * LIMITS
     */
  
    /**
     *
     * @param limit
     * @returns {Reference}
     */
    limitToLast(limit: number): Reference {
      return this.limit('limitToLast', limit);
    }
  
    /**
     *
     * @param limit
     * @returns {Reference}
     */
    limitToFirst(limit: number): Reference {
      return this.limit('limitToFirst', limit);
    }
  
    /**
     *
     * @param name
     * @param limit
     * @returns {Reference}
     */
    limit(name: string, limit: number): Reference {
      const newRef = new Reference(this._database, this.path, this._query.getModifiers());
      newRef._query.limit(name, limit);
      return newRef;
    }
  
    /**
     * FILTERS
     */
  
    /**
     *
     * @param value
     * @param key
     * @returns {Reference}
     */
    equalTo(value: any, key?: string): Reference {
      return this.filter('equalTo', value, key);
    }
  
    /**
     *
     * @param value
     * @param key
     * @returns {Reference}
     */
    endAt(value: any, key?: string): Reference {
      return this.filter('endAt', value, key);
    }
  
    /**
     *
     * @param value
     * @param key
     * @returns {Reference}
     */
    startAt(value: any, key?: string): Reference {
      return this.filter('startAt', value, key);
    }
  
    /**
     *
     * @param name
     * @param value
     * @param key
     * @returns {Reference}
     */
    filter(name: string, value: any, key?: string): Reference {
      const newRef = new Reference(this._database, this.path, this._query.getModifiers());
      newRef._query.filter(name, value, key);
      return newRef;
    }
  
    /**
     *
     * @returns {Disconnect}
     */
    onDisconnect() {
      return new Disconnect(this.path);
    }
  
    /**
     * Creates a Reference to a child of the current Reference, using a relative path.
     * No validation is performed on the path to ensure it has a valid format.
     * @param {String} path relative to current ref's location
     * @returns {!Reference} A new Reference to the path provided, relative to the current
     * Reference
     * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#child}
     */
    child(path: string) {
      return new Reference(this._database, `${this.path}/${path}`);
    }
  
    /**
     * Return the ref as a path string
     * @returns {string}
     */
    toString(): string {
      return this.path;
    }
  
    /**
     * Returns whether another Reference represent the same location and are from the
     * same instance of firebase.app.App - multiple firebase apps not currently supported.
     * @param {Reference} otherRef - Other reference to compare to this one
     * @return {Boolean} Whether otherReference is equal to this one
     *
     * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#isEqual}
     */
    isEqual(otherRef: Reference): boolean {
      return !!otherRef
        && otherRef.constructor === Reference
        && otherRef.key === this.key
        && this._query.queryIdentifier() === otherRef._query.queryIdentifier();
    }
  
    /**
     * GETTERS
     */
  
    /**
     * The parent location of a Reference, or null for the root Reference.
     * @type {Reference}
     *
     * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#parent}
     */
    get parent(): Reference | null {
      if (this.path === '/') return null;
      return new Reference(this._database, this.path.substring(0, this.path.lastIndexOf('/')));
    }
  
    /**
     * A reference to itself
     * @type {!Reference}
     *
     * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#ref}
     */
    get ref(): Reference {
      return this;
    }
  
    /**
     * Reference to the root of the database: '/'
     * @type {!Reference}
     *
     * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#root}
     */
    get root(): Reference {
      return new Reference(this._database, '/');
    }
  
    /**
     * Access then method of promise if set
     * @return {*}
     */
    get then() {
      if (this._promise && this._promise.then) {
        return this._promise.then.bind(this._promise)(() => {
          this._promise = null;
          return this;
        });
      }
  
      return undefined;
    }
  
    /**
     * Access catch method of promise if set
     * @return {*}
     */
    get catch() {
      if (this._promise && this._promise.catch) {
        return this._promise.catch.bind(this._promise)((exception) => {
          this._promise = null;
          return Promise.reject(exception);
        });
      }
  
      return undefined;
    }
  
    /**
     * INTERNALS
     */
  
    /**
     * Generate a unique registration key.
     *
     * @return {string}
     */
    _getRegistrationKey(eventType) {
      return `$${this._database._appName}$/${this.path}$${this._query.queryIdentifier()}$${listeners}$${eventType}`;
    }
  
    /**
     * Generate a string that uniquely identifies this
     * combination of path and query modifiers
     *
     * @return {string}
     * @private
     */
    _getRefKey() {
      return `$${this._database._appName}$/${this.path}$${this._query.queryIdentifier()}`;
    }
  
    /**
     * Return instance of db logger
     */
    get log() {
      return this._database.log;
    }
  
    /**
     * Set the promise this 'thenable' reference relates to
     * @param promise
     * @private
     */
    _setThenable(promise) {
      this._promise = promise;
    }
  
  
    /**
     *
     * @param obj
     * @returns {Object}
     * @private
     */
    _serializeObject(obj: Object) {
      if (!isObject(obj)) return obj;
  
      // json stringify then parse it calls toString on Objects / Classes
      // that support it i.e new Date() becomes a ISO string.
      return tryJSONParse(tryJSONStringify(obj));
    }
  
    /**
     *
     * @param value
     * @returns {*}
     * @private
     */
    _serializeAnyType(value: any) {
      if (isObject(value)) {
        return {
          type: 'object',
          value: this._serializeObject(value),
        };
      }
  
      return {
        type: typeof value,
        value,
      };
    }
  
    /**
     * Register a listener for data changes at the current ref's location.
     * The primary method of reading data from a Database.
     *
     * Listeners can be unbound using {@link off}.
     *
     * Event Types:
     *
     * - value: {@link callback}.
     * - child_added: {@link callback}
     * - child_removed: {@link callback}
     * - child_changed: {@link callback}
     * - child_moved: {@link callback}
     *
     * @param {ReferenceEventType} eventType - Type of event to attach a callback for.
     * @param {ReferenceEventCallback} callback - Function that will be called
     * when the event occurs with the new data.
     * @param {cancelCallbackOrContext=} cancelCallbackOrContext - Optional callback that is called
     * if the event subscription fails. {@link cancelCallbackOrContext}
     * @param {*=} context - Optional object to bind the callbacks to when calling them.
     * @returns {ReferenceEventCallback} callback function, unmodified (unbound), for
     * convenience if you want to pass an inline function to on() and store it later for
     * removing using off().
     *
     * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#on}
     */
    on(eventType: string, callback: () => any, cancelCallbackOrContext?: () => any, context?: Object): Function {
      if (!eventType) {
        throw new Error('Query.on failed: Function called with 0 arguments. Expects at least 2.');
      }
  
      if (!isString(eventType) || !ReferenceEventTypes[eventType]) {
        throw new Error(`Query.on failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
      }
  
      if (!callback) {
        throw new Error('Query.on failed: Function called with 1 argument. Expects at least 2.');
      }
  
      if (!isFunction(callback)) {
        throw new Error('Query.on failed: Second argument must be a valid function.');
      }
  
      if (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext) && !isObject(context) && !isObject(cancelCallbackOrContext)) {
        throw new Error('Query.on failed: Function called with 3 arguments, but third optional argument `cancelCallbackOrContext` was not a function.');
      }
  
      if (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext) && context) {
        throw new Error('Query.on failed: Function called with 4 arguments, but third optional argument `cancelCallbackOrContext` was not a function.');
      }
  
      const eventRegistrationKey = this._getRegistrationKey(eventType);
      const registrationCancellationKey = `${eventRegistrationKey}$cancelled`;
      const _context = (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext)) ? cancelCallbackOrContext : context;
      const registrationObj = {
        eventType,
        ref: this,
        path: this.path,
        key: this._getRefKey(),
        appName: this._database._appName,
        eventRegistrationKey,
      };
  
      this._syncTree.addRegistration(registrationObj, _context ? callback.bind(_context) : callback);
  
      if (isFunction(cancelCallbackOrContext)) {
        // cancellations have their own separate registration
        // as these are one off events, and they're not guaranteed
        // to occur either, only happens on failure to register on native
        this._syncTree.addRegistration(
          {
            ref: this,
            once: true,
            path: this.path,
            key: this._getRefKey(),
            appName: this._database._appName,
            eventType: `${eventType}$cancelled`,
            eventRegistrationKey: registrationCancellationKey,
          },
          _context ? cancelCallbackOrContext.bind(_context) : cancelCallbackOrContext,
        );
      }
  
      // initialise the native listener if not already listening
      this._database._native.on({
        eventType,
        path: this.path,
        key: this._getRefKey(),
        appName: this._database._appName,
        modifiers: this._query.getModifiers(),
        hasCancellationCallback: isFunction(cancelCallbackOrContext),
        registration: {
          eventRegistrationKey,
          key: registrationObj.key,
          registrationCancellationKey,
        },
      });
  
      // increment number of listeners - just s short way of making
      // every registration unique per .on() call
      listeners += 1;
  
      // return original unbound successCallback for
      // the purposes of calling .off(eventType, callback) at a later date
      return callback;
    }
  
    /**
     * Detaches a callback previously attached with on().
     *
     * Detach a callback previously attached with on(). Note that if on() was called
     * multiple times with the same eventType and callback, the callback will be called
     * multiple times for each event, and off() must be called multiple times to
     * remove the callback. Calling off() on a parent listener will not automatically
     * remove listeners registered on child nodes, off() must also be called on any
     * child listeners to remove the callback.
     *
     *  If a callback is not specified, all callbacks for the specified eventType will be removed.
     * Similarly, if no eventType or callback is specified, all callbacks for the Reference will be removed.
     * @param eventType
     * @param originalCallback
     */
    off(eventType?: string = '', originalCallback?: () => any) {
      if (!arguments.length) {
        // Firebase Docs:
        //     if no eventType or callback is specified, all callbacks for the Reference will be removed.
        return this._syncTree.removeListenersForRegistrations(this._syncTree.getRegistrationsByPath(this.path));
      }
  
      /*
       * VALIDATE ARGS
       */
      if (eventType && (!isString(eventType) || !ReferenceEventTypes[eventType])) {
        throw new Error(`Query.off failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
      }
  
      if (originalCallback && !isFunction(originalCallback)) {
        throw new Error('Query.off failed: Function called with 2 arguments, but second optional argument was not a function.');
      }
  
      // Firebase Docs:
      //     Note that if on() was called
      //     multiple times with the same eventType and callback, the callback will be called
      //     multiple times for each event, and off() must be called multiple times to
      //     remove the callback.
      // Remove only a single registration
      if (eventType && originalCallback) {
        const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType);
  
        // remove the paired cancellation registration if any exist
        this._syncTree.removeListenersForRegistrations([`${registrations[0]}$cancelled`]);
  
        // remove only the first registration to match firebase web sdk
        // call multiple times to remove multiple registrations
        return this._syncTree.removeListenerRegistrations(originalCallback, [registrations[0]]);
      }
  
      // Firebase Docs:
      //     If a callback is not specified, all callbacks for the specified eventType will be removed.
      const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType);
  
      this._syncTree.removeListenersForRegistrations(
        this._syncTree.getRegistrationsByPathEvent(this.path, `${eventType}$cancelled`),
      );
  
      return this._syncTree.removeListenersForRegistrations(registrations);
    }
  
    get _syncTree() {
      return INTERNALS.SyncTree;
    }
  }
  