"use strict";
/**
 * Copyright 2018, OpenCensus Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const defaultLogger = require("../common/console-logger");
const types_1 = require("../metrics/export/types");
const bucket_boundaries_1 = require("./bucket-boundaries");
const metric_utils_1 = require("./metric-utils");
const recorder_1 = require("./recorder");
const types_2 = require("./types");
const RECORD_SEPARATOR = String.fromCharCode(30);
const UNIT_SEPARATOR = String.fromCharCode(31);
// String that has only printable characters
const invalidString = /[^\u0020-\u007e]/;
class BaseView {
    /**
     * Creates a new View instance. This constructor is used by Stats. User should
     * prefer using Stats.createView() instead.
     * @param name The view name
     * @param measure The view measure
     * @param aggregation The view aggregation type
     * @param tagsKeys The Tags' keys that view will have
     * @param description The view description
     * @param bucketBoundaries The view bucket boundaries for a distribution
     * aggregation type
     * @param logger
     */
    constructor(name, measure, aggregation, tagsKeys, description, bucketBoundaries, logger = defaultLogger) {
        /**
         * A map of stringified tags representing columns labels or tag keys, concept
         * similar to dimensions on multidimensional modeling, to AggregationData.
         * If no Tags are provided, then, all data is recorded in a single
         * aggregation.
         */
        this.rows = {};
        /** true if the view was registered */
        this.registered = false;
        if (aggregation === types_2.AggregationType.DISTRIBUTION && !bucketBoundaries) {
            throw new Error('No bucketBoundaries specified');
        }
        this.logger = logger.logger();
        this.name = name;
        this.description = description;
        this.measure = measure;
        this.columns = tagsKeys;
        this.aggregation = aggregation;
        this.startTime = Date.now();
        this.bucketBoundaries = new bucket_boundaries_1.BucketBoundaries(bucketBoundaries);
        this.metricDescriptor = metric_utils_1.MetricUtils.viewToMetricDescriptor(this);
    }
    /** Gets the view's tag keys */
    getColumns() {
        return this.columns;
    }
    /**
     * Records a measurement in the proper view's row. This method is used by
     * Stats. User should prefer using Stats.record() instead.
     *
     * Measurements with measurement type INT64 will have its value truncated.
     * @param measurement The measurement to record
     */
    recordMeasurement(measurement) {
        // Checks if measurement has valid tags
        if (this.invalidTags(measurement.tags)) {
            return;
        }
        // Checks if measurement has all tags in views
        for (const tagKey of this.columns) {
            if (!Object.keys(measurement.tags).some((key) => key === tagKey)) {
                return;
            }
        }
        const encodedTags = this.encodeTags(measurement.tags);
        if (!this.rows[encodedTags]) {
            this.rows[encodedTags] = this.createAggregationData(measurement.tags);
        }
        recorder_1.Recorder.addMeasurement(this.rows[encodedTags], measurement);
    }
    /**
     * Encodes a Tags object into a key sorted string.
     * @param tags The tags to encode
     */
    encodeTags(tags) {
        return Object.keys(tags)
            .sort()
            .map(tagKey => {
            return tagKey + UNIT_SEPARATOR + tags[tagKey];
        })
            .join(RECORD_SEPARATOR);
    }
    /**
     * Checks if tag keys and values have only printable characters.
     * @param tags The tags to be checked
     */
    invalidTags(tags) {
        return Object.keys(tags).some(tagKey => {
            return invalidString.test(tagKey) || invalidString.test(tags[tagKey]);
        });
    }
    /**
     * Creates an empty aggregation data for a given tags.
     * @param tags The tags for that aggregation data
     */
    createAggregationData(tags) {
        const aggregationMetadata = { tags, timestamp: Date.now() };
        const { buckets, bucketCounts } = this.bucketBoundaries;
        const bucketsCopy = Object.assign([], buckets);
        const bucketCountsCopy = Object.assign([], bucketCounts);
        switch (this.aggregation) {
            case types_2.AggregationType.DISTRIBUTION:
                return Object.assign({}, aggregationMetadata, { type: types_2.AggregationType.DISTRIBUTION, startTime: this.startTime, count: 0, sum: 0, mean: null, stdDeviation: null, sumOfSquaredDeviation: null, buckets: bucketsCopy, bucketCounts: bucketCountsCopy });
            case types_2.AggregationType.SUM:
                return Object.assign({}, aggregationMetadata, { type: types_2.AggregationType.SUM, value: 0 });
            case types_2.AggregationType.COUNT:
                return Object.assign({}, aggregationMetadata, { type: types_2.AggregationType.COUNT, value: 0 });
            default:
                return Object.assign({}, aggregationMetadata, { type: types_2.AggregationType.LAST_VALUE, value: undefined });
        }
    }
    /**
     * Gets view`s metric
     * @returns {Metric}
     */
    getMetric() {
        const { type } = this.metricDescriptor;
        let startTimestamp;
        // The moment when this point was recorded.
        const [currentSeconds, currentNanos] = process.hrtime();
        const now = { seconds: currentSeconds, nanos: currentNanos };
        switch (type) {
            case types_1.MetricDescriptorType.GAUGE_INT64:
            case types_1.MetricDescriptorType.GAUGE_DOUBLE:
                startTimestamp = null;
                break;
            default:
                const [seconds, nanos] = process.hrtime();
                // TODO (mayurkale): This should be set when create Cumulative view.
                startTimestamp = { seconds, nanos };
        }
        const timeseries = [];
        Object.keys(this.rows).forEach(key => {
            const { tags } = this.rows[key];
            const labelValues = metric_utils_1.MetricUtils.tagsToLabelValues(tags);
            const point = this.toPoint(now, this.getSnapshot(tags));
            if (startTimestamp) {
                timeseries.push({ startTimestamp, labelValues, points: [point] });
            }
            else {
                timeseries.push({ labelValues, points: [point] });
            }
        });
        return { descriptor: this.metricDescriptor, timeseries };
    }
    /**
     * Converts snapshot to point
     * @param timestamp The timestamp
     * @param data The aggregated data
     * @returns {Point}
     */
    toPoint(timestamp, data) {
        let value;
        if (data.type === types_2.AggregationType.DISTRIBUTION) {
            // TODO: Add examplar transition
            const { count, sum, sumOfSquaredDeviation } = data;
            value = {
                count,
                sum,
                sumOfSquaredDeviation,
                bucketOptions: { explicit: { bounds: data.buckets } },
                buckets: data.bucketCounts
            };
        }
        else {
            value = data.value;
        }
        return { timestamp, value };
    }
    /**
     * Returns a snapshot of an AggregationData for that tags/labels values.
     * @param tags The desired data's tags
     * @returns {AggregationData}
     */
    getSnapshot(tags) {
        return this.rows[this.encodeTags(tags)];
    }
}
exports.BaseView = BaseView;
//# sourceMappingURL=view.js.map