"use strict";
// The MIT License (MIT)
//
// Copyright (c) 2017 Firebase
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
const apps_1 = require("../apps");
const cloud_functions_1 = require("../cloud-functions");
const utils_1 = require("../utils");
const config_1 = require("../config");
/** @internal */
exports.provider = 'google.firebase.database';
/** @internal */
exports.service = 'firebaseio.com';
// NOTE(inlined): Should we relax this a bit to allow staging or alternate implementations of our API?
const databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com');
/**
 * Selects a database instance that will trigger the function.
 * If omitted, will pick the default database for your project.
 * @param instance The Realtime Database instance to use.
 */
function instance(instance) {
    return _instanceWithOpts(instance, {});
}
exports.instance = instance;
/**
 * Select Firebase Realtime Database Reference to listen to.
 *
 * This method behaves very similarly to the method of the same name in the
 * client and Admin Firebase SDKs. Any change to the Database that affects the
 * data at or below the provided `path` will fire an event in Cloud Functions.
 *
 * There are three important differences between listening to a Realtime
 * Database event in Cloud Functions and using the Realtime Database in the
 * client and Admin SDKs:
 * 1. Cloud Functions allows wildcards in the `path` name. Any `path` component
 *    in curly brackets (`{}`) is a wildcard that matches all strings. The value
 *    that matched a certain invocation of a Cloud Function is returned as part
 *    of the `context.params` object. For example, `ref("messages/{messageId}")`
 *    matches changes at `/messages/message1` or `/messages/message2`, resulting
 *    in  `context.params.messageId` being set to `"message1"` or `"message2"`,
 *    respectively.
 * 2. Cloud Functions do not fire an event for data that already existed before
 *    the Cloud Function was deployed.
 * 3. Cloud Function events have access to more information, including information
 *    about the user who triggered the Cloud Function.
 * @param ref Path of the database to listen to.
 */
function ref(path) {
    return _refWithOpts(path, {});
}
exports.ref = ref;
/** @internal */
function _instanceWithOpts(instance, opts) {
    return new InstanceBuilder(instance, opts);
}
exports._instanceWithOpts = _instanceWithOpts;
class InstanceBuilder {
    /* @internal */
    constructor(instance, opts) {
        this.instance = instance;
        this.opts = opts;
    }
    ref(path) {
        const normalized = utils_1.normalizePath(path);
        return new RefBuilder(apps_1.apps(), () => `projects/_/instances/${this.instance}/refs/${normalized}`, this.opts);
    }
}
exports.InstanceBuilder = InstanceBuilder;
/** @internal */
function _refWithOpts(path, opts) {
    const resourceGetter = () => {
        const normalized = utils_1.normalizePath(path);
        const databaseURL = config_1.firebaseConfig().databaseURL;
        if (!databaseURL) {
            throw new Error('Missing expected firebase config value databaseURL, ' +
                'config is actually' +
                JSON.stringify(config_1.firebaseConfig()) +
                '\n If you are unit testing, please set process.env.FIREBASE_CONFIG');
        }
        const match = databaseURL.match(databaseURLRegex);
        if (!match) {
            throw new Error('Invalid value for config firebase.databaseURL: ' + databaseURL);
        }
        const subdomain = match[1];
        return `projects/_/instances/${subdomain}/refs/${normalized}`;
    };
    return new RefBuilder(apps_1.apps(), resourceGetter, opts);
}
exports._refWithOpts = _refWithOpts;
/** Builder used to create Cloud Functions for Firebase Realtime Database References. */
class RefBuilder {
    /** @internal */
    constructor(apps, triggerResource, opts) {
        this.apps = apps;
        this.triggerResource = triggerResource;
        this.opts = opts;
        this.changeConstructor = (raw) => {
            let [dbInstance, path] = resourceToInstanceAndPath(raw.context.resource.name);
            let before = new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance);
            let after = new DataSnapshot(utils_1.applyChange(raw.data.data, raw.data.delta), path, this.apps.admin, dbInstance);
            return {
                before: before,
                after: after,
            };
        };
    }
    /** Respond to any write that affects a ref. */
    onWrite(handler) {
        return this.onOperation(handler, 'ref.write', this.changeConstructor);
    }
    /** Respond to update on a ref. */
    onUpdate(handler) {
        return this.onOperation(handler, 'ref.update', this.changeConstructor);
    }
    /** Respond to new data on a ref. */
    onCreate(handler) {
        let dataConstructor = (raw) => {
            let [dbInstance, path] = resourceToInstanceAndPath(raw.context.resource.name);
            return new DataSnapshot(raw.data.delta, path, this.apps.admin, dbInstance);
        };
        return this.onOperation(handler, 'ref.create', dataConstructor);
    }
    /** Respond to all data being deleted from a ref. */
    onDelete(handler) {
        let dataConstructor = (raw) => {
            let [dbInstance, path] = resourceToInstanceAndPath(raw.context.resource.name);
            return new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance);
        };
        return this.onOperation(handler, 'ref.delete', dataConstructor);
    }
    onOperation(handler, eventType, dataConstructor) {
        return cloud_functions_1.makeCloudFunction({
            handler,
            provider: exports.provider,
            service: exports.service,
            eventType,
            legacyEventType: `providers/${exports.provider}/eventTypes/${eventType}`,
            triggerResource: this.triggerResource,
            dataConstructor: dataConstructor,
            before: event => this.apps.retain(),
            after: event => this.apps.release(),
            opts: this.opts,
        });
    }
}
exports.RefBuilder = RefBuilder;
/* Utility function to extract database reference from resource string */
/** @internal */
function resourceToInstanceAndPath(resource) {
    let resourceRegex = `projects/([^/]+)/instances/([^/]+)/refs(/.+)?`;
    let match = resource.match(new RegExp(resourceRegex));
    if (!match) {
        throw new Error(`Unexpected resource string for Firebase Realtime Database event: ${resource}. ` +
            'Expected string in the format of "projects/_/instances/{firebaseioSubdomain}/refs/{ref=**}"');
    }
    let [, project, dbInstanceName, path] = match;
    if (project !== '_') {
        throw new Error(`Expect project to be '_' in a Firebase Realtime Database event`);
    }
    let dbInstance = 'https://' + dbInstanceName + '.firebaseio.com';
    return [dbInstance, path];
}
exports.resourceToInstanceAndPath = resourceToInstanceAndPath;
class DataSnapshot {
    constructor(data, path, // path will be undefined for the database root
    app, instance) {
        this.app = app;
        if (instance) {
            // SDK always supplies instance, but user's unit tests may not
            this.instance = instance;
        }
        else if (app) {
            this.instance = app.options.databaseURL;
        }
        else if (process.env.GCLOUD_PROJECT) {
            this.instance =
                'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com';
        }
        this._path = path;
        this._data = data;
    }
    /** Ref returns a reference to the database with full admin access. */
    get ref() {
        if (!this.app) {
            // may be unpopulated in user's unit tests
            throw new Error('Please supply a Firebase app in the constructor for DataSnapshot' +
                ' in order to use the .ref method.');
        }
        if (!this._ref) {
            this._ref = this.app.database(this.instance).ref(this._fullPath());
        }
        return this._ref;
    }
    get key() {
        let last = _.last(utils_1.pathParts(this._fullPath()));
        return !last || last === '' ? null : last;
    }
    val() {
        let parts = utils_1.pathParts(this._childPath);
        let source = this._data;
        let node = _.cloneDeep(parts.length ? _.get(source, parts, null) : source);
        return this._checkAndConvertToArray(node);
    }
    // TODO(inlined): figure out what to do here
    exportVal() {
        return this.val();
    }
    // TODO(inlined): figure out what to do here
    getPriority() {
        return 0;
    }
    exists() {
        return !_.isNull(this.val());
    }
    child(childPath) {
        if (!childPath) {
            return this;
        }
        return this._dup(childPath);
    }
    forEach(action) {
        let val = this.val();
        if (_.isPlainObject(val)) {
            return _.some(val, (value, key) => action(this.child(key)) === true);
        }
        return false;
    }
    hasChild(childPath) {
        return this.child(childPath).exists();
    }
    hasChildren() {
        let val = this.val();
        return _.isPlainObject(val) && _.keys(val).length > 0;
    }
    numChildren() {
        let val = this.val();
        return _.isPlainObject(val) ? Object.keys(val).length : 0;
    }
    /**
     * Prints the value of the snapshot; use '.previous.toJSON()' and '.current.toJSON()' to explicitly see
     * the previous and current values of the snapshot.
     */
    toJSON() {
        return this.val();
    }
    /* Recursive function to check if keys are numeric & convert node object to array if they are */
    _checkAndConvertToArray(node) {
        if (node === null || typeof node === 'undefined') {
            return null;
        }
        if (typeof node !== 'object') {
            return node;
        }
        let obj = {};
        let numKeys = 0;
        let maxKey = 0;
        let allIntegerKeys = true;
        for (let key in node) {
            if (!node.hasOwnProperty(key)) {
                continue;
            }
            let childNode = node[key];
            obj[key] = this._checkAndConvertToArray(childNode);
            numKeys++;
            const integerRegExp = /^(0|[1-9]\d*)$/;
            if (allIntegerKeys && integerRegExp.test(key)) {
                maxKey = Math.max(maxKey, Number(key));
            }
            else {
                allIntegerKeys = false;
            }
        }
        if (allIntegerKeys && maxKey < 2 * numKeys) {
            // convert to array.
            let array = [];
            _.forOwn(obj, (val, key) => {
                array[key] = val;
            });
            return array;
        }
        return obj;
    }
    _dup(childPath) {
        let dup = new DataSnapshot(this._data, undefined, this.app, this.instance);
        [dup._path, dup._childPath] = [this._path, this._childPath];
        if (childPath) {
            dup._childPath = utils_1.joinPath(dup._childPath, childPath);
        }
        return dup;
    }
    _fullPath() {
        let out = (this._path || '') + '/' + (this._childPath || '');
        return out;
    }
}
exports.DataSnapshot = DataSnapshot;