const promBundle = require("express-prom-bundle");
import * as Prom from "prom-client";
import { MultipleInstanceInterface } from "./multipleInstanceInterface";
import * as Index from "./index.d";
import Settings from "./settings";

export enum AGGREGATOR {
    MIN = 1 << 0,
    AVERAGE = 1 << 1,
    MAX = 1 << 2,
}

interface Aggregator {
    set(value: number): void;
    get(): number;
    reset(): void;
    type(): string;
}

class Max implements Aggregator {
    private max: number = Number.MIN_VALUE;
    
    public set(value: number){
        if(value > this.max){
            this.max = value;
        }
    }

    public get(): number{
        if(this.max != Number.MIN_VALUE)
            return this.max;
        return 0;            
    }

    public reset(){
        this.max = Number.MIN_VALUE;
    }

    public type(): string{
        return "max";
    }
}

class Avg implements Aggregator {
    private sum: number = 0;
    private count: number = 0;
    
    public set(value: number){
        this.sum += value;
        this.count++;
    }

    public get(): number{
        return this.count == 0 ? 0 : (this.sum / this.count);
    }

    public reset(){
        this.sum = 0;
        this.count = 0;
    }

    public type(): string{
        return "avg";
    }
}

class Min implements Aggregator {
    private min: number = Number.MAX_VALUE;
    
    public set(value: number){
        if(this.min > value){
            this.min = value;
        }
    }

    public get(): number{
        if(this.min != Number.MAX_VALUE)
            return this.min;
        return 0;
    }

    public reset(){
        this.min = Number.MAX_VALUE;
    }

    public type(): string{
        return "min";
    }
}

class GaugeWraper extends Prom.Gauge {
    private aggregators: Aggregator[];

    constructor(name: string, help: string, labels?: string[], aggregator?: number) {
        let aggr = [];
        let label = labels != undefined ? labels : [];
        if(aggregator != undefined && aggregator > 0){
            if(aggregator & AGGREGATOR.MAX){
                aggr.push(new Max());
            }
            if(aggregator & AGGREGATOR.AVERAGE){
                aggr.push(new Avg());
            }
            if(aggregator & AGGREGATOR.MIN){
                aggr.push(new Min());
            }
            label.push("aggregator");
        }
        super({
            name: name,
            help: help,
            labelNames: label
        });
        this.aggregators = aggr;
    }

    set(labels: any, value?: number, timestamp?: number | Date): void {
        let _labels = value != undefined ? labels : {};
        let _value = value != undefined ? value : labels;

        if(value == undefined){
            super.set(_value, timestamp);
        }else{
            super.set(JSON.parse(JSON.stringify(labels)), _value, timestamp);            
        }

        this.aggregators.forEach(aggregator => {
            aggregator.set(_value);
            _labels["aggregator"] = aggregator.type();
            super.set(JSON.parse(JSON.stringify(_labels)), aggregator.get());
        });
    }

    reset(){
        super.reset();
        if(this.aggregators != null){
            this.aggregators.forEach(aggregator => {
                aggregator.reset();
            });
        }
    }
}

export default class Registry implements MultipleInstanceInterface {
    static AGGREGATOR = AGGREGATOR;    

    private options: Index.MetricsOptions;
    private registy: Prom.Registry;

    private counters: {[key: string]: Prom.Counter} = {};
    private gauges: {[key: string]: GaugeWraper} = {};
    private histograms: {[key: string]: Prom.Histogram} = {};

    constructor(token: string){
        this.options = Settings.Instance.getMetrics(token);
        let name = this.options != undefined ? this.options.name : undefined;
        if(name == undefined || name == "global"){
            this.registy = promBundle.promClient.register;
        }else{
            this.registy = new promBundle.promClient.Registry();
        }
    }
    
    public counter(name: string): Prom.Counter | null{
        return this.counters[name];
    }

    public gauge(name: string): GaugeWraper | null{
        return this.gauges[name];
    }

    public histogram(name: string): Prom.Histogram | null{
        return this.histograms[name];
    }

    public newCounter(name: string, help: string, labels?: string[]): Prom.Counter{
        let counter = this.counters[name];
        if(counter != null){
            this.registy.removeSingleMetric(name);
        }

        counter = new promBundle.promClient.Counter({
            name: name,
            help: help,
            labelNames: labels
        });
        this.counters[name] = counter;
        this.registy.registerMetric(counter);

        return counter;
    }

    public newGauge(name: string, help: string, labels?: string[], aggregator?: number): GaugeWraper{
        let gauge = this.gauges[name];
        if(gauge != null){
            this.registy.removeSingleMetric(name);
        }

        gauge = new GaugeWraper(name, help, labels, aggregator);
        this.gauges[name] = gauge;
        this.registy.registerMetric(gauge);

        return gauge;
    }

    public newHistogram(name: string, help: string, labels?: string[], buckets?: number[]): Prom.Histogram{
        let histogram = this.histograms[name];
        if(histogram != null){
            this.registy.removeSingleMetric(name);
        }

        var settings: any = {
            name: name,
            help: help,
            labelNames: labels ? labels : []
        }
        if(buckets != null){
            settings.buckets = buckets;
        }

        histogram = new Prom.Histogram(settings);
        this.histograms[name] = histogram;
        this.registy.registerMetric(histogram);

        return histogram;
    }

    public deleteCounter(name: string): void{
        if(this.counters[name] != null){
            this.registy.removeSingleMetric(name);
            delete this.counters[name];
        }
    }

    public deleteGauge(name: string): void{
        if(this.gauges[name] != null){
            this.registy.removeSingleMetric(name);
            delete this.gauges[name];
        }
    }

    public deleteHistogram(name: string): void{
        if(this.histograms[name] != null){
            this.registy.removeSingleMetric(name);
            delete this.histograms[name];
        }
    }

    public getRegistry(): Prom.Registry{
        return this.registy;
    }

    public destroy(){
        this.registy.clear();
        this.counters = {};
        this.gauges = {};
        this.histograms = {};
    }
    public getStatus(){
        return "";
    }
    public isFunctional(){
        return true;
    }

    /**
	 * Get string representation for all metrics
	 */
    public metrics(opts?: Prom.MetricsOpts): string{
        return this.registy.metrics(opts);
    }
    
    /**
     * Reset all metrics in the registry
     */
    public resetMetrics(): void{
        this.registy.resetMetrics();
    }

    /**
     * Get all metrics as objects
     */
    public getMetricsAsJSON(): Prom.metric[]{
        return this.registy.getMetricsAsJSON();
    }

    /**
     * Set static labels to every metric emitted by this registry
     * @param labels of name/value pairs:
     * { defaultLabel: "value", anotherLabel: "value 2" }
     */
    public setDefaultLabels(labels: Object): void{
        this.registy.setDefaultLabels(labels);
    }

    public get contentType(){
        return this.registy.contentType;
    }
    
}