"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 });
var _ = require("lodash");
var apps_1 = require("../apps");
var cloud_functions_1 = require("../cloud-functions");
var utils_1 = require("../utils");
var index_1 = require("../index");
/** @internal */
exports.provider = 'google.firebase.database';
// NOTE(inlined): Should we relax this a bit to allow staging or alternate implementations of our API?
var databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com');
/**
 * Handle events at a Firebase Realtime Database Reference.
 *
 * 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 `event.params` object. For example, `ref("messages/{messageId}")`
 *    matches changes at `/messages/message1` or `/messages/message2`, resulting
 *    in  `event.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 a
 *    snapshot of the previous event data and information about the user who
 *    triggered the Cloud Function.
 */
function ref(path) {
    var normalized = utils_1.normalizePath(path);
    var databaseURL = index_1.config().firebase.databaseURL;
    if (!databaseURL) {
        throw new Error('Missing expected config value firebase.databaseURL, ' +
            'config is actually' + JSON.stringify(index_1.config()));
    }
    var match = databaseURL.match(databaseURLRegex);
    if (!match) {
        throw new Error('Invalid value for config firebase.databaseURL: ' + databaseURL);
    }
    var subdomain = match[1];
    var resource = "projects/_/instances/" + subdomain + "/refs/" + normalized;
    return new RefBuilder(apps_1.apps(), resource);
}
exports.ref = ref;
/** Builder used to create Cloud Functions for Firebase Realtime Database References. */
var RefBuilder = (function () {
    /** @internal */
    function RefBuilder(apps, resource) {
        this.apps = apps;
        this.resource = resource;
    }
    /** Respond to any write that affects a ref. */
    RefBuilder.prototype.onWrite = function (handler) {
        return this.onOperation(handler, 'ref.write');
    };
    /** Respond to new data on a ref. */
    RefBuilder.prototype.onCreate = function (handler) {
        return this.onOperation(handler, 'ref.create');
    };
    /** Respond to update on a ref. */
    RefBuilder.prototype.onUpdate = function (handler) {
        return this.onOperation(handler, 'ref.update');
    };
    /** Respond to all data being deleted from a ref. */
    RefBuilder.prototype.onDelete = function (handler) {
        return this.onOperation(handler, 'ref.delete');
    };
    RefBuilder.prototype.onOperation = function (handler, eventType) {
        var _this = this;
        var dataConstructor = function (raw) {
            if (raw.data instanceof DeltaSnapshot) {
                return raw.data;
            }
            return new DeltaSnapshot(_this.apps.forMode(raw.auth), _this.apps.admin, raw.data.data, raw.data.delta, resourceToPath(raw.resource));
        };
        return cloud_functions_1.makeCloudFunction({
            provider: exports.provider, handler: handler,
            eventType: eventType,
            resource: this.resource,
            dataConstructor: dataConstructor,
            before: function (event) { return _this.apps.retain(event); },
            after: function (event) { return _this.apps.release(event); },
        });
    };
    return RefBuilder;
}());
exports.RefBuilder = RefBuilder;
/* Utility function to extract database reference from resource string */
/** @internal */
function resourceToPath(resource) {
    var resourceRegex = "projects/([^/]+)/instances/([^/]+)/refs(/.+)?";
    var 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=**}"');
    }
    var project = match[1], path = match[3];
    if (project !== '_') {
        throw new Error("Expect project to be '_' in a Firebase Realtime Database event");
    }
    return path;
}
exports.resourceToPath = resourceToPath;
var DeltaSnapshot = (function () {
    function DeltaSnapshot(app, adminApp, data, delta, path // path will be undefined for the database root
    ) {
        this.app = app;
        this.adminApp = adminApp;
        if (delta !== undefined) {
            this._path = path;
            this._data = data;
            this._delta = delta;
            this._newData = utils_1.applyChange(this._data, this._delta);
        }
    }
    Object.defineProperty(DeltaSnapshot.prototype, "ref", {
        get: function () {
            if (!this._ref) {
                this._ref = this.app.database().ref(this._fullPath());
            }
            return this._ref;
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(DeltaSnapshot.prototype, "adminRef", {
        get: function () {
            if (!this._adminRef) {
                this._adminRef = this.adminApp.database().ref(this._fullPath());
            }
            return this._adminRef;
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(DeltaSnapshot.prototype, "key", {
        get: function () {
            var last = _.last(utils_1.pathParts(this._fullPath()));
            return (!last || last === '') ? null : last;
        },
        enumerable: true,
        configurable: true
    });
    DeltaSnapshot.prototype.val = function () {
        var parts = utils_1.pathParts(this._childPath);
        var source = this._isPrevious ? this._data : this._newData;
        var node = _.cloneDeep(parts.length ? _.get(source, parts, null) : source);
        return this._checkAndConvertToArray(node);
    };
    // TODO(inlined): figure out what to do here
    DeltaSnapshot.prototype.exportVal = function () { return this.val(); };
    // TODO(inlined): figure out what to do here
    DeltaSnapshot.prototype.getPriority = function () {
        return 0;
    };
    DeltaSnapshot.prototype.exists = function () {
        return !_.isNull(this.val());
    };
    DeltaSnapshot.prototype.child = function (childPath) {
        if (!childPath) {
            return this;
        }
        return this._dup(this._isPrevious, childPath);
    };
    Object.defineProperty(DeltaSnapshot.prototype, "previous", {
        get: function () {
            return this._isPrevious ? this : this._dup(true);
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(DeltaSnapshot.prototype, "current", {
        get: function () {
            return this._isPrevious ? this._dup(false) : this;
        },
        enumerable: true,
        configurable: true
    });
    DeltaSnapshot.prototype.changed = function () {
        return utils_1.valAt(this._delta, this._childPath) !== undefined;
    };
    DeltaSnapshot.prototype.forEach = function (action) {
        var _this = this;
        var val = this.val();
        if (_.isPlainObject(val)) {
            return _.some(val, function (value, key) { return action(_this.child(key)) === true; });
        }
        return false;
    };
    DeltaSnapshot.prototype.hasChild = function (childPath) {
        return this.child(childPath).exists();
    };
    DeltaSnapshot.prototype.hasChildren = function () {
        var val = this.val();
        return _.isPlainObject(val) && _.keys(val).length > 0;
    };
    DeltaSnapshot.prototype.numChildren = function () {
        var 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.
     */
    DeltaSnapshot.prototype.toJSON = function () {
        return this.val();
    };
    /* Recursive function to check if keys are numeric & convert node object to array if they are */
    DeltaSnapshot.prototype._checkAndConvertToArray = function (node) {
        if (node === null || typeof node === 'undefined') {
            return null;
        }
        if (typeof node !== 'object') {
            return node;
        }
        var obj = {};
        var numKeys = 0;
        var maxKey = 0;
        var allIntegerKeys = true;
        for (var key in node) {
            if (!node.hasOwnProperty(key)) {
                continue;
            }
            var childNode = node[key];
            obj[key] = this._checkAndConvertToArray(childNode);
            numKeys++;
            var 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.
            var array_1 = [];
            _.forOwn(obj, function (val, key) {
                array_1[key] = val;
            });
            return array_1;
        }
        return obj;
    };
    DeltaSnapshot.prototype._dup = function (previous, childPath) {
        var dup = new DeltaSnapshot(this.app, this.adminApp, undefined, undefined);
        _a = [this._path, this._data, this._delta, this._childPath, this._newData], dup._path = _a[0], dup._data = _a[1], dup._delta = _a[2], dup._childPath = _a[3], dup._newData = _a[4];
        if (previous) {
            dup._isPrevious = true;
        }
        if (childPath) {
            dup._childPath = utils_1.joinPath(dup._childPath, childPath);
        }
        return dup;
        var _a;
    };
    DeltaSnapshot.prototype._fullPath = function () {
        var out = (this._path || '') + '/' + (this._childPath || '');
        return out;
    };
    return DeltaSnapshot;
}());
exports.DeltaSnapshot = DeltaSnapshot;
